Compare commits

...

26 Commits

Author SHA1 Message Date
Jamie V
f4849bcc04 source maps fo debuggin 2024-03-12 14:20:42 -07:00
Andrew Henry
5f0bd10c61 Request batch when idle (#7526)
See https://github.com/nasa/openmct/pull/7526 for details
2024-03-12 20:46:06 +00:00
Jesse Mazzella
8c2558bfe0 fix(#7524): Open in New Tab action from a sub-object in a layout (#7542)
* refactor: url tools use named exports

* fix: refactor method and remove customUrlParams

* test(e2e): verify bounds are preserved in data pivoting

* test: remove test as feature is no longer needed

- dataVisualization logic has moved from MMGIS plugin to the open source. As such, we can just use the time conductor bounds

* refactor: autoformat keeps changing this so i'mma just commit it

* refactor: remove unnecessary code

* refactor: simplify, add docs

* Revert "refactor: remove unnecessary code"

This reverts commit 87aef35c510230835fb682b80e89a6006ef2d923.

* a11y: improve aria labels for ITC

* fix: simplify url method

* fix: update ITC app actions

* test: add test to generate test data for display layout w/ overlay plot + ITC enabled

* test(e2e): add suite + test for open in new tab from subobject

- needs cleanup

* a11y: various a11y improvement drivebys

* a11y: clock indicator needs to be quiet

* a11y: add `aria-live` to SuperMenu details

* a11y: greatly improve a11y of Menus and SuperMenus

* test(e2e): clean up test

* fix: improve a11y for context menus, fix test

* chore: remove nop-longer-recommended extension

* feat: provide one more bound option for example data viz

* fix: no need for `mount`, use dynamic rendering instead

* Revert "fix: simplify url method"

This reverts commit b24c7dabc783a9a1c3f2460eada99f452259f566.

* fix: correct time conductor bounds when opening in a new tab from a plot in the inspector

* test: fix e2e tests

* Revert "test: remove test as feature is no longer needed"

This reverts commit 759ebd4667bffb1979d5f62af6b47f349dcd9f77.

* test: move 2p annotation to test

* test: fix e2e

* fix: no words for the word god today

* test: fix e2e

* fix: e2e test

* test: fix test

* driveby: fix perf test

* fix: revert required prop change

---------

Co-authored-by: David Tsay <3614296+davetsay@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-03-11 16:39:38 -07:00
dependabot[bot]
0eadc7a4ae chore(deps-dev): bump copy-webpack-plugin from 11.0.0 to 12.0.2 (#7574)
Bumps [copy-webpack-plugin](https://github.com/webpack-contrib/copy-webpack-plugin) from 11.0.0 to 12.0.2.
- [Release notes](https://github.com/webpack-contrib/copy-webpack-plugin/releases)
- [Changelog](https://github.com/webpack-contrib/copy-webpack-plugin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/copy-webpack-plugin/compare/v11.0.0...v12.0.2)

---
updated-dependencies:
- dependency-name: copy-webpack-plugin
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-03-11 16:33:15 -07:00
dependabot[bot]
cad4652a08 chore(deps-dev): bump karma-webpack from 5.0.0 to 5.0.1 (#7575)
Bumps [karma-webpack](https://github.com/webpack-contrib/karma-webpack) from 5.0.0 to 5.0.1.
- [Release notes](https://github.com/webpack-contrib/karma-webpack/releases)
- [Changelog](https://github.com/codymikol/karma-webpack/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/karma-webpack/compare/v5.0.0...v5.0.1)

---
updated-dependencies:
- dependency-name: karma-webpack
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-03-11 16:03:50 -07:00
dependabot[bot]
8379f2d073 chore(deps-dev): bump imports-loader from 4.0.1 to 5.0.0 (#7576)
Bumps [imports-loader](https://github.com/webpack-contrib/imports-loader) from 4.0.1 to 5.0.0.
- [Release notes](https://github.com/webpack-contrib/imports-loader/releases)
- [Changelog](https://github.com/webpack-contrib/imports-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/imports-loader/compare/v4.0.1...v5.0.0)

---
updated-dependencies:
- dependency-name: imports-loader
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-03-11 16:03:12 -07:00
dependabot[bot]
8ed112a4a8 chore(deps-dev): bump sass from 1.68.0 to 1.71.1 (#7577)
Bumps [sass](https://github.com/sass/dart-sass) from 1.68.0 to 1.71.1.
- [Release notes](https://github.com/sass/dart-sass/releases)
- [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sass/dart-sass/compare/1.68.0...1.71.1)

---
updated-dependencies:
- dependency-name: sass
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-03-11 15:42:05 -07:00
dependabot[bot]
14c58c4410 chore(deps-dev): bump npm-run-all2 from 6.1.1 to 6.1.2 (#7579)
Bumps [npm-run-all2](https://github.com/bcomnes/npm-run-all2) from 6.1.1 to 6.1.2.
- [Release notes](https://github.com/bcomnes/npm-run-all2/releases)
- [Changelog](https://github.com/bcomnes/npm-run-all2/blob/master/CHANGELOG.md)
- [Commits](https://github.com/bcomnes/npm-run-all2/compare/v6.1.1...v6.1.2)

---
updated-dependencies:
- dependency-name: npm-run-all2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-11 15:40:13 -07:00
Rukmini Bose (Ruki)
87ba9fcbc0 [Mobile] Make Time Conductor Usable Again (#7515)
* Initial changes to refactor Time Conductor

* Finish refactor using grid-template

* Finish total refactor of Time Conductor

* Initial mobile changes

* Fix TC on mobile by changing grid template

* Fix more mobile stuff

* Add ellipsize to TC popup options and rearrange popup inputs and labels

* Small final changes so TC is adaptive to extreme cases

* Add e2e mobile test

---------

Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-03-08 12:22:09 -08:00
Jesse Mazzella
ab49f3f3a1 fix(#7215)[Fault Management]: Fix shelving and acknowledging faults with single and bulk actions (#7559)
* refactor: merge FaultManagementListView into FaultManagementView

* refactor: make `selectedFaults` a computed property

* refactor: use named exports

* fix: reset fault map AFTER selectedFaults have been acknowledged

* a11y: add aria labels for fault management toolbar buttons

* refactor: use named import/exports

* a11y: add label

* a11y: add aria label for checkboxes

* fix: acknowledging or shelving single fault from context menu should only apply to selected fault

* refactor: use change event instead of input event for checkbox

* test: fix e2e tests, remove expect.softs

* test: stabilize fault management e2e tests
2024-03-06 15:28:38 -08:00
Shefali Joshi
df969722d1 d3 implementation of progress pie chart (#7485)
* d3 implementation of progress pie chart

* Handle 0% and 100% cases

* PR #7485
- Minor tweaks to `s-selected` styling.

* add in-progress class for compact view

* Fix issue where updating progress for pie chart wasn't working till at least one clock tick.
Write tests for progress pie

* update documentation for clock annotation

* Update clock annotation in tests

* split long testfile

* driveby missing test

* driveby fix flake

* temp: fix flake and prep for visual test

* Fix linting errors

* this should be resolved

* these keep popping up

* moving some of this around

* moving this around

* the test

* Fix imports for tests

* no longer need constant

* move to front

* Stabalize name

* test(missionStatus): fix visual test

---------

Co-authored-by: Charles Hacskaylo <charles.f.hacskaylo@nasa.gov>
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
2024-03-05 14:45:28 -08:00
dependabot[bot]
ef62633df1 chore(deps-dev): bump sass-loader from 14.0.0 to 14.1.1 (#7512)
Bumps [sass-loader](https://github.com/webpack-contrib/sass-loader) from 14.0.0 to 14.1.1.
- [Release notes](https://github.com/webpack-contrib/sass-loader/releases)
- [Changelog](https://github.com/webpack-contrib/sass-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/sass-loader/compare/v14.0.0...v14.1.1)

---
updated-dependencies:
- dependency-name: sass-loader
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-03-04 19:10:15 -08:00
Jesse Mazzella
597dc58eb7 chore: minor formatting fix for PR template (#7550)
Minor formatting nitpick
2024-03-04 19:09:33 -08:00
John Hill
5d00d642f3 [CI] Temp disable flakefinder as we it's generating too many GHA minutes (#7548)
Temp disable flakefinder as we it's generating too many GHA minutes
2024-03-04 18:32:12 -08:00
Jesse Mazzella
39ab81c3d0 fix(overlay plot): legend updates correctly when removing element via remove action (#7531)
* fix: remove duplicate listeners
* fix: if no series found, don't splice
* test(e2e): update legend a11y and add test
2024-03-05 02:07:44 +00:00
Shefali Joshi
0bdd0963a4 Limit lines handle plot resizing (#7151)
* Fix error when removing staleness subscription due to incorrect parameter

* On resize, clear the drawing API to reset the height and width for point calculation.

* Add e2e test to test limit lines after resizing the plot view.

* We need to update viewport when drawing limits in case there is no data for plots.

* Address review comments. change event naming convention and reduce debounce time.

* Use limit line and label seriesKeys to make ids unique

* Improve locator for limit lines checkbox

* Add a check for network requests when limit lines are redrawn
2024-03-04 17:02:30 -08:00
dependabot[bot]
eae51356c8 chore(deps-dev): bump plotly.js-basic-dist-min from 2.20.0 to 2.29.1 (#7523)
Bumps [plotly.js-basic-dist-min](https://github.com/plotly/plotly.js) from 2.20.0 to 2.29.1.
- [Release notes](https://github.com/plotly/plotly.js/releases)
- [Changelog](https://github.com/plotly/plotly.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/plotly/plotly.js/compare/v2.20.0...v2.29.1)

---
updated-dependencies:
- dependency-name: plotly.js-basic-dist-min
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-03-04 16:22:18 -08:00
dependabot[bot]
a36ad3f5e7 chore(deps-dev): bump webpack-dev-server from 4.15.1 to 5.0.2 (#7544)
Bumps [webpack-dev-server](https://github.com/webpack/webpack-dev-server) from 4.15.1 to 5.0.2.
- [Release notes](https://github.com/webpack/webpack-dev-server/releases)
- [Changelog](https://github.com/webpack/webpack-dev-server/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-server/compare/v4.15.1...v5.0.2)

---
updated-dependencies:
- dependency-name: webpack-dev-server
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-03-04 16:11:50 -08:00
dependabot[bot]
307ededd19 chore(deps-dev): bump eslint-plugin-vue from 9.18.1 to 9.22.0 (#7545)
Bumps [eslint-plugin-vue](https://github.com/vuejs/eslint-plugin-vue) from 9.18.1 to 9.22.0.
- [Release notes](https://github.com/vuejs/eslint-plugin-vue/releases)
- [Commits](https://github.com/vuejs/eslint-plugin-vue/compare/v9.18.1...v9.22.0)

---
updated-dependencies:
- dependency-name: eslint-plugin-vue
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-03-04 15:57:12 -08:00
dependabot[bot]
d7ecfdf10f chore(deps-dev): bump webpack from 5.89.0 to 5.90.3 (#7546)
Bumps [webpack](https://github.com/webpack/webpack) from 5.89.0 to 5.90.3.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.89.0...v5.90.3)

---
updated-dependencies:
- dependency-name: webpack
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-03-04 13:42:26 -08:00
dependabot[bot]
a1c36f314d chore(deps-dev): bump sanitize-html from 2.11.0 to 2.12.1 (#7539)
Bumps [sanitize-html](https://github.com/apostrophecms/sanitize-html) from 2.11.0 to 2.12.1.
- [Changelog](https://github.com/apostrophecms/sanitize-html/blob/main/CHANGELOG.md)
- [Commits](https://github.com/apostrophecms/sanitize-html/compare/2.11.0...2.12.1)

---
updated-dependencies:
- dependency-name: sanitize-html
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-03-04 13:20:17 -08:00
dependabot[bot]
86e636cbce chore(deps-dev): bump @axe-core/playwright from 4.8.2 to 4.8.5 (#7496)
Bumps [@axe-core/playwright](https://github.com/dequelabs/axe-core-npm) from 4.8.2 to 4.8.5.
- [Release notes](https://github.com/dequelabs/axe-core-npm/releases)
- [Changelog](https://github.com/dequelabs/axe-core-npm/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/dequelabs/axe-core-npm/commits/v4.8.5)

---
updated-dependencies:
- dependency-name: "@axe-core/playwright"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-03-03 09:06:49 -08:00
dependabot[bot]
0d2b36ae82 chore(deps-dev): bump vue-eslint-parser from 9.3.2 to 9.4.2 (#7471)
Bumps [vue-eslint-parser](https://github.com/vuejs/vue-eslint-parser) from 9.3.2 to 9.4.2.
- [Release notes](https://github.com/vuejs/vue-eslint-parser/releases)
- [Commits](https://github.com/vuejs/vue-eslint-parser/compare/v9.3.2...v9.4.2)

---
updated-dependencies:
- dependency-name: vue-eslint-parser
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-03-03 08:55:45 -08:00
dependabot[bot]
95072da257 chore(deps-dev): bump marked from 11.2.0 to 12.0.0 (#7467)
Bumps [marked](https://github.com/markedjs/marked) from 11.2.0 to 12.0.0.
- [Release notes](https://github.com/markedjs/marked/releases)
- [Changelog](https://github.com/markedjs/marked/blob/master/.releaserc.json)
- [Commits](https://github.com/markedjs/marked/compare/v11.2.0...v12.0.0)

---
updated-dependencies:
- dependency-name: marked
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-03-03 08:41:49 -08:00
dependabot[bot]
faa2621e26 chore(deps-dev): bump css-loader from 6.8.1 to 6.10.0 (#7466)
Bumps [css-loader](https://github.com/webpack-contrib/css-loader) from 6.8.1 to 6.10.0.
- [Release notes](https://github.com/webpack-contrib/css-loader/releases)
- [Changelog](https://github.com/webpack-contrib/css-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/css-loader/compare/v6.8.1...v6.10.0)

---
updated-dependencies:
- dependency-name: css-loader
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-03-03 08:29:37 -08:00
Rukmini Bose (Ruki)
73eead6b72 [Mobile] Center Confirmation Dialog Texts (#7492)
* Make overlay messages centered

* Fix changes so that only dialogs and not forms are affected

* Fix buttons such that they are right-aligned

* Reduce to one worker for stability

* Add test to cover new capabilities

* lint fixes

* Closes #7343
- Fixed an oversight that caused the top of form dialogs to
be scrolled out of view by default.
- Fixed approach to vertical centering for `-fit` type confirmation dialogs.
- Reduced size of confirmation dialog icons.
- Smoke tested in Chrome mobile emulator in a large variety of mobile
viewport sizes and orientations.

* Closes #7343
- Removes extra margin unintentionally added to `l-overlay-large`.

---------

Co-authored-by: John Hill <john.c.hill@nasa.gov>
Co-authored-by: Charles Hacskaylo <charles.f.hacskaylo@nasa.gov>
2024-03-03 16:14:45 +00:00
96 changed files with 2213 additions and 1131 deletions

View File

@@ -8,7 +8,7 @@ Closes <!--- Insert Issue Number(s) this PR addresses. Start by typing # will op
* [ ] Have you followed the guidelines in our [Contributing document](https://github.com/nasa/openmct/blob/master/CONTRIBUTING.md)?
* [ ] Have you checked to ensure there aren't other open [Pull Requests](https://github.com/nasa/openmct/pulls) for the same update/change?
* [ ] Is this a notable change that will require a special callout in the release notes [Notable Change](../docs/src/process/release.md) ? For example, will this break compatibility with existing APIs or projects which source these plugins?
* [ ] Is this a [notable change](../docs/src/process/release.md) that will require a special callout in the release notes? For example, will this break compatibility with existing APIs or projects that consume these plugins?
### Author Checklist

View File

@@ -1,15 +1,15 @@
name: 'pr:e2e:flakefinder'
on:
push:
branches: master
# push:
# branches: master
workflow_dispatch:
pull_request:
types:
- labeled
- opened
schedule:
- cron: '0 0 * * *'
# pull_request:
# types:
# - labeled
# - opened
# schedule:
# - cron: '0 0 * * *'
jobs:
e2e-flakefinder:

View File

@@ -5,7 +5,6 @@
// List of extensions which should be recommended for users of this workspace.
"recommendations": [
"Vue.volar",
"Vue.vscode-typescript-vue-plugin",
"dbaeumer.vscode-eslint",
"rvest.vs-code-prettier-eslint"
],

View File

@@ -15,5 +15,5 @@ export default merge(common, {
__OPENMCT_ROOT_RELATIVE__: '""'
})
],
devtool: 'source-map'
devtool: 'eval-source-map'
});

View File

@@ -238,6 +238,7 @@ Current list of test tags:
|`@unstable` | A new test or test which is known to be flaky.|
|`@2p` | Indicates that multiple users are involved, or multiple tabs/pages are used. Useful for testing multi-user interactivity.|
|`@generatedata` | Indicates that a test is used to generate testdata or test the generated test data. Usually to be associated with localstorage, but this may grow over time.|
|`@clock` | A test which modifies the clock. These have expanded out of the visual tests and into the functional tests.
### Continuous Integration
@@ -447,6 +448,7 @@ By adhering to this principle, we can create tests that are both robust and refl
- Utilize `percyCSS` to ignore time-based elements. For more details, consult our [percyCSS file](./.percy.ci.yml).
- Use Open MCT's fixed-time mode unless explicitly testing realtime clock
- Employ the `createExampleTelemetryObject` appAction to source telemetry and specify a `name` to avoid autogenerated names.
- Avoid creating objects with a time component like timers and clocks.
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')`

View File

@@ -505,15 +505,14 @@ async function setTimeConductorBounds(page, startDate, endDate) {
* @param {string} startDate
* @param {string} endDate
*/
async function setIndependentTimeConductorBounds(page, startDate, endDate) {
// Activate Independent Time Conductor in Fixed Time Mode
await page.getByRole('switch').click();
async function setIndependentTimeConductorBounds(page, { start, end }) {
// Activate Independent Time Conductor
await page.getByLabel('Enable Independent Time Conductor').click();
// Bring up the time conductor popup
await page.click('.c-conductor-holder--compact .c-compact-tc');
await page.getByLabel('Independent Time Conductor Settings').click();
await expect(page.locator('.itc-popout')).toBeInViewport();
await setTimeBounds(page, startDate, endDate);
await setTimeBounds(page, start, end);
await page.keyboard.press('Enter');
}

View File

@@ -21,10 +21,12 @@
*****************************************************************************/
import { fileURLToPath } from 'url';
import { expect } from '../pluginFixtures.js';
/**
* @param {import('@playwright/test').Page} page
*/
async function navigateToFaultManagementWithExample(page) {
export async function navigateToFaultManagementWithExample(page) {
await page.addInitScript({
path: fileURLToPath(new URL('./addInitExampleFaultProvider.js', import.meta.url))
});
@@ -35,7 +37,7 @@ async function navigateToFaultManagementWithExample(page) {
/**
* @param {import('@playwright/test').Page} page
*/
async function navigateToFaultManagementWithStaticExample(page) {
export async function navigateToFaultManagementWithStaticExample(page) {
await page.addInitScript({
path: fileURLToPath(new URL('./addInitExampleFaultProviderStatic.js', import.meta.url))
});
@@ -46,7 +48,7 @@ async function navigateToFaultManagementWithStaticExample(page) {
/**
* @param {import('@playwright/test').Page} page
*/
async function navigateToFaultManagementWithoutExample(page) {
export async function navigateToFaultManagementWithoutExample(page) {
await page.addInitScript({
path: fileURLToPath(new URL('./addInitFaultManagementPlugin.js', import.meta.url))
});
@@ -57,7 +59,7 @@ async function navigateToFaultManagementWithoutExample(page) {
/**
* @param {import('@playwright/test').Page} page
*/
async function navigateToFaultItemInTree(page) {
export async function navigateToFaultItemInTree(page) {
await page.goto('./', { waitUntil: 'networkidle' });
const faultManagementTreeItem = page
@@ -75,88 +77,95 @@ async function navigateToFaultItemInTree(page) {
/**
* @param {import('@playwright/test').Page} page
*/
async function acknowledgeFault(page, rowNumber) {
export async function acknowledgeFault(page, rowNumber) {
await openFaultRowMenu(page, rowNumber);
await page.locator('.c-menu >> text="Acknowledge"').click();
// Click [aria-label="Save"]
await page.locator('[aria-label="Save"]').click();
await page.getByLabel('Acknowledge', { exact: true }).click();
await page.getByLabel('Save').click();
}
/**
* @param {import('@playwright/test').Page} page
*/
async function shelveMultipleFaults(page, ...nums) {
export async function shelveMultipleFaults(page, ...nums) {
const selectRows = nums.map((num) => {
return selectFaultItem(page, num);
});
await Promise.all(selectRows);
await page.locator('button:has-text("Shelve")').click();
await page.locator('[aria-label="Save"]').click();
await page.getByLabel('Shelve selected faults').click();
await page.getByLabel('Save').click();
}
/**
* @param {import('@playwright/test').Page} page
*/
async function acknowledgeMultipleFaults(page, ...nums) {
export async function acknowledgeMultipleFaults(page, ...nums) {
const selectRows = nums.map((num) => {
return selectFaultItem(page, num);
});
await Promise.all(selectRows);
await page.locator('button:has-text("Acknowledge")').click();
await page.locator('[aria-label="Save"]').click();
await page.getByLabel('Save').click();
}
/**
* @param {import('@playwright/test').Page} page
*/
async function shelveFault(page, rowNumber) {
export async function shelveFault(page, rowNumber) {
await openFaultRowMenu(page, rowNumber);
await page.locator('.c-menu >> text="Shelve"').click();
// Click [aria-label="Save"]
await page.locator('[aria-label="Save"]').click();
await page.getByLabel('Save').click();
}
/**
* @param {import('@playwright/test').Page} page
*/
async function changeViewTo(page, view) {
export async function changeViewTo(page, view) {
await page.locator('.c-fault-mgmt__search-row select').first().selectOption(view);
}
/**
* @param {import('@playwright/test').Page} page
*/
async function sortFaultsBy(page, sort) {
export async function sortFaultsBy(page, sort) {
await page.locator('.c-fault-mgmt__list-header-sortButton select').selectOption(sort);
}
/**
* @param {import('@playwright/test').Page} page
*/
async function enterSearchTerm(page, term) {
export async function enterSearchTerm(page, term) {
await page.locator('.c-fault-mgmt-search [aria-label="Search Input"]').fill(term);
}
/**
* @param {import('@playwright/test').Page} page
*/
async function clearSearch(page) {
export async function clearSearch(page) {
await enterSearchTerm(page, '');
}
/**
* @param {import('@playwright/test').Page} page
*/
async function selectFaultItem(page, rowNumber) {
await page.locator(`.c-fault-mgmt-item > input >> nth=${rowNumber - 1}`).check();
export async function selectFaultItem(page, rowNumber) {
await page
.getByLabel('Select fault')
.nth(rowNumber - 1)
.check({
// Need force here because checkbox state is changed by an event emitted by the checkbox
// eslint-disable-next-line playwright/no-force-option
force: true
});
await expect(page.getByLabel('Select fault').nth(rowNumber - 1)).toBeChecked();
}
/**
* @param {import('@playwright/test').Page} page
*/
async function getHighestSeverity(page) {
export async function getHighestSeverity(page) {
const criticalCount = await page.locator('[title=CRITICAL]').count();
const warningCount = await page.locator('[title=WARNING]').count();
@@ -172,7 +181,7 @@ async function getHighestSeverity(page) {
/**
* @param {import('@playwright/test').Page} page
*/
async function getLowestSeverity(page) {
export async function getLowestSeverity(page) {
const warningCount = await page.locator('[title=WARNING]').count();
const watchCount = await page.locator('[title=WATCH]').count();
@@ -188,7 +197,7 @@ async function getLowestSeverity(page) {
/**
* @param {import('@playwright/test').Page} page
*/
async function getFaultResultCount(page) {
export async function getFaultResultCount(page) {
const count = await page.locator('.c-faults-list-view-item-body > .c-fault-mgmt__list').count();
return count;
@@ -197,7 +206,7 @@ async function getFaultResultCount(page) {
/**
* @param {import('@playwright/test').Page} page
*/
function getFault(page, rowNumber) {
export function getFault(page, rowNumber) {
const fault = page.locator(
`.c-faults-list-view-item-body > .c-fault-mgmt__list >> nth=${rowNumber - 1}`
);
@@ -208,7 +217,7 @@ function getFault(page, rowNumber) {
/**
* @param {import('@playwright/test').Page} page
*/
function getFaultByName(page, name) {
export function getFaultByName(page, name) {
const fault = page.locator(`.c-fault-mgmt__list-faultname:has-text("${name}")`);
return fault;
@@ -217,7 +226,7 @@ function getFaultByName(page, name) {
/**
* @param {import('@playwright/test').Page} page
*/
async function getFaultName(page, rowNumber) {
export async function getFaultName(page, rowNumber) {
const faultName = await page
.locator(`.c-fault-mgmt__list-faultname >> nth=${rowNumber - 1}`)
.textContent();
@@ -228,7 +237,7 @@ async function getFaultName(page, rowNumber) {
/**
* @param {import('@playwright/test').Page} page
*/
async function getFaultSeverity(page, rowNumber) {
export async function getFaultSeverity(page, rowNumber) {
const faultSeverity = await page
.locator(`.c-faults-list-view-item-body .c-fault-mgmt__list-severity >> nth=${rowNumber - 1}`)
.getAttribute('title');
@@ -239,7 +248,7 @@ async function getFaultSeverity(page, rowNumber) {
/**
* @param {import('@playwright/test').Page} page
*/
async function getFaultNamespace(page, rowNumber) {
export async function getFaultNamespace(page, rowNumber) {
const faultNamespace = await page
.locator(`.c-fault-mgmt__list-path >> nth=${rowNumber - 1}`)
.textContent();
@@ -250,7 +259,7 @@ async function getFaultNamespace(page, rowNumber) {
/**
* @param {import('@playwright/test').Page} page
*/
async function getFaultTriggerTime(page, rowNumber) {
export async function getFaultTriggerTime(page, rowNumber) {
const faultTriggerTime = await page
.locator(`.c-fault-mgmt__list-trigTime >> nth=${rowNumber - 1} >> .c-fault-mgmt-item__value`)
.textContent();
@@ -261,35 +270,10 @@ async function getFaultTriggerTime(page, rowNumber) {
/**
* @param {import('@playwright/test').Page} page
*/
async function openFaultRowMenu(page, rowNumber) {
export async function openFaultRowMenu(page, rowNumber) {
// select
await page
.locator(`.c-fault-mgmt-item > .c-fault-mgmt__list-action-button >> nth=${rowNumber - 1}`)
.getByLabel('Disposition actions')
.nth(rowNumber - 1)
.click();
}
export {
acknowledgeFault,
acknowledgeMultipleFaults,
changeViewTo,
clearSearch,
enterSearchTerm,
getFault,
getFaultByName,
getFaultName,
getFaultNamespace,
getFaultResultCount,
getFaultSeverity,
getFaultTriggerTime,
getHighestSeverity,
getLowestSeverity,
navigateToFaultItemInTree,
navigateToFaultManagementWithExample,
navigateToFaultManagementWithoutExample,
navigateToFaultManagementWithStaticExample,
openFaultRowMenu,
selectFaultItem,
shelveFault,
shelveMultipleFaults,
sortFaultsBy
};

View File

@@ -20,6 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { createDomainObjectWithDefaults, createPlanFromJSON } from '../appActions.js';
import { expect } from '../pluginFixtures.js';
/**
@@ -142,6 +143,18 @@ export function getLatestEndTime(planJson) {
return Math.max(...activities.map((activity) => activity.end));
}
/**
*
* @param {object} planJson
* @returns {object}
*/
export function getFirstActivity(planJson) {
const groups = Object.keys(planJson);
const firstGroupKey = groups[0];
const firstGroupItems = planJson[firstGroupKey];
return firstGroupItems[0];
}
/**
* Uses the Open MCT API to set the status of a plan to 'draft'.
* @param {import('@playwright/test').Page} page
@@ -172,3 +185,55 @@ export async function addPlanGetInterceptor(page) {
});
});
}
/**
* Create a Plan from JSON and add it to a Timelist and Navigate to the Plan view
* @param {import('@playwright/test').Page} page
*/
export async function createTimelistWithPlanAndSetActivityInProgress(page, planJson) {
await page.goto('./', { waitUntil: 'domcontentloaded' });
const timelist = await createDomainObjectWithDefaults(page, {
name: 'Time List',
type: 'Time List'
});
await createPlanFromJSON(page, {
name: 'Test Plan',
json: planJson,
parent: timelist.uuid
});
// Ensure that all activities are shown in the expanded view
const groups = Object.keys(planJson);
const firstGroupKey = groups[0];
const firstGroupItems = planJson[firstGroupKey];
const firstActivityForPlan = firstGroupItems[0];
const lastActivity = firstGroupItems[firstGroupItems.length - 1];
const startBound = firstActivityForPlan.start;
const endBound = lastActivity.end;
// Switch to fixed time mode with all plan events within the bounds
await page.goto(
`${timelist.url}?tc.mode=fixed&tc.startBound=${startBound}&tc.endBound=${endBound}&tc.timeSystem=utc&view=timelist.view`
);
// Change the object to edit mode
await page.getByRole('button', { name: 'Edit Object' }).click();
// Find the display properties section in the inspector
await page.getByRole('tab', { name: 'View Properties' }).click();
// Switch to expanded view and save the setting
await page.getByLabel('Display Style').selectOption({ label: 'Expanded' });
// Click on the "Save" button
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
const anActivity = page.getByRole('row').nth(0);
// Set the activity to in progress
await anActivity.click();
await page.getByRole('tab', { name: 'Activity' }).click();
await page.getByLabel('Activity Status', { exact: true }).selectOption({ label: 'In progress' });
}

View File

@@ -3,7 +3,6 @@
import { devices } from '@playwright/test';
const MAX_FAILURES = 5;
const NUM_WORKERS = 2;
import { fileURLToPath } from 'url';
@@ -20,7 +19,8 @@ const config = {
reuseExistingServer: true //This was originally disabled to prevent differences in local debugging vs. CI. However, it significantly speeds up local debugging.
},
maxFailures: MAX_FAILURES, //Limits failures to 5 to reduce CI Waste
workers: NUM_WORKERS, //Limit to 2 for CircleCI Agent
workers: 1, //Limit to 1 due to resource constraints similar to https://github.com/percy/cli/discussions/1067
use: {
baseURL: 'http://localhost:8080/',
headless: true,

File diff suppressed because one or more lines are too long

View File

@@ -174,6 +174,6 @@ test.describe('AppActions', () => {
type: 'Folder'
});
await openObjectTreeContextMenu(page, folder.url);
await expect(page.getByLabel('Menu')).toBeVisible();
await expect(page.getByLabel(`${folder.name} Context Menu`)).toBeVisible();
});
});

View File

@@ -33,13 +33,18 @@
import { fileURLToPath } from 'url';
import { createDomainObjectWithDefaults, createExampleTelemetryObject } from '../../appActions.js';
import {
createDomainObjectWithDefaults,
createExampleTelemetryObject,
setIndependentTimeConductorBounds,
setTimeConductorBounds
} from '../../appActions.js';
import { MISSION_TIME } from '../../constants.js';
import { expect, test } from '../../pluginFixtures.js';
const overlayPlotName = 'Overlay Plot with Telemetry Object';
test.describe('Generate Visual Test Data @localStorage @generatedata', () => {
test.describe('Generate Visual Test Data @localStorage @generatedata @clock', () => {
test.use({
clockOptions: {
now: MISSION_TIME,
@@ -89,6 +94,53 @@ test.describe('Generate Visual Test Data @localStorage @generatedata', () => {
});
});
test('Generate display layout with 1 child overlay plot', async ({ page, context }) => {
const parent = await createDomainObjectWithDefaults(page, {
type: 'Display Layout',
name: 'Parent Display Layout'
});
const overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot',
name: 'Child Overlay Plot 1',
parent: parent.uuid
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'Child SWG 1',
parent: overlayPlot.uuid
});
await page.goto(parent.url, { waitUntil: 'domcontentloaded' });
await setIndependentTimeConductorBounds(page, {
start: '2024-11-12 19:11:11.000Z',
end: '2024-11-12 20:11:11.000Z'
});
const NEW_GLOBAL_START_BOUNDS = '2024-11-11 19:11:11.000Z';
const NEW_GLOBAL_END_BOUNDS = '2024-11-11 20:11:11.000Z';
await setTimeConductorBounds(page, NEW_GLOBAL_START_BOUNDS, NEW_GLOBAL_END_BOUNDS);
// Verify that the global time conductor bounds have been updated
expect(
await page.getByLabel('Global Time Conductor').getByLabel('Start bounds').textContent()
).toEqual(NEW_GLOBAL_START_BOUNDS);
expect(
await page.getByLabel('Global Time Conductor').getByLabel('End bounds').textContent()
).toEqual(NEW_GLOBAL_END_BOUNDS);
//Save localStorage for future test execution
await context.storageState({
path: fileURLToPath(
new URL(
'../../../e2e/test-data/display_layout_with_child_overlay_plot.json',
import.meta.url
)
)
});
});
test('Generate flexible layout with 2 child display layouts', async ({ page, context }) => {
// Create Display Layout
const parent = await createDomainObjectWithDefaults(page, {

View File

@@ -22,29 +22,13 @@
import fs from 'fs';
import { createDomainObjectWithDefaults, createPlanFromJSON } from '../../../appActions.js';
import { getEarliestStartTime } from '../../../helper/planningUtils';
import { expect, test } from '../../../pluginFixtures.js';
const examplePlanSmall3 = JSON.parse(
fs.readFileSync(
new URL('../../../test-data/examplePlans/ExamplePlan_Small3.json', import.meta.url)
)
);
const examplePlanSmall1 = JSON.parse(
fs.readFileSync(
new URL('../../../test-data/examplePlans/ExamplePlan_Small1.json', import.meta.url)
)
);
// eslint-disable-next-line no-unused-vars
const START_TIME_COLUMN = 0;
// eslint-disable-next-line no-unused-vars
const END_TIME_COLUMN = 1;
const TIME_TO_FROM_COLUMN = 2;
// eslint-disable-next-line no-unused-vars
const ACTIVITY_COLUMN = 3;
const HEADER_ROW = 0;
const NUM_COLUMNS = 5;
test.describe('Time List', () => {
test("Create a Time List, add a single Plan to it, verify all the activities are displayed with no milliseconds and selecting an activity shows it's properties", async ({
page
@@ -161,7 +145,7 @@ test("View a timelist in expanded view, verify all the activities are displayed
await expect(eventCount).toEqual(firstGroupItems.length);
});
await test.step('Shows activity properties when a row is selected', async () => {
await test.step('Shows activity properties when a row is selected in the expanded view', async () => {
await page.getByRole('row').nth(2).click();
// Find the activity state section in the inspector
@@ -171,167 +155,10 @@ test("View a timelist in expanded view, verify all the activities are displayed
'Not started'
);
});
});
/**
* The regular expression used to parse the countdown string.
* Some examples of valid Countdown strings:
* ```
* '35D 02:03:04'
* '-1D 01:02:03'
* '01:02:03'
* '-05:06:07'
* ```
*/
const COUNTDOWN_REGEXP = /(-)?(\d+D\s)?(\d{2}):(\d{2}):(\d{2})/;
/**
* @typedef {Object} CountdownOrUpObject
* @property {string} sign - The sign of the countdown ('-' if the countdown is negative, '+' otherwise).
* @property {string} days - The number of days in the countdown (undefined if there are no days).
* @property {string} hours - The number of hours in the countdown.
* @property {string} minutes - The number of minutes in the countdown.
* @property {string} seconds - The number of seconds in the countdown.
* @property {string} toString - The countdown string.
*/
/**
* Object representing the indices of the capture groups in a countdown regex match.
*
* @typedef {{ SIGN: number, DAYS: number, HOURS: number, MINUTES: number, SECONDS: number, REGEXP: RegExp }}
* @property {number} SIGN - The index for the sign capture group (1 if a '-' sign is present, otherwise undefined).
* @property {number} DAYS - The index for the days capture group (2 for the number of days, otherwise undefined).
* @property {number} HOURS - The index for the hours capture group (3 for the hour part of the time).
* @property {number} MINUTES - The index for the minutes capture group (4 for the minute part of the time).
* @property {number} SECONDS - The index for the seconds capture group (5 for the second part of the time).
*/
const COUNTDOWN = Object.freeze({
SIGN: 1,
DAYS: 2,
HOURS: 3,
MINUTES: 4,
SECONDS: 5
});
test.describe('Time List with controlled clock', () => {
test.use({
clockOptions: {
now: getEarliestStartTime(examplePlanSmall3),
shouldAdvanceTime: true
}
});
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
});
test('Time List shows current events and counts down correctly in real-time mode', async ({
page
}) => {
await test.step('Create a Time List, add a Plan to it, and switch to real-time mode', async () => {
// Create Time List
const timelist = await createDomainObjectWithDefaults(page, {
type: 'Time List'
});
// Create a Plan with events that count down and up.
// Add it as a child to the Time List.
await createPlanFromJSON(page, {
json: examplePlanSmall3,
parent: timelist.uuid
});
// Navigate to the Time List in real-time mode
await page.goto(
`${timelist.url}?tc.mode=local&tc.startDelta=900000&tc.endDelta=1800000&tc.timeSystem=utc&view=grid`
);
});
const countUpCells = [
getCellByIndex(page, 1, TIME_TO_FROM_COLUMN),
getCellByIndex(page, 2, TIME_TO_FROM_COLUMN)
];
const countdownCells = [
getCellByIndex(page, 3, TIME_TO_FROM_COLUMN),
getCellByIndex(page, 4, TIME_TO_FROM_COLUMN)
];
// Verify that the countdown cells are counting down
for (let i = 0; i < countdownCells.length; i++) {
await test.step(`Countdown cell ${i + 1} counts down`, async () => {
const countdownCell = countdownCells[i];
// Get the initial countdown timestamp object
const beforeCountdown = await getAndAssertCountdownOrUpObject(page, i + 3);
// should not have a '-' sign
await expect(countdownCell).not.toHaveText('-');
// Wait until it changes
await expect(countdownCell).not.toHaveText(beforeCountdown.toString());
// Get the new countdown timestamp object
const afterCountdown = await getAndAssertCountdownOrUpObject(page, i + 3);
// Verify that the new countdown timestamp object is less than the old one
expect(Number(afterCountdown.seconds)).toBeLessThan(Number(beforeCountdown.seconds));
});
}
// Verify that the count-up cells are counting up
for (let i = 0; i < countUpCells.length; i++) {
await test.step(`Count-up cell ${i + 1} counts up`, async () => {
const countUpCell = countUpCells[i];
// Get the initial count-up timestamp object
const beforeCountUp = await getAndAssertCountdownOrUpObject(page, i + 1);
// should not have a '+' sign
await expect(countUpCell).not.toHaveText('+');
// Wait until it changes
await expect(countUpCell).not.toHaveText(beforeCountUp.toString());
// Get the new count-up timestamp object
const afterCountUp = await getAndAssertCountdownOrUpObject(page, i + 1);
// Verify that the new count-up timestamp object is greater than the old one
expect(Number(afterCountUp.seconds)).toBeGreaterThan(Number(beforeCountUp.seconds));
});
}
await test.step("Verify absence of progress indication for an activity that's not in progress", async () => {
// When an activity is not in progress, the progress pie is not visible
const hidden = await page.getByRole('row').locator('path').nth(1).isHidden();
await expect(hidden).toBe(true);
});
});
/**
* Get the cell at the given row and column indices.
* @param {import('@playwright/test').Page} page
* @param {number} rowIndex
* @param {number} columnIndex
* @returns {import('@playwright/test').Locator} cell
*/
function getCellByIndex(page, rowIndex, columnIndex) {
return page.getByRole('cell').nth(rowIndex * NUM_COLUMNS + columnIndex);
}
/**
* Return the innerText of the cell at the given row and column indices.
* @param {import('@playwright/test').Page} page
* @param {number} rowIndex
* @param {number} columnIndex
* @returns {Promise<string>} text
*/
async function getCellTextByIndex(page, rowIndex, columnIndex) {
const text = await getCellByIndex(page, rowIndex, columnIndex).innerText();
return text;
}
/**
* Get the text from the countdown (or countup) cell in the given row, assert that it matches the countdown/countup
* regex, and return an object representing the countdown.
* @param {import('@playwright/test').Page} page
* @param {number} rowIndex the row index
* @returns {Promise<CountdownOrUpObject>} The countdown (or countup) object
*/
async function getAndAssertCountdownOrUpObject(page, rowIndex) {
const timeToFrom = await getCellTextByIndex(page, HEADER_ROW + rowIndex, TIME_TO_FROM_COLUMN);
expect(timeToFrom).toMatch(COUNTDOWN_REGEXP);
const match = timeToFrom.match(COUNTDOWN_REGEXP);
return {
sign: match[COUNTDOWN.SIGN],
days: match[COUNTDOWN.DAYS],
hours: match[COUNTDOWN.HOURS],
minutes: match[COUNTDOWN.MINUTES],
seconds: match[COUNTDOWN.SECONDS],
toString: () => timeToFrom
};
}

View File

@@ -0,0 +1,290 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, 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.
*****************************************************************************/
/*
Collection of Time List tests set to run with browser clock manipulate made possible with the
clockOptions plugin fixture.
*/
import fs from 'fs';
import { createDomainObjectWithDefaults, createPlanFromJSON } from '../../../appActions.js';
import {
createTimelistWithPlanAndSetActivityInProgress,
getEarliestStartTime,
getFirstActivity
} from '../../../helper/planningUtils';
import { expect, test } from '../../../pluginFixtures.js';
const examplePlanSmall3 = JSON.parse(
fs.readFileSync(
new URL('../../../test-data/examplePlans/ExamplePlan_Small3.json', import.meta.url)
)
);
const examplePlanSmall1 = JSON.parse(
fs.readFileSync(
new URL('../../../test-data/examplePlans/ExamplePlan_Small1.json', import.meta.url)
)
);
const TIME_TO_FROM_COLUMN = 2;
const HEADER_ROW = 0;
const NUM_COLUMNS = 5;
const FULL_CIRCLE_PATH =
'M3.061616997868383e-15,-50A50,50,0,1,1,-3.061616997868383e-15,50A50,50,0,1,1,3.061616997868383e-15,-50Z';
/**
* The regular expression used to parse the countdown string.
* Some examples of valid Countdown strings:
* ```
* '35D 02:03:04'
* '-1D 01:02:03'
* '01:02:03'
* '-05:06:07'
* ```
*/
const COUNTDOWN_REGEXP = /(-)?(\d+D\s)?(\d{2}):(\d{2}):(\d{2})/;
/**
* @typedef {Object} CountdownOrUpObject
* @property {string} sign - The sign of the countdown ('-' if the countdown is negative, '+' otherwise).
* @property {string} days - The number of days in the countdown (undefined if there are no days).
* @property {string} hours - The number of hours in the countdown.
* @property {string} minutes - The number of minutes in the countdown.
* @property {string} seconds - The number of seconds in the countdown.
* @property {string} toString - The countdown string.
*/
/**
* Object representing the indices of the capture groups in a countdown regex match.
*
* @typedef {{ SIGN: number, DAYS: number, HOURS: number, MINUTES: number, SECONDS: number, REGEXP: RegExp }}
* @property {number} SIGN - The index for the sign capture group (1 if a '-' sign is present, otherwise undefined).
* @property {number} DAYS - The index for the days capture group (2 for the number of days, otherwise undefined).
* @property {number} HOURS - The index for the hours capture group (3 for the hour part of the time).
* @property {number} MINUTES - The index for the minutes capture group (4 for the minute part of the time).
* @property {number} SECONDS - The index for the seconds capture group (5 for the second part of the time).
*/
const COUNTDOWN = Object.freeze({
SIGN: 1,
DAYS: 2,
HOURS: 3,
MINUTES: 4,
SECONDS: 5
});
test.describe('Time List with controlled clock @clock', () => {
test.use({
clockOptions: {
now: getEarliestStartTime(examplePlanSmall3),
shouldAdvanceTime: true
}
});
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
// Create Time List
const timelist = await createDomainObjectWithDefaults(page, {
type: 'Time List'
});
// Create a Plan with events that count down and up.
// Add it as a child to the Time List.
await createPlanFromJSON(page, {
json: examplePlanSmall3,
parent: timelist.uuid
});
// Navigate to the Time List in real-time mode
await page.goto(
`${timelist.url}?tc.mode=local&tc.startDelta=900000&tc.endDelta=1800000&tc.timeSystem=utc&view=grid`
);
//Expand the viewport to show the entire time list
await page.getByLabel('Collapse Inspect Pane').click();
await page.getByLabel('Collapse Browse Pane').click();
});
test('Time List shows current events and counts down correctly in real-time mode', async ({
page
}) => {
const countUpCells = [
getTimeListCellByIndex(page, 1, TIME_TO_FROM_COLUMN),
getTimeListCellByIndex(page, 2, TIME_TO_FROM_COLUMN)
];
const countdownCells = [
getTimeListCellByIndex(page, 3, TIME_TO_FROM_COLUMN),
getTimeListCellByIndex(page, 4, TIME_TO_FROM_COLUMN)
];
// Verify that the countdown cells are counting down
for (let i = 0; i < countdownCells.length; i++) {
await test.step(`Countdown cell ${i + 1} counts down`, async () => {
const countdownCell = countdownCells[i];
// Get the initial countdown timestamp object
const beforeCountdown = await getAndAssertCountdownOrUpObject(page, i + 3);
// should not have a '-' sign
await expect(countdownCell).not.toHaveText('-');
// Wait until it changes
await expect(countdownCell).not.toHaveText(beforeCountdown.toString());
// Get the new countdown timestamp object
const afterCountdown = await getAndAssertCountdownOrUpObject(page, i + 3);
// Verify that the new countdown timestamp object is less than the old one
expect(Number(afterCountdown.seconds)).toBeLessThan(Number(beforeCountdown.seconds));
});
}
// Verify that the count-up cells are counting up
for (let i = 0; i < countUpCells.length; i++) {
await test.step(`Count-up cell ${i + 1} counts up`, async () => {
const countUpCell = countUpCells[i];
// Get the initial count-up timestamp object
const beforeCountUp = await getAndAssertCountdownOrUpObject(page, i + 1);
// should not have a '+' sign
await expect(countUpCell).not.toHaveText('+');
// Wait until it changes
await expect(countUpCell).not.toHaveText(beforeCountUp.toString());
// Get the new count-up timestamp object
const afterCountUp = await getAndAssertCountdownOrUpObject(page, i + 1);
// Verify that the new count-up timestamp object is greater than the old one
expect(Number(afterCountUp.seconds)).toBeGreaterThan(Number(beforeCountUp.seconds));
});
}
});
});
test.describe('Activity progress when activity is in the future @clock', () => {
const firstActivity = getFirstActivity(examplePlanSmall1);
test.use({
clockOptions: {
now: firstActivity.start - 1,
shouldAdvanceTime: true
}
});
test.beforeEach(async ({ page }) => {
await createTimelistWithPlanAndSetActivityInProgress(page, examplePlanSmall1);
});
test('progress pie is empty', async ({ page }) => {
const anActivity = page.getByRole('row').nth(0);
// Progress pie shows no progress when now is less than the start time
await expect(anActivity.getByLabel('Activity in progress').locator('path')).not.toHaveAttribute(
'd'
);
});
});
test.describe('Activity progress when now is between start and end of the activity @clock', () => {
const firstActivity = getFirstActivity(examplePlanSmall1);
test.beforeEach(async ({ page }) => {
await createTimelistWithPlanAndSetActivityInProgress(page, examplePlanSmall1);
});
test.use({
clockOptions: {
now: firstActivity.start + 50000,
shouldAdvanceTime: true
}
});
test('progress pie is partially filled', async ({ page }) => {
const anActivity = page.getByRole('row').nth(0);
const pathElement = anActivity.getByLabel('Activity in progress').locator('path');
// Progress pie shows progress when now is greater than the start time
await expect(pathElement).toHaveAttribute('d');
});
});
test.describe('Activity progress when now is after end of the activity @clock', () => {
const firstActivity = getFirstActivity(examplePlanSmall1);
test.use({
clockOptions: {
now: firstActivity.end + 10000,
shouldAdvanceTime: true
}
});
test.beforeEach(async ({ page }) => {
await createTimelistWithPlanAndSetActivityInProgress(page, examplePlanSmall1);
});
test('progress pie is full', async ({ page }) => {
const anActivity = page.getByRole('row').nth(0);
// Progress pie is completely full and doesn't update if now is greater than the end time
await expect(anActivity.getByLabel('Activity in progress').locator('path')).toHaveAttribute(
'd',
FULL_CIRCLE_PATH
);
});
});
/**
* Get the cell at the given row and column indices.
* @param {import('@playwright/test').Page} page
* @param {number} rowIndex
* @param {number} columnIndex
* @returns {import('@playwright/test').Locator} cell
*/
function getTimeListCellByIndex(page, rowIndex, columnIndex) {
return page.getByRole('cell').nth(rowIndex * NUM_COLUMNS + columnIndex);
}
/**
* Return the innerText of the cell at the given row and column indices.
* @param {import('@playwright/test').Page} page
* @param {number} rowIndex
* @param {number} columnIndex
* @returns {Promise<string>} text
*/
async function getTimeListCellTextByIndex(page, rowIndex, columnIndex) {
const text = await getTimeListCellByIndex(page, rowIndex, columnIndex).innerText();
return text;
}
/**
* Get the text from the countdown (or countup) cell in the given row, assert that it matches the countdown/countup
* regex, and return an object representing the countdown.
* @param {import('@playwright/test').Page} page
* @param {number} rowIndex the row index
* @returns {Promise<CountdownOrUpObject>} The countdown (or countup) object
*/
async function getAndAssertCountdownOrUpObject(page, rowIndex) {
const timeToFrom = await getTimeListCellTextByIndex(
page,
HEADER_ROW + rowIndex,
TIME_TO_FROM_COLUMN
);
expect(timeToFrom).toMatch(COUNTDOWN_REGEXP);
const match = timeToFrom.match(COUNTDOWN_REGEXP);
return {
sign: match[COUNTDOWN.SIGN],
days: match[COUNTDOWN.DAYS],
hours: match[COUNTDOWN.HOURS],
minutes: match[COUNTDOWN.MINUTES],
seconds: match[COUNTDOWN.SECONDS],
toString: () => timeToFrom
};
}

View File

@@ -131,7 +131,10 @@ test.describe('Time Strip', () => {
const startBoundString = new Date(startBound).toISOString().replace('T', ' ');
const endBoundString = new Date(endBound).toISOString().replace('T', ' ');
await setIndependentTimeConductorBounds(page, startBoundString, endBoundString);
await setIndependentTimeConductorBounds(page, {
start: startBoundString,
end: endBoundString
});
expect(await activityBounds.count()).toEqual(1);
});
@@ -160,7 +163,10 @@ test.describe('Time Strip', () => {
const startBoundString = new Date(startBound).toISOString().replace('T', ' ');
const endBoundString = new Date(endBound).toISOString().replace('T', ' ');
await setIndependentTimeConductorBounds(page, startBoundString, endBoundString);
await setIndependentTimeConductorBounds(page, {
start: startBoundString,
end: endBoundString
});
// Verify that two events are displayed
expect(await activityBounds.count()).toEqual(2);

View File

@@ -286,7 +286,7 @@ test.describe('Basic Condition Set Use', () => {
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 page.getByLabel('Open the View Switcher Menu').click();
await expect(page.getByRole('menuitem', { name: /Lad Table/ })).toBeHidden();
await expect(page.getByRole('menuitem', { name: /Conditions View/ })).toBeVisible();

View File

@@ -23,6 +23,7 @@ import { fileURLToPath } from 'url';
import {
createDomainObjectWithDefaults,
navigateToObjectWithFixedTimeBounds,
setFixedTimeMode,
setIndependentTimeConductorBounds,
setRealTimeMode,
@@ -30,12 +31,120 @@ import {
} from '../../../../appActions.js';
import { expect, test } from '../../../../pluginFixtures.js';
const LOCALSTORAGE_PATH = fileURLToPath(
const CHILD_LAYOUT_STORAGE_STATE_PATH = fileURLToPath(
new URL('../../../../test-data/display_layout_with_child_layouts.json', import.meta.url)
);
const CHILD_PLOT_STORAGE_STATE_PATH = fileURLToPath(
new URL('../../../../test-data/display_layout_with_child_overlay_plot.json', import.meta.url)
);
const TINY_IMAGE_BASE64 =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII';
test.describe('Display Layout Sub-object Actions @localStorage', () => {
const INIT_ITC_START_BOUNDS = '2024-11-12 19:11:11.000Z';
const INIT_ITC_END_BOUNDS = '2024-11-12 20:11:11.000Z';
const NEW_GLOBAL_START_BOUNDS = '2024-11-11 19:11:11.000Z';
const NEW_GLOBAL_END_BOUNDS = '2024-11-11 20:11:11.000Z';
test.use({
storageState: CHILD_PLOT_STORAGE_STATE_PATH
});
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
await page.getByLabel('Expand My Items folder').click();
const waitForMyItemsNavigation = page.waitForURL(`**/mine/?*`);
await page
.getByLabel('Main Tree')
.getByLabel('Navigate to Parent Display Layout layout Object')
.click();
// Wait for the URL to change to the display layout
await waitForMyItemsNavigation;
});
test('Open in New Tab action preserves time bounds @2p', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7524'
});
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/6982'
});
const TEST_FIXED_START_TIME = 1731352271000; // 2024-11-11 19:11:11.000Z
const TEST_FIXED_END_TIME = TEST_FIXED_START_TIME + 3600000; // 2024-11-11 20:11:11.000Z
// Verify the ITC has the expected initial bounds
expect(
await page
.getByLabel('Child Overlay Plot 1 Frame Controls')
.getByLabel('Start bounds')
.textContent()
).toEqual(INIT_ITC_START_BOUNDS);
expect(
await page
.getByLabel('Child Overlay Plot 1 Frame Controls')
.getByLabel('End bounds')
.textContent()
).toEqual(INIT_ITC_END_BOUNDS);
// Update the global fixed bounds to 2024-11-11 19:11:11.000Z / 2024-11-11 20:11:11.000Z
const url = page.url().split('?')[0];
await navigateToObjectWithFixedTimeBounds(
page,
url,
TEST_FIXED_START_TIME,
TEST_FIXED_END_TIME
);
// ITC bounds should still match the initial ITC bounds
expect(
await page
.getByLabel('Child Overlay Plot 1 Frame Controls')
.getByLabel('Start bounds')
.textContent()
).toEqual(INIT_ITC_START_BOUNDS);
expect(
await page
.getByLabel('Child Overlay Plot 1 Frame Controls')
.getByLabel('End bounds')
.textContent()
).toEqual(INIT_ITC_END_BOUNDS);
// Open the Child Overlay Plot 1 in a new tab
await page.getByLabel('View menu items').click();
const pagePromise = page.context().waitForEvent('page');
await page.getByLabel('Open In New Tab').click();
const newPage = await pagePromise;
await newPage.waitForLoadState('domcontentloaded');
// Verify that the global time conductor bounds in the new page match the updated global bounds
expect(
await newPage.getByLabel('Global Time Conductor').getByLabel('Start bounds').textContent()
).toEqual(NEW_GLOBAL_START_BOUNDS);
expect(
await newPage.getByLabel('Global Time Conductor').getByLabel('End bounds').textContent()
).toEqual(NEW_GLOBAL_END_BOUNDS);
// Verify that the ITC is enabled in the new page
await expect(newPage.getByLabel('Disable Independent Time Conductor')).toBeVisible();
// Verify that the ITC bounds in the new page match the original ITC bounds
expect(
await newPage
.getByLabel('Independent Time Conductor Panel')
.getByLabel('Start bounds')
.textContent()
).toEqual(INIT_ITC_START_BOUNDS);
expect(
await newPage
.getByLabel('Independent Time Conductor Panel')
.getByLabel('End bounds')
.textContent()
).toEqual(INIT_ITC_END_BOUNDS);
});
});
test.describe('Display Layout Toolbar Actions @localStorage', () => {
const PARENT_DISPLAY_LAYOUT_NAME = 'Parent Display Layout';
const CHILD_DISPLAY_LAYOUT_NAME1 = 'Child Layout 1';
@@ -50,7 +159,7 @@ test.describe('Display Layout Toolbar Actions @localStorage', () => {
await page.getByLabel('Edit Object').click();
});
test.use({
storageState: LOCALSTORAGE_PATH
storageState: CHILD_LAYOUT_STORAGE_STATE_PATH
});
test('can add/remove Text element to a single layout', async ({ page }) => {
@@ -163,7 +272,7 @@ test.describe('Display Layout', () => {
expect(trimmedDisplayValue).toBe(formattedTelemetryValue);
// ensure we can right click on the alpha-numeric widget and view historical data
await page.getByLabel('Sine', { exact: true }).click({
await page.getByLabel(/Alpha-numeric telemetry value of.*/).click({
button: 'right'
});
await page.getByLabel('View Historical Data').click();
@@ -336,7 +445,7 @@ test.describe('Display Layout', () => {
const startDate = '2021-12-30 01:01:00.000Z';
const endDate = '2021-12-30 01:11:00.000Z';
await setIndependentTimeConductorBounds(page, startDate, endDate);
await setIndependentTimeConductorBounds(page, { start: startDate, end: endDate });
// check image date
await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible();

View File

@@ -20,25 +20,46 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import * as utils from '../../../../helper/faultUtils.js';
import {
acknowledgeFault,
acknowledgeMultipleFaults,
changeViewTo,
clearSearch,
enterSearchTerm,
getFault,
getFaultByName,
getFaultName,
getFaultNamespace,
getFaultResultCount,
getFaultSeverity,
getFaultTriggerTime,
getHighestSeverity,
getLowestSeverity,
navigateToFaultManagementWithExample,
navigateToFaultManagementWithoutExample,
selectFaultItem,
shelveFault,
shelveMultipleFaults,
sortFaultsBy
} from '../../../../helper/faultUtils.js';
import { expect, test } from '../../../../pluginFixtures.js';
test.describe('The Fault Management Plugin using example faults', () => {
test.beforeEach(async ({ page }) => {
await utils.navigateToFaultManagementWithExample(page);
await navigateToFaultManagementWithExample(page);
});
test('Shows a criticality icon for every fault @unstable', async ({ page }) => {
test('Shows a criticality icon for every fault', async ({ page }) => {
const faultCount = await page.locator('c-fault-mgmt__list').count();
const criticalityIconCount = await page.locator('c-fault-mgmt__list-severity').count();
expect.soft(faultCount).toEqual(criticalityIconCount);
expect(faultCount).toEqual(criticalityIconCount);
});
test('When selecting a fault, it has an "is-selected" class and it\'s information shows in the inspector @unstable', async ({
test('When selecting a fault, it has an "is-selected" class and it\'s information shows in the inspector', async ({
page
}) => {
await utils.selectFaultItem(page, 1);
await selectFaultItem(page, 1);
await page.getByRole('tab', { name: 'Config' }).click();
const selectedFaultName = await page
@@ -48,22 +69,22 @@ test.describe('The Fault Management Plugin using example faults', () => {
.locator(`.c-inspector__properties >> :text("${selectedFaultName}")`)
.count();
await expect
.soft(page.locator('.c-faults-list-view-item-body > .c-fault-mgmt__list').first())
.toHaveClass(/is-selected/);
expect.soft(inspectorFaultNameCount).toEqual(1);
await expect(
page.locator('.c-faults-list-view-item-body > .c-fault-mgmt__list').first()
).toHaveClass(/is-selected/);
expect(inspectorFaultNameCount).toEqual(1);
});
test('When selecting multiple faults, no specific fault information is shown in the inspector @unstable', async ({
test('When selecting multiple faults, no specific fault information is shown in the inspector', async ({
page
}) => {
await utils.selectFaultItem(page, 1);
await utils.selectFaultItem(page, 2);
await selectFaultItem(page, 1);
await selectFaultItem(page, 2);
const selectedRows = page.locator(
'.c-fault-mgmt__list.is-selected .c-fault-mgmt__list-faultname'
);
expect.soft(await selectedRows.count()).toEqual(2);
expect(await selectedRows.count()).toEqual(2);
await page.getByRole('tab', { name: 'Config' }).click();
const firstSelectedFaultName = await selectedRows.nth(0).textContent();
@@ -75,180 +96,180 @@ test.describe('The Fault Management Plugin using example faults', () => {
.locator(`.c-inspector__properties >> :text("${secondSelectedFaultName}")`)
.count();
expect.soft(firstNameInInspectorCount).toEqual(0);
expect.soft(secondNameInInspectorCount).toEqual(0);
expect(firstNameInInspectorCount).toEqual(0);
expect(secondNameInInspectorCount).toEqual(0);
});
test('Allows you to shelve a fault @unstable', async ({ page }) => {
const shelvedFaultName = await utils.getFaultName(page, 2);
const beforeShelvedFault = utils.getFaultByName(page, shelvedFaultName);
test('Allows you to shelve a fault', async ({ page }) => {
const shelvedFaultName = await getFaultName(page, 2);
const beforeShelvedFault = getFaultByName(page, shelvedFaultName);
expect.soft(await beforeShelvedFault.count()).toBe(1);
await expect(beforeShelvedFault).toHaveCount(1);
await utils.shelveFault(page, 2);
await shelveFault(page, 2);
// check it is removed from standard view
const afterShelvedFault = utils.getFaultByName(page, shelvedFaultName);
expect.soft(await afterShelvedFault.count()).toBe(0);
const afterShelvedFault = getFaultByName(page, shelvedFaultName);
expect(await afterShelvedFault.count()).toBe(0);
await utils.changeViewTo(page, 'shelved');
await changeViewTo(page, 'shelved');
const shelvedViewFault = utils.getFaultByName(page, shelvedFaultName);
const shelvedViewFault = getFaultByName(page, shelvedFaultName);
expect.soft(await shelvedViewFault.count()).toBe(1);
expect(await shelvedViewFault.count()).toBe(1);
});
test('Allows you to acknowledge a fault @unstable', async ({ page }) => {
const acknowledgedFaultName = await utils.getFaultName(page, 3);
test('Allows you to acknowledge a fault', async ({ page }) => {
const acknowledgedFaultName = await getFaultName(page, 3);
await utils.acknowledgeFault(page, 3);
await acknowledgeFault(page, 3);
const fault = utils.getFault(page, 3);
await expect.soft(fault).toHaveClass(/is-acknowledged/);
const fault = getFault(page, 3);
await expect(fault).toHaveClass(/is-acknowledged/);
await utils.changeViewTo(page, 'acknowledged');
await changeViewTo(page, 'acknowledged');
const acknowledgedViewFaultName = await utils.getFaultName(page, 1);
expect.soft(acknowledgedFaultName).toEqual(acknowledgedViewFaultName);
const acknowledgedViewFaultName = await getFaultName(page, 1);
expect(acknowledgedFaultName).toEqual(acknowledgedViewFaultName);
});
test('Allows you to shelve multiple faults @unstable', async ({ page }) => {
const shelvedFaultNameOne = await utils.getFaultName(page, 1);
const shelvedFaultNameFour = await utils.getFaultName(page, 4);
test('Allows you to shelve multiple faults', async ({ page }) => {
const shelvedFaultNameOne = await getFaultName(page, 1);
const shelvedFaultNameFour = await getFaultName(page, 4);
const beforeShelvedFaultOne = utils.getFaultByName(page, shelvedFaultNameOne);
const beforeShelvedFaultFour = utils.getFaultByName(page, shelvedFaultNameFour);
const beforeShelvedFaultOne = getFaultByName(page, shelvedFaultNameOne);
const beforeShelvedFaultFour = getFaultByName(page, shelvedFaultNameFour);
expect.soft(await beforeShelvedFaultOne.count()).toBe(1);
expect.soft(await beforeShelvedFaultFour.count()).toBe(1);
await expect(beforeShelvedFaultOne).toHaveCount(1);
await expect(beforeShelvedFaultFour).toHaveCount(1);
await utils.shelveMultipleFaults(page, 1, 4);
await shelveMultipleFaults(page, 1, 4);
// check it is removed from standard view
const afterShelvedFaultOne = utils.getFaultByName(page, shelvedFaultNameOne);
const afterShelvedFaultFour = utils.getFaultByName(page, shelvedFaultNameFour);
expect.soft(await afterShelvedFaultOne.count()).toBe(0);
expect.soft(await afterShelvedFaultFour.count()).toBe(0);
const afterShelvedFaultOne = getFaultByName(page, shelvedFaultNameOne);
const afterShelvedFaultFour = getFaultByName(page, shelvedFaultNameFour);
await expect(afterShelvedFaultOne).toHaveCount(0);
await expect(afterShelvedFaultFour).toHaveCount(0);
await utils.changeViewTo(page, 'shelved');
await changeViewTo(page, 'shelved');
const shelvedViewFaultOne = utils.getFaultByName(page, shelvedFaultNameOne);
const shelvedViewFaultFour = utils.getFaultByName(page, shelvedFaultNameFour);
const shelvedViewFaultOne = getFaultByName(page, shelvedFaultNameOne);
const shelvedViewFaultFour = getFaultByName(page, shelvedFaultNameFour);
expect.soft(await shelvedViewFaultOne.count()).toBe(1);
expect.soft(await shelvedViewFaultFour.count()).toBe(1);
await expect(shelvedViewFaultOne).toHaveCount(1);
await expect(shelvedViewFaultFour).toHaveCount(1);
});
test('Allows you to acknowledge multiple faults @unstable', async ({ page }) => {
const acknowledgedFaultNameTwo = await utils.getFaultName(page, 2);
const acknowledgedFaultNameFive = await utils.getFaultName(page, 5);
test('Allows you to acknowledge multiple faults', async ({ page }) => {
const acknowledgedFaultNameTwo = await getFaultName(page, 2);
const acknowledgedFaultNameFive = await getFaultName(page, 5);
await utils.acknowledgeMultipleFaults(page, 2, 5);
await acknowledgeMultipleFaults(page, 2, 5);
const faultTwo = utils.getFault(page, 2);
const faultFive = utils.getFault(page, 5);
const faultTwo = getFault(page, 2);
const faultFive = getFault(page, 5);
// check they have been acknowledged
await expect.soft(faultTwo).toHaveClass(/is-acknowledged/);
await expect.soft(faultFive).toHaveClass(/is-acknowledged/);
await expect(faultTwo).toHaveClass(/is-acknowledged/);
await expect(faultFive).toHaveClass(/is-acknowledged/);
await utils.changeViewTo(page, 'acknowledged');
await changeViewTo(page, 'acknowledged');
const acknowledgedViewFaultTwo = utils.getFaultByName(page, acknowledgedFaultNameTwo);
const acknowledgedViewFaultFive = utils.getFaultByName(page, acknowledgedFaultNameFive);
const acknowledgedViewFaultTwo = getFaultByName(page, acknowledgedFaultNameTwo);
const acknowledgedViewFaultFive = getFaultByName(page, acknowledgedFaultNameFive);
expect.soft(await acknowledgedViewFaultTwo.count()).toBe(1);
expect.soft(await acknowledgedViewFaultFive.count()).toBe(1);
await expect(acknowledgedViewFaultTwo).toHaveCount(1);
await expect(acknowledgedViewFaultFive).toHaveCount(1);
});
test('Allows you to search faults @unstable', async ({ page }) => {
const faultThreeNamespace = await utils.getFaultNamespace(page, 3);
const faultTwoName = await utils.getFaultName(page, 2);
const faultFiveTriggerTime = await utils.getFaultTriggerTime(page, 5);
test('Allows you to search faults', async ({ page }) => {
const faultThreeNamespace = await getFaultNamespace(page, 3);
const faultTwoName = await getFaultName(page, 2);
const faultFiveTriggerTime = await getFaultTriggerTime(page, 5);
// should be all faults (5)
let faultResultCount = await utils.getFaultResultCount(page);
expect.soft(faultResultCount).toEqual(5);
let faultResultCount = await getFaultResultCount(page);
expect(faultResultCount).toEqual(5);
// search namespace
await utils.enterSearchTerm(page, faultThreeNamespace);
await enterSearchTerm(page, faultThreeNamespace);
faultResultCount = await utils.getFaultResultCount(page);
expect.soft(faultResultCount).toEqual(1);
expect.soft(await utils.getFaultNamespace(page, 1)).toEqual(faultThreeNamespace);
faultResultCount = await getFaultResultCount(page);
expect(faultResultCount).toEqual(1);
expect(await getFaultNamespace(page, 1)).toEqual(faultThreeNamespace);
// all faults
await utils.clearSearch(page);
faultResultCount = await utils.getFaultResultCount(page);
expect.soft(faultResultCount).toEqual(5);
await clearSearch(page);
faultResultCount = await getFaultResultCount(page);
expect(faultResultCount).toEqual(5);
// search name
await utils.enterSearchTerm(page, faultTwoName);
await enterSearchTerm(page, faultTwoName);
faultResultCount = await utils.getFaultResultCount(page);
expect.soft(faultResultCount).toEqual(1);
expect.soft(await utils.getFaultName(page, 1)).toEqual(faultTwoName);
faultResultCount = await getFaultResultCount(page);
expect(faultResultCount).toEqual(1);
expect(await getFaultName(page, 1)).toEqual(faultTwoName);
// all faults
await utils.clearSearch(page);
faultResultCount = await utils.getFaultResultCount(page);
expect.soft(faultResultCount).toEqual(5);
await clearSearch(page);
faultResultCount = await getFaultResultCount(page);
expect(faultResultCount).toEqual(5);
// search triggerTime
await utils.enterSearchTerm(page, faultFiveTriggerTime);
await enterSearchTerm(page, faultFiveTriggerTime);
faultResultCount = await utils.getFaultResultCount(page);
expect.soft(faultResultCount).toEqual(1);
expect.soft(await utils.getFaultTriggerTime(page, 1)).toEqual(faultFiveTriggerTime);
faultResultCount = await getFaultResultCount(page);
expect(faultResultCount).toEqual(1);
expect(await getFaultTriggerTime(page, 1)).toEqual(faultFiveTriggerTime);
});
test('Allows you to sort faults @unstable', async ({ page }) => {
const highestSeverity = await utils.getHighestSeverity(page);
const lowestSeverity = await utils.getLowestSeverity(page);
test('Allows you to sort faults', async ({ page }) => {
const highestSeverity = await getHighestSeverity(page);
const lowestSeverity = await getLowestSeverity(page);
const faultOneName = 'Example Fault 1';
const faultFiveName = 'Example Fault 5';
let firstFaultName = await utils.getFaultName(page, 1);
let firstFaultName = await getFaultName(page, 1);
expect.soft(firstFaultName).toEqual(faultOneName);
expect(firstFaultName).toEqual(faultOneName);
await utils.sortFaultsBy(page, 'oldest-first');
await sortFaultsBy(page, 'oldest-first');
firstFaultName = await utils.getFaultName(page, 1);
expect.soft(firstFaultName).toEqual(faultFiveName);
firstFaultName = await getFaultName(page, 1);
expect(firstFaultName).toEqual(faultFiveName);
await utils.sortFaultsBy(page, 'severity');
await sortFaultsBy(page, 'severity');
const sortedHighestSeverity = await utils.getFaultSeverity(page, 1);
const sortedLowestSeverity = await utils.getFaultSeverity(page, 5);
expect.soft(sortedHighestSeverity).toEqual(highestSeverity);
expect.soft(sortedLowestSeverity).toEqual(lowestSeverity);
const sortedHighestSeverity = await getFaultSeverity(page, 1);
const sortedLowestSeverity = await getFaultSeverity(page, 5);
expect(sortedHighestSeverity).toEqual(highestSeverity);
expect(sortedLowestSeverity).toEqual(lowestSeverity);
});
});
test.describe('The Fault Management Plugin without using example faults', () => {
test.beforeEach(async ({ page }) => {
await utils.navigateToFaultManagementWithoutExample(page);
await navigateToFaultManagementWithoutExample(page);
});
test('Shows no faults when no faults are provided @unstable', async ({ page }) => {
test('Shows no faults when no faults are provided', async ({ page }) => {
const faultCount = await page.locator('c-fault-mgmt__list').count();
expect.soft(faultCount).toEqual(0);
expect(faultCount).toEqual(0);
await utils.changeViewTo(page, 'acknowledged');
await changeViewTo(page, 'acknowledged');
const acknowledgedCount = await page.locator('c-fault-mgmt__list').count();
expect.soft(acknowledgedCount).toEqual(0);
expect(acknowledgedCount).toEqual(0);
await utils.changeViewTo(page, 'shelved');
await changeViewTo(page, 'shelved');
const shelvedCount = await page.locator('c-fault-mgmt__list').count();
expect.soft(shelvedCount).toEqual(0);
expect(shelvedCount).toEqual(0);
});
test('Will return no faults when searching @unstable', async ({ page }) => {
await utils.enterSearchTerm(page, 'fault');
test('Will return no faults when searching', async ({ page }) => {
await enterSearchTerm(page, 'fault');
const faultCount = await page.locator('c-fault-mgmt__list').count();
expect.soft(faultCount).toEqual(0);
expect(faultCount).toEqual(0);
});
});

View File

@@ -248,11 +248,10 @@ test.describe('Flexible Layout', () => {
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// flip on independent time conductor
await setIndependentTimeConductorBounds(
page,
'2021-12-30 01:01:00.000Z',
'2021-12-30 01:11:00.000Z'
);
await setIndependentTimeConductorBounds(page, {
start: '2021-12-30 01:01:00.000Z',
end: '2021-12-30 01:11:00.000Z'
});
// check image date
await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible();

View File

@@ -175,13 +175,13 @@ test.describe('Gauge', () => {
});
// Try to create a Folder into the Gauge. Should be disallowed.
await page.getByRole('button', { name: /Create/ }).click();
await page.getByRole('button', { name: 'Create' }).click();
await page.getByRole('menuitem', { name: /Folder/ }).click();
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
await page.getByLabel('Cancel').click();
// Try to create a Display Layout into the Gauge. Should be disallowed.
await page.getByRole('button', { name: /Create/ }).click();
await page.getByRole('button', { name: 'Create' }).click();
await page.getByRole('menuitem', { name: /Display Layout/ }).click();
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
});

View File

@@ -37,6 +37,8 @@ test.describe('Testing numeric data with inspector data visualization (i.e., dat
});
test('Can click on telemetry and see data in inspector @2p', async ({ page, context }) => {
const initStartBounds = await page.getByLabel('Start bounds').textContent();
const initEndBounds = await page.getByLabel('End bounds').textContent();
const exampleDataVisualizationSource = await createDomainObjectWithDefaults(page, {
type: 'Example Data Visualization Source'
});
@@ -78,5 +80,9 @@ test.describe('Testing numeric data with inspector data visualization (i.e., dat
await newPage.waitForLoadState();
// expect new tab title to contain 'Second Sine Wave Generator'
await expect(newPage).toHaveTitle('Second Sine Wave Generator');
// Verify that "Open in New Tab" preserves the time bounds
expect(initStartBounds).toEqual(await newPage.getByLabel('Start bounds').textContent());
expect(initEndBounds).toEqual(await newPage.getByLabel('End bounds').textContent());
});
});

View File

@@ -83,7 +83,7 @@ test.describe('Snapshot Container tests', () => {
// name: "Dropped Overlay Plot"
// });
await page.getByLabel('Take a Notebook Snapshot').click();
await page.getByLabel('Open the Notebook Snapshot Menu').click();
await page.getByRole('menuitem', { name: 'Save to Notebook Snapshots' }).click();
await page.getByLabel('Show Snapshots').click();
});

View File

@@ -189,6 +189,57 @@ test.describe('Overlay Plot', () => {
await assertLimitLinesExistAndAreVisible(page);
});
test('Limit lines adjust when series is resized', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/6987'
});
// Create an Overlay Plot with a default SWG
overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot'
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: overlayPlot.uuid
});
await page.goto(overlayPlot.url);
// Assert that no limit lines are shown by default
await page.waitForSelector('.js-limit-area', { state: 'attached' });
expect(await page.locator('.c-plot-limit-line').count()).toBe(0);
// Enter edit mode
await page.getByLabel('Edit Object').click();
// Expand the "Sine Wave Generator" plot series options and enable limit lines
await page.getByRole('tab', { name: 'Config' }).click();
await page
.getByRole('list', { name: 'Plot Series Properties' })
.locator('span')
.first()
.click();
await page
.getByRole('list', { name: 'Plot Series Properties' })
.getByRole('checkbox', { name: 'Limit lines' })
.check();
await assertLimitLinesExistAndAreVisible(page);
// Save (exit edit mode)
await page.locator('button[title="Save"]').click();
await page.locator('li[title="Save and Finish Editing"]').click();
const initialCoords = await assertLimitLinesExistAndAreVisible(page);
// Resize the chart container by showing the snapshot pane.
await page.getByLabel('Show Snapshots').click();
const newCoords = await assertLimitLinesExistAndAreVisible(page);
// We just need to know that the first limit line redrew somewhere lower than the initial y position.
expect(newCoords.y).toBeGreaterThan(initialCoords.y);
});
test('The elements pool supports dragging series into multiple y-axis buckets', async ({
page
}) => {
@@ -305,6 +356,10 @@ test.describe('Overlay Plot', () => {
type: 'Sine Wave Generator',
parent: overlayPlot.uuid
});
const swgB = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: overlayPlot.uuid
});
await page.goto(overlayPlot.url);
// Wait for plot series data to load and be drawn
@@ -319,6 +374,23 @@ test.describe('Overlay Plot', () => {
await page.getByRole('menuitem', { name: 'Remove' }).click();
await page.getByRole('button', { name: 'OK', exact: true }).click();
await expect(swgAElementsPoolItem).toBeHidden();
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7530'
});
await test.step('Verify that the legend is correct after removing a series', async () => {
await page.getByLabel('Plot Canvas').hover();
await page.mouse.move(50, 0, {
steps: 10
});
await expect(page.getByLabel('Plot Legend Item')).toHaveCount(1);
await expect(page.getByLabel(`Plot Legend Item for ${swgA.name}`)).toBeHidden();
await expect(page.getByLabel(`Plot Legend Item for ${swgB.name}`)).toBeVisible();
});
});
});
@@ -337,4 +409,7 @@ async function assertLimitLinesExistAndAreVisible(page) {
for (let i = 0; i < limitLineCount; i++) {
await expect(page.locator('.c-plot-limit-line').nth(i)).toBeVisible();
}
const firstLimitLineCoords = await page.locator('.c-plot-limit-line').first().boundingBox();
return firstLimitLineCoords;
}

View File

@@ -54,7 +54,9 @@ test.describe('Plots work in Previews', () => {
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// right click on the plot and select view large
await page.getByLabel('Sine', { exact: true }).click({ button: 'right' });
await page.getByLabel(/Alpha-numeric telemetry value of.*/).click({
button: 'right'
});
await page.getByLabel('View Historical Data').click();
await expect(page.getByLabel('Preview Container').getByLabel('Plot Canvas')).toBeVisible();
await page.getByRole('button', { name: 'Close' }).click();

View File

@@ -114,7 +114,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(defaultFrameBorderColor),
NO_STYLE_RGBA,
hexToRGB(setTextColor),
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
page
.getByRole('group', { name: 'StackedPlot1 Frame' })
.getByLabel('Stacked Plot Style Target')
);
// Check styles on StackedPlot2. Note: https://github.com/nasa/openmct/issues/7337
@@ -122,7 +124,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(defaultFrameBorderColor),
NO_STYLE_RGBA,
hexToRGB(setTextColor),
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target')
page
.getByRole('group', { name: 'StackedPlot2 Frame' })
.getByLabel('Stacked Plot Style Target')
);
});
@@ -143,7 +147,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(defaultBorderTargetColor),
NO_STYLE_RGBA,
hexToRGB(defaultTextColor),
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
page
.getByRole('group', { name: 'StackedPlot1 Frame' })
.getByLabel('Stacked Plot Style Target')
);
// Check styles on StackedPlot2
@@ -151,7 +157,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(defaultBorderTargetColor),
NO_STYLE_RGBA,
hexToRGB(defaultTextColor),
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target')
page
.getByRole('group', { name: 'StackedPlot2 Frame' })
.getByLabel('Stacked Plot Style Target')
);
// Set styles using setStyles function on StackedPlot1 but not StackedPlot2
@@ -160,7 +168,7 @@ test.describe('Flexible Layout styling', () => {
setBorderColor,
setBackgroundColor,
setTextColor,
page.getByLabel('StackedPlot1 Frame')
page.getByRole('group', { name: 'StackedPlot1 Frame' })
);
// Check styles on StackedPlot1
@@ -168,7 +176,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(setBorderColor),
hexToRGB(setBackgroundColor),
hexToRGB(setTextColor),
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
page
.getByRole('group', { name: 'StackedPlot1 Frame' })
.getByLabel('Stacked Plot Style Target')
);
// Check styles on StackedPlot2
@@ -176,7 +186,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(defaultBorderTargetColor),
NO_STYLE_RGBA,
hexToRGB(defaultTextColor),
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target')
page
.getByRole('group', { name: 'StackedPlot2 Frame' })
.getByLabel('Stacked Plot Style Target')
);
// Save Flexible Layout
@@ -191,7 +203,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(setBorderColor),
hexToRGB(setBackgroundColor),
hexToRGB(setTextColor),
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
page
.getByRole('group', { name: 'StackedPlot1 Frame' })
.getByLabel('Stacked Plot Style Target')
);
// Check styles on StackedPlot2
@@ -199,7 +213,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(defaultBorderTargetColor),
NO_STYLE_RGBA,
hexToRGB(defaultTextColor),
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target')
page
.getByRole('group', { name: 'StackedPlot2 Frame' })
.getByLabel('Stacked Plot Style Target')
);
});
@@ -241,7 +257,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(setBorderColor),
hexToRGB(setBackgroundColor),
hexToRGB(setTextColor),
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
page
.getByRole('group', { name: 'StackedPlot1 Frame' })
.getByLabel('Stacked Plot Style Target')
);
// Check styles on StackedPlot2 to verify they are the default
@@ -249,7 +267,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(defaultBorderTargetColor),
NO_STYLE_RGBA,
hexToRGB(defaultTextColor),
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target')
page
.getByRole('group', { name: 'StackedPlot2 Frame' })
.getByLabel('Stacked Plot Style Target')
);
// Set styles using setStyles function on StackedPlot2
@@ -258,7 +278,7 @@ test.describe('Flexible Layout styling', () => {
setBorderColor,
setBackgroundColor,
setTextColor,
page.getByLabel('StackedPlot2 Frame')
page.getByRole('group', { name: 'StackedPlot2 Frame' })
);
// Check styles on StackedPlot2
@@ -266,7 +286,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(setBorderColor),
hexToRGB(setBackgroundColor),
hexToRGB(setTextColor),
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target')
page
.getByRole('group', { name: 'StackedPlot2 Frame' })
.getByLabel('Stacked Plot Style Target')
);
// Save Flexible Layout
@@ -281,7 +303,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(setBorderColor),
hexToRGB(setBackgroundColor),
hexToRGB(setTextColor),
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
page
.getByRole('group', { name: 'StackedPlot1 Frame' })
.getByLabel('Stacked Plot Style Target')
);
// Check styles on StackedPlot2
@@ -289,7 +313,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(setBorderColor),
hexToRGB(setBackgroundColor),
hexToRGB(setTextColor),
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target')
page
.getByRole('group', { name: 'StackedPlot2 Frame' })
.getByLabel('Stacked Plot Style Target')
);
// Directly navigate to the flexible layout
@@ -326,7 +352,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(setBorderColor),
hexToRGB(setBackgroundColor),
hexToRGB(setTextColor),
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
page
.getByRole('group', { name: 'StackedPlot1 Frame' })
.getByLabel('Stacked Plot Style Target')
);
// Check styles on StackedPlot2 matches previous set colors
@@ -334,7 +362,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(setBorderColor),
hexToRGB(setBackgroundColor),
hexToRGB(setTextColor),
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target')
page
.getByRole('group', { name: 'StackedPlot2 Frame' })
.getByLabel('Stacked Plot Style Target')
);
});
@@ -356,7 +386,7 @@ test.describe('Flexible Layout styling', () => {
setBorderColor,
setBackgroundColor,
setTextColor,
page.getByLabel('StackedPlot1 Frame')
page.getByRole('group', { name: 'StackedPlot1 Frame' })
);
// Check styles using checkStyles function
@@ -364,7 +394,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(setBorderColor),
hexToRGB(setBackgroundColor),
hexToRGB(setTextColor),
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
page
.getByRole('group', { name: 'StackedPlot1 Frame' })
.getByLabel('Stacked Plot Style Target')
);
// Save Flexible Layout
@@ -386,7 +418,7 @@ test.describe('Flexible Layout styling', () => {
'No Style',
'No Style',
'No Style',
page.getByLabel('StackedPlot1 Frame')
page.getByRole('group', { name: 'StackedPlot1 Frame' })
);
// Check styles using checkStyles function
@@ -394,7 +426,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(defaultBorderTargetColor),
NO_STYLE_RGBA,
hexToRGB(inheritedColor),
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
page
.getByRole('group', { name: 'StackedPlot1 Frame' })
.getByLabel('Stacked Plot Style Target')
);
// Save Flexible Layout
await page.getByRole('button', { name: 'Save' }).click();
@@ -408,7 +442,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(defaultBorderTargetColor),
NO_STYLE_RGBA,
hexToRGB(inheritedColor),
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
page
.getByRole('group', { name: 'StackedPlot1 Frame' })
.getByLabel('Stacked Plot Style Target')
);
});

View File

@@ -67,7 +67,7 @@ test.describe('Style Inspector Options', () => {
await expect(page.getByRole('tab', { name: 'Styles' })).toBeVisible();
// Select Stacked Layout Column
await page.getByLabel('Stacked Plot Frame').click();
await page.getByRole('group', { name: 'Stacked Plot Frame' }).click();
// The overall Flex Layout or Stacked Plot itself MUST be style-able.
await expect(page.getByRole('tab', { name: 'Styles' })).toBeVisible();

View File

@@ -66,7 +66,7 @@ test.describe('Timer', () => {
});
});
test.describe('Timer with target date', () => {
test.describe('Timer with target date @clock', () => {
let timer;
test.beforeEach(async ({ page }) => {

View File

@@ -109,7 +109,7 @@ test.describe('Verify tooltips', () => {
async function getToolTip(object) {
await page.locator('.c-create-button').hover();
await page.getByRole('cell', { name: object.name }).hover();
await page.getByLabel('lad name').getByText(object.name).hover();
let tooltipText = await page.locator('.c-tooltip').textContent();
return tooltipText.replace('\n', '').trim();
}

View File

@@ -35,25 +35,61 @@ Make no assumptions about the order that elements appear in the DOM.
import { expect, test } from '../../pluginFixtures.js';
test('Verify that My Items Tree appears @mobile', async ({ page, openmctConfig }) => {
const { myItemsFolderName } = openmctConfig;
//Go to baseURL
await page.goto('./');
test.describe('Smoke tests for @mobile', () => {
test.beforeEach(async ({ page }) => {
//For now, this test is going to be hardcoded against './test-data/display_layout_with_child_layouts.json'
await page.goto('./');
});
//My Items to be visible
await expect(page.getByRole('treeitem', { name: `${myItemsFolderName}` })).toBeVisible();
});
test('Verify that user can search @mobile', async ({ page }) => {
//For now, this test is going to be hardcoded against './test-data/display_layout_with_child_layouts.json'
await page.goto('./');
await page.getByRole('searchbox', { name: 'Search Input' }).click();
await page.getByRole('searchbox', { name: 'Search Input' }).fill('Parent Display Layout');
//Search Results appear in search modal
await expect(page.getByLabel('Object Results').getByText('Parent Display Layout')).toBeVisible();
//Clicking on the search result takes you to the object
await page.getByLabel('Object Results').getByText('Parent Display Layout').click();
await page.getByTitle('Collapse Browse Pane').click();
await expect(page.getByRole('main').getByText('Parent Display Layout')).toBeVisible();
test('Verify that My Items Tree appears @mobile', async ({ page }) => {
//My Items to be visible
await expect(page.getByRole('treeitem', { name: 'My Items' })).toBeVisible();
});
test('Verify that user can search @mobile', async ({ page }) => {
await page.getByRole('searchbox', { name: 'Search Input' }).click();
await page.getByRole('searchbox', { name: 'Search Input' }).fill('Parent Display Layout');
//Search Results appear in search modal
await expect(
page.getByLabel('Object Results').getByText('Parent Display Layout')
).toBeVisible();
//Clicking on the search result takes you to the object
await page.getByLabel('Object Results').getByText('Parent Display Layout').click();
await page.getByTitle('Collapse Browse Pane').click();
await expect(page.getByRole('main').getByText('Parent Display Layout')).toBeVisible();
});
test('Verify that user can change time conductor @mobile', async ({ page }) => {
//Collapse Browse Pane to get more Time Conductor space
await page.getByLabel('Collapse Browse Pane').click();
//Open Time Conductor and change to Real Time Mode and set offset hour by 1 hour
// Disabling line because we're intentionally obscuring the text
// eslint-disable-next-line playwright/no-force-option
await page.getByLabel('Time Conductor Mode').click({ force: true });
await page.getByLabel('Time Conductor Mode Menu').click();
await page.getByLabel('Real-Time').click();
await page.getByLabel('Start offset hours').fill('01');
await page.getByLabel('Submit time offsets').click();
await expect(page.getByLabel('Start offset: 01:30:00')).toBeVisible();
});
test('Remove Object and confirmation dialog @mobile', async ({ page }) => {
await page.getByRole('searchbox', { name: 'Search Input' }).click();
await page.getByRole('searchbox', { name: 'Search Input' }).fill('Parent Display Layout');
//Search Results appear in search modal
//Clicking on the search result takes you to the object
await page.getByLabel('Object Results').getByText('Parent Display Layout').click();
await page.getByTitle('Collapse Browse Pane').click();
await expect(page.getByRole('main').getByText('Parent Display Layout')).toBeVisible();
//Verify both objects are in view
await expect(await page.getByLabel('Child Layout 1 Layout')).toBeVisible();
await expect(await page.getByLabel('Child Layout 2 Layout')).toBeVisible();
//Remove First Object to bring up confirmation dialog
await page.getByLabel('View menu items').nth(1).click();
await page.getByLabel('Remove').click();
await page.getByRole('button', { name: 'OK' }).click();
//Verify that the object is removed
await expect(await page.getByLabel('Child Layout 1 Layout')).toBeVisible();
expect(await page.getByLabel('Child Layout 2 Layout').count()).toBe(0);
});
});

View File

@@ -178,7 +178,7 @@ test.describe('Performance tests', () => {
console.log('jpgResourceTiming ' + JSON.stringify(jpgResourceTiming));
// Click Close Icon
await page.locator('[aria-label="Close"]').click();
await page.getByRole('button', { name: 'Close' }).click();
await page.evaluate(() => window.performance.mark('view-large-close-button'));
//await client.send('HeapProfiler.enable');

View File

@@ -25,11 +25,12 @@ Tests the branding associated with the default deployment. At least the about mo
*/
import percySnapshot from '@percy/playwright';
import { fileURLToPath } from 'url';
import { expect, test } from '../../../avpFixtures.js';
import { VISUAL_URL } from '../../../constants.js';
//Declare the scope of the visual test
//Declare the component scope of the visual test for Percy
const header = '.l-shell__head';
test.describe('Visual - Header @a11y', () => {
@@ -68,14 +69,34 @@ test.describe('Visual - Header @a11y', () => {
});
test('show snapshot button', async ({ page, theme }) => {
await page.getByLabel('Take a Notebook Snapshot').click();
await page.getByLabel('Open the Notebook Snapshot Menu').click();
await page.getByRole('menuitem', { name: 'Save to Notebook Snapshots' }).click();
await percySnapshot(page, `Notebook Snapshot Show button (theme: '${theme}')`, {
scope: header
});
await expect(await page.getByLabel('Show Snapshots')).toBeVisible();
await expect(page.getByLabel('Show Snapshots')).toBeVisible();
});
});
//Header test with all mission status options. Right now, this is just Mission Status, but should grow over time
test.describe('Mission Header @a11y', () => {
test.beforeEach(async ({ page }) => {
await page.addInitScript({
path: fileURLToPath(new URL('../../../helper/addInitExampleUser.js', import.meta.url))
});
await page.goto('./', { waitUntil: 'domcontentloaded' });
await expect(page.getByText('Select Role')).toBeVisible();
// set role
await page.getByRole('button', { name: 'Select', exact: true }).click();
// dismiss role confirmation popup
await page.getByRole('button', { name: 'Dismiss' }).click();
});
test('Mission status panel', async ({ page, theme }) => {
await percySnapshot(page, `Header default with Mission Header (theme: '${theme}')`, {
scope: header
});
});
});
// Skipping for https://github.com/nasa/openmct/issues/7421

View File

@@ -28,7 +28,7 @@ import { MISSION_TIME, VISUAL_URL } from '../../../constants.js';
//Declare the scope of the visual test
const inspectorPane = '.l-shell__pane-inspector';
test.describe('Visual - Inspector @ally', () => {
test.describe('Visual - Inspector @ally @clock', () => {
test.beforeEach(async ({ page }) => {
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
});

View File

@@ -30,7 +30,7 @@ import percySnapshot from '@percy/playwright';
import { MISSION_TIME, VISUAL_URL } from '../../constants.js';
import { expect, test } from '../../pluginFixtures.js';
test.describe('Visual - Controlled Clock', () => {
test.describe('Visual - Controlled Clock @clock', () => {
test.beforeEach(async ({ page }) => {
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
});

View File

@@ -20,18 +20,26 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import percySnapshot from '@percy/playwright';
import { fileURLToPath } from 'url';
import * as utils from '../../helper/faultUtils.js';
import {
acknowledgeFault,
changeViewTo,
navigateToFaultManagementWithoutExample,
navigateToFaultManagementWithStaticExample,
openFaultRowMenu,
selectFaultItem,
shelveFault
} from '../../helper/faultUtils.js';
import { expect, test } from '../../pluginFixtures.js';
test.describe('Fault Management Visual Tests', () => {
test('icon test', async ({ page, theme }) => {
await page.addInitScript({
path: fileURLToPath(new URL('../../helper/addInitFaultManagementPlugin.js', import.meta.url))
});
await page.goto('./', { waitUntil: 'domcontentloaded' });
test.describe('Fault Management Visual Tests - without example', () => {
test.beforeEach(async ({ page }) => {
await navigateToFaultManagementWithoutExample(page);
await page.getByLabel('Collapse Inspect Pane').click();
await page.getByLabel('Click to collapse items').click();
});
test('fault management icon appears in tree', async ({ page, theme }) => {
// Wait for status bar to load
await expect(
page.getByRole('status', {
@@ -51,14 +59,20 @@ test.describe('Fault Management Visual Tests', () => {
await percySnapshot(page, `Fault Management icon appears in tree (theme: '${theme}')`);
});
});
test.describe('Fault Management Visual Tests', () => {
test.beforeEach(async ({ page }) => {
await navigateToFaultManagementWithStaticExample(page);
await page.getByLabel('Collapse Inspect Pane').click();
await page.getByLabel('Click to collapse items').click();
});
test('fault list and acknowledged faults', async ({ page, theme }) => {
await utils.navigateToFaultManagementWithStaticExample(page);
await percySnapshot(page, `Shows a list of faults in the standard view (theme: '${theme}')`);
await utils.acknowledgeFault(page, 1);
await utils.changeViewTo(page, 'acknowledged');
await acknowledgeFault(page, 1);
await changeViewTo(page, 'acknowledged');
await percySnapshot(
page,
@@ -67,14 +81,12 @@ test.describe('Fault Management Visual Tests', () => {
});
test('shelved faults', async ({ page, theme }) => {
await utils.navigateToFaultManagementWithStaticExample(page);
await utils.shelveFault(page, 1);
await utils.changeViewTo(page, 'shelved');
await shelveFault(page, 1);
await changeViewTo(page, 'shelved');
await percySnapshot(page, `Shelved faults appear in the shelved view (theme: '${theme}')`);
await utils.openFaultRowMenu(page, 1);
await openFaultRowMenu(page, 1);
await percySnapshot(
page,
@@ -83,9 +95,7 @@ test.describe('Fault Management Visual Tests', () => {
});
test('3-dot menu for fault', async ({ page, theme }) => {
await utils.navigateToFaultManagementWithStaticExample(page);
await utils.openFaultRowMenu(page, 1);
await openFaultRowMenu(page, 1);
await percySnapshot(
page,
@@ -94,9 +104,7 @@ test.describe('Fault Management Visual Tests', () => {
});
test('ability to acknowledge or shelve', async ({ page, theme }) => {
await utils.navigateToFaultManagementWithStaticExample(page);
await utils.selectFaultItem(page, 1);
await selectFaultItem(page, 1);
await percySnapshot(
page,

View File

@@ -32,12 +32,12 @@ test.describe('Mission Status Visual Tests @a11y', () => {
});
await page.goto('./', { waitUntil: 'domcontentloaded' });
await expect(page.getByText('Select Role')).toBeVisible();
// Description should be empty https://github.com/nasa/openmct/issues/6978
await expect(page.locator('c-message__action-text')).toBeHidden();
// set role
await page.getByRole('button', { name: 'Select', exact: true }).click();
// dismiss role confirmation popup
await page.getByRole('button', { name: 'Dismiss' }).click();
await page.getByLabel('Collapse Inspect Pane').click();
await page.getByLabel('Collapse Browse Pane').click();
});
test('Mission status panel', async ({ page, theme }) => {
await page.getByLabel('Toggle Mission Status Panel').click();

View File

@@ -26,13 +26,41 @@ import fs from 'fs';
import { createDomainObjectWithDefaults, createPlanFromJSON } from '../../appActions.js';
import { test } from '../../avpFixtures.js';
import { VISUAL_URL } from '../../constants.js';
import { setBoundsToSpanAllActivities, setDraftStatusForPlan } from '../../helper/planningUtils.js';
import {
createTimelistWithPlanAndSetActivityInProgress,
getFirstActivity,
setBoundsToSpanAllActivities,
setDraftStatusForPlan
} from '../../helper/planningUtils.js';
const examplePlanSmall = JSON.parse(
const examplePlanSmall1 = JSON.parse(
fs.readFileSync(new URL('../../test-data/examplePlans/ExamplePlan_Small1.json', import.meta.url))
);
const examplePlanSmall2 = JSON.parse(
fs.readFileSync(new URL('../../test-data/examplePlans/ExamplePlan_Small2.json', import.meta.url))
);
const snapshotScope = '.l-shell__pane-main .l-pane__contents';
test.describe('Visual - Timelist progress bar @clock', () => {
const firstActivity = getFirstActivity(examplePlanSmall1);
test.use({
clockOptions: {
now: firstActivity.end + 10000,
shouldAdvanceTime: true
}
});
test.beforeEach(async ({ page }) => {
await createTimelistWithPlanAndSetActivityInProgress(page, examplePlanSmall1);
await page.getByLabel('Click to collapse items').click();
});
test('progress pie is full', async ({ page, theme }) => {
// Progress pie is completely full and doesn't update if now is greater than the end time
await percySnapshot(page, `Time List with Activity in Progress (theme: ${theme})`);
});
});
test.describe('Visual - Planning', () => {
test.beforeEach(async ({ page }) => {
@@ -42,42 +70,41 @@ test.describe('Visual - Planning', () => {
test('Plan View', async ({ page, theme }) => {
const plan = await createPlanFromJSON(page, {
name: 'Plan Visual Test',
json: examplePlanSmall
json: examplePlanSmall2
});
await setBoundsToSpanAllActivities(page, examplePlanSmall, plan.url);
await percySnapshot(page, `Plan View (theme: ${theme})`, {
scope: snapshotScope
});
await setBoundsToSpanAllActivities(page, examplePlanSmall2, plan.url);
await percySnapshot(page, `Plan View (theme: ${theme})`);
});
test('Plan View w/ draft status', async ({ page, theme }) => {
const plan = await createPlanFromJSON(page, {
name: 'Plan Visual Test (Draft)',
json: examplePlanSmall
json: examplePlanSmall2
});
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
await setDraftStatusForPlan(page, plan);
await setBoundsToSpanAllActivities(page, examplePlanSmall, plan.url);
await percySnapshot(page, `Plan View w/ draft status (theme: ${theme})`, {
scope: snapshotScope
});
await setBoundsToSpanAllActivities(page, examplePlanSmall2, plan.url);
await percySnapshot(page, `Plan View w/ draft status (theme: ${theme})`);
});
});
test.describe('Visual - Gantt Chart', () => {
test.beforeEach(async ({ page }) => {
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
});
test('Gantt Chart View', async ({ page, theme }) => {
const ganttChart = await createDomainObjectWithDefaults(page, {
type: 'Gantt Chart',
name: 'Gantt Chart Visual Test'
});
await createPlanFromJSON(page, {
json: examplePlanSmall,
json: examplePlanSmall2,
parent: ganttChart.uuid
});
await setBoundsToSpanAllActivities(page, examplePlanSmall, ganttChart.url);
await percySnapshot(page, `Gantt Chart View (theme: ${theme}) - Clipped Activity Names`, {
scope: snapshotScope
});
await setBoundsToSpanAllActivities(page, examplePlanSmall2, ganttChart.url);
await percySnapshot(page, `Gantt Chart View (theme: ${theme}) - Clipped Activity Names`);
// Expand the inspect pane and uncheck the 'Clip Activity Names' option
await page.getByRole('button', { name: 'Expand Inspect Pane' }).click();
@@ -93,9 +120,7 @@ test.describe('Visual - Planning', () => {
// Dismiss the notification
await page.getByLabel('Dismiss').click();
await percySnapshot(page, `Gantt Chart View (theme: ${theme}) - Unclipped Activity Names`, {
scope: snapshotScope
});
await percySnapshot(page, `Gantt Chart View (theme: ${theme}) - Unclipped Activity Names`);
});
test('Gantt Chart View w/ draft status', async ({ page, theme }) => {
@@ -104,7 +129,7 @@ test.describe('Visual - Planning', () => {
name: 'Gantt Chart Visual Test (Draft)'
});
const plan = await createPlanFromJSON(page, {
json: examplePlanSmall,
json: examplePlanSmall2,
parent: ganttChart.uuid
});
@@ -112,10 +137,8 @@ test.describe('Visual - Planning', () => {
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
await setBoundsToSpanAllActivities(page, examplePlanSmall, ganttChart.url);
await percySnapshot(page, `Gantt Chart View w/ draft status (theme: ${theme})`, {
scope: snapshotScope
});
await setBoundsToSpanAllActivities(page, examplePlanSmall2, ganttChart.url);
await percySnapshot(page, `Gantt Chart View w/ draft status (theme: ${theme})`);
// Expand the inspect pane and uncheck the 'Clip Activity Names' option
await page.getByRole('button', { name: 'Expand Inspect Pane' }).click();
@@ -133,14 +156,12 @@ test.describe('Visual - Planning', () => {
await percySnapshot(
page,
`Gantt Chart View w/ draft status (theme: ${theme}) - Unclipped Activity Names`,
{
scope: snapshotScope
}
`Gantt Chart View w/ draft status (theme: ${theme}) - Unclipped Activity Names`
);
});
// Skipping for https://github.com/nasa/openmct/issues/7421
// test.afterEach(async ({ page }, testInfo) => {
// await scanForA11yViolations(page, testInfo.title);
// });
});
// Skipping for https://github.com/nasa/openmct/issues/7421
// test.afterEach(async ({ page }, testInfo) => {
// await scanForA11yViolations(page, testInfo.title);
// });

View File

@@ -91,7 +91,7 @@ test.describe('Flexible Layout styling @a11y', () => {
setBorderColor,
setBackgroundColor,
setTextColor,
page.getByLabel('StackedPlot1 Frame')
page.getByRole('group', { name: 'StackedPlot1 Frame' })
);
await percySnapshot(

View File

@@ -53,11 +53,11 @@ test.describe('Visual - Telemetry Views', () => {
await page.goto(telemetry.url, { waitUntil: 'domcontentloaded' });
//Click this button to see telemetry display options
await page.getByRole('button', { name: 'Plot' }).click();
await page.getByLabel('Open the View Switcher Menu').click();
await page.getByLabel('Telemetry Table').click();
//Get Table View in place
expect(await page.getByLabel('Expand Columns')).toBeInViewport();
await expect(page.getByLabel('Expand Columns')).toBeInViewport();
await percySnapshot(page, `Default Telemetry Table View (theme: ${theme})`);

View File

@@ -55,6 +55,7 @@
</template>
<script>
const ONE_HOUR = 60 * 60 * 1000;
export default {
inject: ['openmct', 'domainObject'],
data() {
@@ -77,6 +78,10 @@ export default {
selectItem(item, event) {
event.stopPropagation();
const bounds = this.openmct.time.getBounds();
const otherBounds = {
start: bounds.start - ONE_HOUR,
end: bounds.end + ONE_HOUR
};
const selection = [
{
element: this.$el,
@@ -88,6 +93,9 @@ export default {
icon: item.type.cssClass
},
dataRanges: [
{
bounds: otherBounds
},
{
bounds
}

View File

@@ -20,13 +20,13 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import utils from './utils.js';
import { acknowledgeFault, randomFaults, shelveFault } from './utils.js';
export default function (staticFaults = false) {
return function install(openmct) {
openmct.install(openmct.plugins.FaultManagement());
const faultsData = utils.randomFaults(staticFaults);
const faultsData = randomFaults(staticFaults);
openmct.faults.addProvider({
request(domainObject, options) {
@@ -44,14 +44,14 @@ export default function (staticFaults = false) {
return domainObject.type === 'faultManagement';
},
acknowledgeFault(fault, { comment = '' }) {
utils.acknowledgeFault(fault);
acknowledgeFault(fault);
return Promise.resolve({
success: true
});
},
shelveFault(fault, duration) {
utils.shelveFault(fault, duration);
shelveFault(fault, duration);
return Promise.resolve({
success: true

View File

@@ -43,7 +43,7 @@ const getRandom = {
}
};
function shelveFault(
export function shelveFault(
fault,
opts = {
shelved: true,
@@ -58,11 +58,11 @@ function shelveFault(
}, opts.shelveDuration);
}
function acknowledgeFault(fault) {
export function acknowledgeFault(fault) {
fault.acknowledged = true;
}
function randomFaults(staticFaults, count = 5) {
export function randomFaults(staticFaults, count = 5) {
let faults = [];
for (let x = 1, y = count + 1; x < y; x++) {
@@ -71,9 +71,3 @@ function randomFaults(staticFaults, count = 5) {
return faults;
}
export default {
randomFaults,
shelveFault,
acknowledgeFault
};

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.
-->
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />

View File

@@ -5,13 +5,14 @@
"type": "module",
"main": "dist/openmct.js",
"devDependencies": {
"@axe-core/playwright": "4.8.2",
"@axe-core/playwright": "4.8.5",
"@babel/eslint-parser": "7.23.3",
"@braintree/sanitize-url": "6.0.4",
"@percy/cli": "1.27.4",
"@percy/playwright": "1.0.4",
"@playwright/test": "1.39.0",
"@types/d3-axis": "3.0.6",
"@types/d3-shape": "3.0.0",
"@types/d3-scale": "4.0.8",
"@types/d3-selection": "3.0.10",
"@types/eventemitter3": "1.2.0",
@@ -22,10 +23,11 @@
"babel-plugin-istanbul": "6.1.1",
"codecov": "3.8.3",
"comma-separated-values": "3.6.4",
"copy-webpack-plugin": "11.0.0",
"copy-webpack-plugin": "12.0.2",
"cspell": "7.3.8",
"css-loader": "6.8.1",
"css-loader": "6.10.0",
"d3-axis": "3.0.0",
"d3-shape": "3.0.0",
"d3-scale": "4.0.2",
"d3-selection": "3.0.0",
"eslint": "8.56.0",
@@ -36,14 +38,14 @@
"eslint-plugin-prettier": "5.1.3",
"eslint-plugin-simple-import-sort": "10.0.0",
"eslint-plugin-unicorn": "49.0.0",
"eslint-plugin-vue": "9.18.1",
"eslint-plugin-vue": "9.22.0",
"eslint-plugin-you-dont-need-lodash-underscore": "6.13.0",
"eventemitter3": "1.2.0",
"file-saver": "2.0.5",
"flatbush": "4.2.0",
"git-rev-sync": "3.0.2",
"html2canvas": "1.4.1",
"imports-loader": "4.0.1",
"imports-loader": "5.0.0",
"jasmine-core": "5.1.1",
"karma": "6.4.2",
"karma-chrome-launcher": "3.2.0",
@@ -54,26 +56,26 @@
"karma-junit-reporter": "2.0.1",
"karma-sourcemap-loader": "0.4.0",
"karma-spec-reporter": "0.0.36",
"karma-webpack": "5.0.0",
"karma-webpack": "5.0.1",
"location-bar": "3.0.1",
"lodash": "4.17.21",
"marked": "11.2.0",
"marked": "12.0.0",
"mini-css-extract-plugin": "2.7.6",
"moment": "2.30.1",
"moment-duration-format": "2.3.2",
"moment-timezone": "0.5.41",
"npm-run-all2": "6.1.1",
"npm-run-all2": "6.1.2",
"nyc": "15.1.0",
"painterro": "1.2.87",
"plotly.js-basic-dist-min": "2.20.0",
"plotly.js-basic-dist-min": "2.29.1",
"plotly.js-gl2d-dist-min": "2.20.0",
"prettier": "3.2.5",
"prettier-eslint": "16.3.0",
"printj": "1.3.1",
"resolve-url-loader": "5.0.0",
"sanitize-html": "2.11.0",
"sass": "1.68.0",
"sass-loader": "14.0.0",
"sanitize-html": "2.12.1",
"sass": "1.71.1",
"sass-loader": "14.1.1",
"sinon": "17.0.0",
"style-loader": "3.3.3",
"terser-webpack-plugin": "5.3.9",
@@ -81,11 +83,11 @@
"typescript": "5.3.3",
"uuid": "9.0.1",
"vue": "3.4.19",
"vue-eslint-parser": "9.3.2",
"vue-eslint-parser": "9.4.2",
"vue-loader": "16.8.3",
"webpack": "5.89.0",
"webpack": "5.90.3",
"webpack-cli": "5.1.1",
"webpack-dev-server": "4.15.1",
"webpack-dev-server": "5.0.2",
"webpack-merge": "5.10.0"
},
"scripts": {

View File

@@ -29,10 +29,13 @@
:key="action.name"
role="menuitem"
:aria-disabled="action.isDisabled"
:class="action.cssClass"
:aria-label="action.name"
aria-describedby="item-description"
:class="action.cssClass"
:title="action.description"
@click="action.onItemClicked"
@mouseover="toggleItem(action)"
@mouseleave="toggleItem()"
>
{{ action.name }}
</li>
@@ -52,16 +55,23 @@
v-for="action in options.actions"
:key="action.name"
role="menuitem"
aria-describedby="item-description"
:aria-disabled="action.isDisabled"
:class="action.cssClass"
:aria-label="action.name"
:title="action.description"
@click="action.onItemClicked"
@mouseover="toggleItem(action)"
@mouseleave="toggleItem()"
>
{{ action.name }}
</li>
<li v-if="options.actions.length === 0">No actions defined.</li>
</ul>
<div v-if="hoveredItem" id="item-description" class="visually-hidden" aria-live="polite">
<span v-if="hoveredItem.name">{{ hoveredItem.name }}</span>
<span v-if="hoveredItem.description">: {{ hoveredItem.description }}</span>
</div>
</div>
</template>
@@ -70,11 +80,21 @@ import popupMenuMixin from '../mixins/popupMenuMixin.js';
export default {
mixins: [popupMenuMixin],
inject: ['options'],
data() {
return {
hoveredItem: null
};
},
computed: {
optionsLabel() {
const label = this.options.label ? `${this.options.label} Menu` : 'Menu';
const label = this.options.label ? `${this.options.label} Context Menu` : 'Context Menu';
return label;
}
},
methods: {
toggleItem(action) {
this.hoveredItem = action ?? null;
}
}
};
</script>

View File

@@ -38,8 +38,8 @@
:key="action.name"
role="menuitem"
:aria-disabled="action.isDisabled"
aria-describedby="item-description"
:class="action.cssClass"
:title="action.description"
@click="action.onItemClicked"
@mouseover="toggleItemDescription(action)"
@mouseleave="toggleItemDescription()"
@@ -64,7 +64,7 @@
role="menuitem"
:class="action.cssClass"
:aria-label="action.name"
:title="action.description"
aria-describedby="item-description"
@click="action.onItemClicked"
@mouseover="toggleItemDescription(action)"
@mouseleave="toggleItemDescription()"
@@ -74,13 +74,13 @@
<li v-if="options.actions.length === 0">No actions defined.</li>
</ul>
<div class="c-super-menu__item-description">
<div :class="['l-item-description__icon', 'bg-' + hoveredItem.cssClass]"></div>
<div aria-live="polite" class="c-super-menu__item-description">
<div :class="itemDescriptionIconClass"></div>
<div class="l-item-description__name">
{{ hoveredItem.name }}
{{ hoveredItemName }}
</div>
<div class="l-item-description__description">
{{ hoveredItem.description }}
<div id="item-description" class="l-item-description__description">
{{ hoveredItemDescription }}
</div>
</div>
</div>
@@ -90,26 +90,39 @@ import popupMenuMixin from '../mixins/popupMenuMixin.js';
export default {
mixins: [popupMenuMixin],
inject: ['options'],
data: function () {
data() {
return {
hoveredItem: {}
hoveredItem: null
};
},
computed: {
optionsLabel() {
const label = this.options.label ? `${this.options.label} Super Menu` : 'Super Menu';
return label;
},
itemDescriptionIconClass() {
const iconClass = ['l-item-description__icon'];
if (this.hoveredItem) {
iconClass.push('bg-' + this.hoveredItem.cssClass);
}
return iconClass;
},
hoveredItemName() {
return this.hoveredItem?.name ?? '';
},
hoveredItemDescription() {
return this.hoveredItem?.description ?? '';
}
},
methods: {
toggleItemDescription(action = {}) {
toggleItemDescription(action = null) {
const hoveredItem = {
name: action.name,
description: action.description,
cssClass: action.cssClass
name: action?.name,
description: action?.description,
cssClass: action?.cssClass
};
this.hoveredItem = Object.assign({}, this.hoveredItem, hoveredItem);
this.hoveredItem = hoveredItem;
}
}
};

View File

@@ -29,27 +29,29 @@
class="c-click-icon c-overlay__close-button icon-x"
@click.stop="destroy"
></button>
<div
ref="element"
class="c-overlay__contents js-notebook-snapshot-item-wrapper"
tabindex="0"
aria-modal="true"
aria-label="Overlay"
role="dialog"
></div>
<div v-if="buttons" class="c-overlay__button-bar">
<button
v-for="(button, index) in buttons"
ref="buttons"
:key="index"
class="c-button js-overlay__button"
<div class="c-overlay__content-wrapper">
<div
ref="element"
class="c-overlay__contents js-notebook-snapshot-item-wrapper"
tabindex="0"
:class="{ 'c-button--major': focusIndex === index }"
@focus="focusIndex = index"
@click="buttonClickHandler(button.callback)"
>
{{ button.label }}
</button>
aria-modal="true"
aria-label="Overlay"
role="dialog"
></div>
<div v-if="buttons" class="c-overlay__button-bar">
<button
v-for="(button, index) in buttons"
ref="buttons"
:key="index"
class="c-button js-overlay__button"
tabindex="0"
:class="{ 'c-button--major': focusIndex === index }"
@focus="focusIndex = index"
@click="buttonClickHandler(button.callback)"
>
{{ button.label }}
</button>
</div>
</div>
</div>
</div>

View File

@@ -15,7 +15,7 @@
&__icon {
// Holds a background SVG graphic
$s: 80px;
$s: 50px;
flex: 0 0 auto;
min-width: $s;
min-height: $s;

View File

@@ -19,7 +19,9 @@
z-index: 70;
&__blocker {
display: none; // Mobile-first
// Mobile-first: use the blocker to create a full look to dialogs
@include abs();
background: $colorBodyBg;
}
&__outer {
@@ -27,7 +29,13 @@
background: $colorBodyBg;
display: flex;
flex-direction: column;
padding: $overlayInnerMargin;
body.mobile .l-overlay-fit & {
// Vertically center small dialogs in mobile
top: 50%;
bottom: auto;
transform: translateY(-50%);
}
}
&__close-button {
@@ -39,12 +47,32 @@
z-index: 99;
}
&__content-wrapper {
display: flex;
height: 100%;
overflow: auto;
flex-direction: column;
gap: $interiorMargin;
body.desktop & {
overflow: hidden;
}
.l-overlay-fit &,
.l-overlay-dialog & {
margin: $overlayInnerMargin;
}
}
&__contents {
flex: 1 1 auto;
display: flex;
flex-direction: column;
outline: none;
overflow: auto;
body.mobile & {
flex: none;
}
}
&__top-bar {
@@ -78,6 +106,10 @@
display: flex;
justify-content: flex-end;
margin-top: $interiorMargin;
body.mobile & {
justify-content: flex-end;
padding-right: $interiorMargin;
}
> * + * {
margin-left: $interiorMargin;

View File

@@ -20,7 +20,6 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import installWorker from './WebSocketWorker.js';
const DEFAULT_RATE_MS = 1000;
/**
* Describes the strategy to be used when batching WebSocket messages
*
@@ -51,11 +50,21 @@ const DEFAULT_RATE_MS = 1000;
*
* @memberof module:openmct.telemetry
*/
// Shim for Internet Explorer, I mean Safari. It doesn't support requestIdleCallback, but it's in a tech preview, so it will be dropping soon.
const requestIdleCallback =
// eslint-disable-next-line compat/compat
window.requestIdleCallback ?? ((fn, { timeout }) => setTimeout(fn, timeout));
const ONE_SECOND = 1000;
const FIVE_SECONDS = 5 * ONE_SECOND;
class BatchingWebSocket extends EventTarget {
#worker;
#openmct;
#showingRateLimitNotification;
#rate;
#maxBatchSize;
#applicationIsInitializing;
#maxBatchWait;
#firstBatchReceived;
constructor(openmct) {
super();
@@ -66,7 +75,10 @@ class BatchingWebSocket extends EventTarget {
this.#worker = new Worker(workerUrl);
this.#openmct = openmct;
this.#showingRateLimitNotification = false;
this.#rate = DEFAULT_RATE_MS;
this.#maxBatchSize = Number.POSITIVE_INFINITY;
this.#maxBatchWait = ONE_SECOND;
this.#applicationIsInitializing = true;
this.#firstBatchReceived = false;
const routeMessageToHandler = this.#routeMessageToHandler.bind(this);
this.#worker.addEventListener('message', routeMessageToHandler);
@@ -78,6 +90,20 @@ class BatchingWebSocket extends EventTarget {
},
{ once: true }
);
openmct.once('start', () => {
// An idle callback is a pretty good indication that a complex display is done loading. At that point set the batch size more conservatively.
// Force it after 5 seconds if it hasn't happened yet.
requestIdleCallback(
() => {
this.#applicationIsInitializing = false;
this.setMaxBatchSize(this.#maxBatchSize);
},
{
timeout: FIVE_SECONDS
}
);
});
}
/**
@@ -129,14 +155,6 @@ class BatchingWebSocket extends EventTarget {
});
}
/**
* When using batching, sets the rate at which batches of messages are released.
* @param {Number} rate the amount of time to wait, in ms, between batches.
*/
setRate(rate) {
this.#rate = rate;
}
/**
* @param {Number} maxBatchSize the maximum length of a batch of messages. For example,
* the maximum number of telemetry values to batch before dropping them
@@ -151,12 +169,29 @@ class BatchingWebSocket extends EventTarget {
* 15 would probably be a better batch size.
*/
setMaxBatchSize(maxBatchSize) {
this.#maxBatchSize = maxBatchSize;
if (!this.#applicationIsInitializing) {
this.#sendMaxBatchSizeToWorker(this.#maxBatchSize);
}
}
setMaxBatchWait(wait) {
this.#maxBatchWait = wait;
this.#sendBatchWaitToWorker(this.#maxBatchWait);
}
#sendMaxBatchSizeToWorker(maxBatchSize) {
this.#worker.postMessage({
type: 'setMaxBatchSize',
maxBatchSize
});
}
#sendBatchWaitToWorker(maxBatchWait) {
this.#worker.postMessage({
type: 'setMaxBatchWait',
maxBatchWait
});
}
/**
* Disconnect the associated WebSocket. Generally speaking there is no need to call
* this manually.
@@ -169,7 +204,9 @@ class BatchingWebSocket extends EventTarget {
#routeMessageToHandler(message) {
if (message.data.type === 'batch') {
if (message.data.batch.dropped === true && !this.#showingRateLimitNotification) {
this.start = Date.now();
const batch = message.data.batch;
if (batch.dropped === true && !this.#showingRateLimitNotification) {
const notification = this.#openmct.notifications.alert(
'Telemetry dropped due to client rate limiting.',
{ hint: 'Refresh individual telemetry views to retrieve dropped telemetry if needed.' }
@@ -179,16 +216,45 @@ class BatchingWebSocket extends EventTarget {
this.#showingRateLimitNotification = false;
});
}
this.dispatchEvent(new CustomEvent('batch', { detail: message.data.batch }));
setTimeout(() => {
this.#readyForNextBatch();
}, this.#rate);
this.dispatchEvent(new CustomEvent('batch', { detail: batch }));
this.#waitUntilIdleAndRequestNextBatch(batch);
} else if (message.data.type === 'message') {
this.dispatchEvent(new CustomEvent('message', { detail: message.data.message }));
} else if (message.data.type === 'reconnected') {
this.dispatchEvent(new CustomEvent('reconnected'));
} else {
throw new Error(`Unknown message type: ${message.data.type}`);
}
}
#waitUntilIdleAndRequestNextBatch(batch) {
requestIdleCallback(
(state) => {
if (this.#firstBatchReceived === false) {
this.#firstBatchReceived = true;
}
const now = Date.now();
const waitedFor = now - this.start;
if (state.didTimeout === true) {
if (document.visibilityState === 'visible') {
console.warn(`Event loop is too busy to process batch.`);
this.#waitUntilIdleAndRequestNextBatch(batch);
} else {
// After ingesting a telemetry batch, wait until the event loop is idle again before
// informing the worker we are ready for another batch.
this.#readyForNextBatch();
}
} else {
if (waitedFor > ONE_SECOND) {
console.warn(`Warning, batch processing took ${waitedFor}ms`);
}
this.#readyForNextBatch();
}
},
{ timeout: ONE_SECOND }
);
}
}
export default BatchingWebSocket;

View File

@@ -85,6 +85,7 @@ const SUBSCRIBE_STRATEGY = {
export default class TelemetryAPI {
#isGreedyLAD;
#subscribeCache;
#hasReturnedFirstData;
get SUBSCRIBE_STRATEGY() {
return SUBSCRIBE_STRATEGY;
@@ -108,6 +109,7 @@ export default class TelemetryAPI {
this.#isGreedyLAD = true;
this.BatchingWebSocket = BatchingWebSocket;
this.#subscribeCache = {};
this.#hasReturnedFirstData = false;
}
abortAllRequests() {
@@ -383,7 +385,10 @@ export default class TelemetryAPI {
arguments[1] = await this.applyRequestInterceptors(domainObject, arguments[1]);
try {
const telemetry = await provider.request(...arguments);
if (!this.#hasReturnedFirstData) {
this.#hasReturnedFirstData = true;
performance.mark('firstHistoricalDataReturned');
}
return telemetry;
} catch (error) {
if (error.name !== 'AbortError') {

View File

@@ -21,6 +21,7 @@
*****************************************************************************/
/* eslint-disable max-classes-per-file */
export default function installWorker() {
const ONE_SECOND = 1000;
const FALLBACK_AND_WAIT_MS = [1000, 5000, 5000, 10000, 10000, 30000];
/**
@@ -44,6 +45,13 @@ export default function installWorker() {
#currentWaitIndex = 0;
#messageCallbacks = [];
#wsUrl;
#reconnecting = false;
#worker;
constructor(worker) {
super();
this.#worker = worker;
}
/**
* Establish a new WebSocket connection to the given URL
@@ -62,6 +70,9 @@ export default function installWorker() {
this.#isConnecting = true;
this.#webSocket = new WebSocket(url);
//Exposed to e2e tests so that the websocket can be manipulated during tests. Cannot find any other way to do this.
// Playwright does not support forcing websocket state changes.
this.#worker.currentWebSocket = this.#webSocket;
const boundConnected = this.#connected.bind(this);
this.#webSocket.addEventListener('open', boundConnected);
@@ -100,12 +111,17 @@ export default function installWorker() {
}
#connected() {
console.debug('Websocket connected.');
console.info('Websocket connected.');
this.#isConnected = true;
this.#isConnecting = false;
this.#currentWaitIndex = 0;
this.dispatchEvent(new Event('connected'));
if (this.#reconnecting) {
this.#worker.postMessage({
type: 'reconnected'
});
this.#reconnecting = false;
}
this.#flushQueue();
}
@@ -138,6 +154,7 @@ export default function installWorker() {
if (this.#reconnectTimeoutHandle) {
return;
}
this.#reconnecting = true;
this.#reconnectTimeoutHandle = setTimeout(() => {
this.connect(this.#wsUrl);
@@ -207,6 +224,9 @@ export default function installWorker() {
case 'setMaxBatchSize':
this.#messageBatcher.setMaxBatchSize(message.data.maxBatchSize);
break;
case 'setMaxBatchWait':
this.#messageBatcher.setMaxBatchWait(message.data.maxBatchWait);
break;
default:
throw new Error(`Unknown message type: ${type}`);
}
@@ -245,7 +265,6 @@ export default function installWorker() {
}
routeMessageToHandler(data) {
//Implement batching here
if (this.#messageBatcher.shouldBatchMessage(data)) {
this.#messageBatcher.addMessageToBatch(data);
} else {
@@ -267,12 +286,15 @@ export default function installWorker() {
#maxBatchSize;
#readyForNextBatch;
#worker;
#throttledSendNextBatch;
constructor(worker) {
this.#maxBatchSize = 10;
// No dropping telemetry unless we're explicitly told to.
this.#maxBatchSize = Number.POSITIVE_INFINITY;
this.#readyForNextBatch = false;
this.#worker = worker;
this.#resetBatch();
this.setMaxBatchWait(ONE_SECOND);
}
#resetBatch() {
this.#batch = {};
@@ -310,23 +332,29 @@ export default function installWorker() {
const batchId = this.#batchingStrategy.getBatchIdFromMessage(message);
let batch = this.#batch[batchId];
if (batch === undefined) {
this.#hasBatch = true;
batch = this.#batch[batchId] = [message];
} else {
batch.push(message);
}
if (batch.length > this.#maxBatchSize) {
console.warn(
`Exceeded max batch size of ${this.#maxBatchSize} for ${batchId}. Dropping value.`
);
batch.shift();
this.#batch.dropped = this.#batch.dropped || true;
this.#batch.dropped = true;
}
if (this.#readyForNextBatch) {
this.#sendNextBatch();
} else {
this.#hasBatch = true;
this.#throttledSendNextBatch();
}
}
setMaxBatchSize(maxBatchSize) {
this.#maxBatchSize = maxBatchSize;
}
setMaxBatchWait(maxBatchWait) {
this.#throttledSendNextBatch = throttle(this.#sendNextBatch.bind(this), maxBatchWait);
}
/**
* Indicates that client code is ready to receive the next batch of
* messages. If a batch is available, it will be immediately sent.
@@ -335,7 +363,7 @@ export default function installWorker() {
*/
readyForNextBatch() {
if (this.#hasBatch) {
this.#sendNextBatch();
this.#throttledSendNextBatch();
} else {
this.#readyForNextBatch = true;
}
@@ -352,7 +380,34 @@ export default function installWorker() {
}
}
const websocket = new ResilientWebSocket();
function throttle(callback, wait) {
let last = 0;
let throttling = false;
return function (...args) {
if (throttling) {
return;
}
const now = performance.now();
const timeSinceLast = now - last;
if (timeSinceLast >= wait) {
last = now;
callback(...args);
} else if (!throttling) {
throttling = true;
setTimeout(() => {
last = performance.now();
throttling = false;
callback(...args);
}, wait - timeSinceLast);
}
};
}
const websocket = new ResilientWebSocket(self);
const messageBatcher = new MessageBatcher(self);
const workerBroker = new WorkerToWebSocketMessageBroker(websocket, messageBatcher);
const websocketBroker = new WebSocketToWorkerMessageBroker(messageBatcher, self);
@@ -363,4 +418,6 @@ export default function installWorker() {
websocket.registerMessageCallback((data) => {
websocketBroker.routeMessageToHandler(data);
});
self.websocketInstance = websocket;
}

View File

@@ -24,11 +24,13 @@
<tr
ref="tableRow"
class="js-lad-table__body__row c-table__selectable-row"
aria-label="lad row"
@click="clickedRow"
@contextmenu.prevent="showContextMenu"
>
<td
ref="tableCell"
aria-label="lad name"
class="js-first-data"
@mouseover.ctrl="showToolTip"
@mouseleave="hideToolTip"
@@ -58,7 +60,7 @@
const CONTEXT_MENU_ACTIONS = ['viewDatumAction', 'viewHistoricalData', 'remove'];
const BLANK_VALUE = '---';
import identifierToString from '/src/tools/url.js';
import { objectPathToUrl } from '/src/tools/url.js';
import PreviewAction from '@/ui/preview/PreviewAction.js';
import tooltipHelpers from '../../../api/tooltips/tooltipMixins.js';
@@ -260,7 +262,7 @@ export default {
event.preventDefault();
this.preview(this.objectPath);
} else {
const resultUrl = identifierToString(this.openmct, this.objectPath);
const resultUrl = objectPathToUrl(this.openmct, this.objectPath);
this.openmct.router.navigate(resultUrl);
}
},

View File

@@ -22,7 +22,7 @@
<template>
<div class="c-lad-table-wrapper u-style-receiver js-style-receiver" :class="staleClass">
<table class="c-table c-lad-table" :class="applyLayoutClass">
<table aria-label="lad table" class="c-table c-lad-table" :class="applyLayoutClass">
<thead>
<tr>
<th>Name</th>

View File

@@ -25,6 +25,7 @@
aria-label="Clock Indicator"
class="c-indicator t-indicator-clock icon-clock no-minify c-indicator--not-clickable"
role="complementary"
aria-live="off"
>
<span class="label c-indicator__label">
{{ timeTextValue }}

View File

@@ -22,6 +22,7 @@
<template>
<div
aria-label="sub object frame"
class="l-layout__frame c-frame"
:class="{
'no-frame': !item.hasFrame,

View File

@@ -37,24 +37,24 @@
:style="styleObject"
:data-font-size="item.fontSize"
:data-font="item.font"
aria-label="Alpha-numeric telemetry"
@contextmenu.prevent="showContextMenu"
@mouseover.ctrl="showToolTip"
@mouseleave="hideToolTip"
>
<div
class="is-status__indicator"
:aria-label="`This item is ${status}`"
:title="`This item is ${status}`"
></div>
<div class="is-status__indicator"></div>
<div v-if="showLabel" class="c-telemetry-view__label">
<div class="c-telemetry-view__label-text">
<div
class="c-telemetry-view__label-text"
:aria-label="`Alpha-numeric telemetry name for ${domainObject.name}`"
>
{{ domainObject.name }}
</div>
</div>
<div
v-if="showValue"
:aria-label="fieldName"
:aria-label="`Alpha-numeric telemetry value of ${telemetryValue}`"
:title="fieldName"
class="c-telemetry-view__value"
:class="[telemetryClass]"

View File

@@ -23,7 +23,7 @@
<template>
<div class="c-fault-mgmt-item-header c-fault-mgmt__list-header c-fault-mgmt__list">
<div class="c-fault-mgmt-item-header c-fault-mgmt__checkbox">
<input type="checkbox" :checked="isSelectAll" @input="selectAll" />
<input type="checkbox" :checked="isSelectAll" @change="selectAll" />
</div>
<div
class="c-fault-mgmt-item-header c-fault-mgmt__list-header-results c-fault-mgmt__list-severity"

View File

@@ -23,7 +23,12 @@
<template>
<div class="c-fault-mgmt__list data-selectable" :class="classesFromState">
<div class="c-fault-mgmt-item c-fault-mgmt__list-checkbox">
<input type="checkbox" :checked="isSelected" @input="toggleSelected" />
<input
type="checkbox"
:aria-label="checkBoxAriaLabel"
:checked="isSelected"
@change="toggleSelected"
/>
</div>
<div class="c-fault-mgmt-item">
<div
@@ -60,6 +65,7 @@
<button
class="c-fault-mgmt__list-action-button l-browse-bar__actions c-icon-button icon-3-dots"
title="Disposition Actions"
aria-label="Disposition Actions"
@click="showActionMenu"
></button>
</div>
@@ -86,13 +92,14 @@ export default {
},
isSelected: {
type: Boolean,
default: () => {
return false;
}
default: false
}
},
emits: ['acknowledge-selected', 'shelve-selected', 'toggle-selected'],
emits: ['acknowledge-selected', 'shelve-selected', 'toggle-selected', 'clear-all-selected'],
computed: {
checkBoxAriaLabel() {
return `Select fault: ${this.fault.name}`;
},
classesFromState() {
const exclusiveStates = [
{
@@ -171,6 +178,7 @@ export default {
name: 'Acknowledge',
description: '',
onItemClicked: (e) => {
this.clearAllSelected();
this.$emit('acknowledge-selected', [this.fault]);
}
},
@@ -179,6 +187,7 @@ export default {
name: 'Shelve',
description: '',
onItemClicked: () => {
this.clearAllSelected();
this.$emit('shelve-selected', [this.fault], { shelved: true });
}
},
@@ -188,6 +197,7 @@ export default {
name: 'Unshelve',
description: '',
onItemClicked: () => {
this.clearAllSelected();
this.$emit('shelve-selected', [this.fault], { shelved: false });
}
}
@@ -202,6 +212,9 @@ export default {
};
this.$emit('toggle-selected', faultData);
},
clearAllSelected() {
this.$emit('clear-all-selected');
}
}
};

View File

@@ -1,307 +0,0 @@
<!--
Open MCT, Copyright (c) 2014-2024, 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-faults-list-view">
<FaultManagementSearch
:search-term="searchTerm"
@filter-changed="updateFilter"
@update-search-term="updateSearchTerm"
/>
<FaultManagementToolbar
v-if="showToolbar"
:selected-faults="selectedFaults"
@acknowledge-selected="toggleAcknowledgeSelected"
@shelve-selected="toggleShelveSelected"
/>
<div class="c-faults-list-view-header-item-container-wrapper">
<div class="c-faults-list-view-header-item-container">
<FaultManagementListHeader
class="header"
:selected-faults="Object.values(selectedFaults)"
:total-faults-count="filteredFaultsList.length"
@select-all="selectAll"
@sort-changed="sortChanged"
/>
<div class="c-faults-list-view-item-body">
<template v-if="filteredFaultsList.length > 0">
<FaultManagementListItem
v-for="fault of filteredFaultsList"
:key="fault.id"
:fault="fault"
:is-selected="isSelected(fault)"
@toggle-selected="toggleSelected"
@acknowledge-selected="toggleAcknowledgeSelected"
@shelve-selected="toggleShelveSelected"
/>
</template>
</div>
</div>
</div>
</div>
</template>
<script>
import { FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS, FILTER_ITEMS, SORT_ITEMS } from './constants.js';
import FaultManagementListHeader from './FaultManagementListHeader.vue';
import FaultManagementListItem from './FaultManagementListItem.vue';
import FaultManagementSearch from './FaultManagementSearch.vue';
import FaultManagementToolbar from './FaultManagementToolbar.vue';
const SEARCH_KEYS = [
'id',
'triggerValueInfo',
'currentValueInfo',
'triggerTime',
'severity',
'name',
'shortDescription',
'namespace'
];
export default {
components: {
FaultManagementListHeader,
FaultManagementListItem,
FaultManagementSearch,
FaultManagementToolbar
},
inject: ['openmct', 'domainObject'],
props: {
faultsList: {
type: Array,
default: () => []
}
},
data() {
return {
filterIndex: 0,
searchTerm: '',
selectedFaults: {},
sortBy: Object.values(SORT_ITEMS)[0].value
};
},
computed: {
filteredFaultsList() {
const filterName = FILTER_ITEMS[this.filterIndex];
let list = this.faultsList;
// Exclude shelved alarms from all views except the Shelved view
if (filterName !== 'Shelved') {
list = list.filter((fault) => fault.shelved !== true);
}
if (filterName === 'Acknowledged') {
list = list.filter((fault) => fault.acknowledged);
} else if (filterName === 'Unacknowledged') {
list = list.filter((fault) => !fault.acknowledged);
} else if (filterName === 'Shelved') {
list = list.filter((fault) => fault.shelved);
}
if (this.searchTerm.length > 0) {
list = list.filter(this.filterUsingSearchTerm);
}
list.sort(SORT_ITEMS[this.sortBy].sortFunction);
return list;
},
showToolbar() {
return this.openmct.faults.supportsActions();
}
},
methods: {
filterUsingSearchTerm(fault) {
if (!fault) {
return false;
}
let match = false;
SEARCH_KEYS.forEach((key) => {
if (fault[key]?.toString().toLowerCase().includes(this.searchTerm)) {
match = true;
}
});
return match;
},
isSelected(fault) {
return Boolean(this.selectedFaults[fault.id]);
},
selectAll(toggle = false) {
this.faultsList.forEach((fault) => {
const faultData = {
fault,
selected: toggle
};
this.toggleSelected(faultData);
});
},
sortChanged(sort) {
this.sortBy = sort.value;
},
toggleSelected({ fault, selected = false }) {
if (selected) {
this.selectedFaults[fault.id] = fault;
} else {
delete this.selectedFaults[fault.id];
}
const selectedFaults = Object.values(this.selectedFaults);
this.openmct.selection.select(
[
{
element: this.$el,
context: {
item: this.openmct.router.path[0]
}
},
{
element: this.$el,
context: {
selectedFaults
}
}
],
false
);
},
toggleAcknowledgeSelected(faults = Object.values(this.selectedFaults)) {
let title = '';
if (faults.length > 1) {
title = `Acknowledge ${faults.length} selected faults`;
} else {
title = `Acknowledge fault: ${faults[0].name}`;
}
const formStructure = {
title,
sections: [
{
rows: [
{
key: 'comment',
control: 'textarea',
name: 'Optional comment',
pattern: '\\S+',
required: false,
cssClass: 'l-input-lg',
value: ''
}
]
}
],
buttons: {
submit: {
label: 'Acknowledge'
}
}
};
this.openmct.forms.showForm(formStructure).then((data) => {
Object.values(faults).forEach((selectedFault) => {
this.openmct.faults.acknowledgeFault(selectedFault, data);
});
});
this.selectedFaults = {};
},
async toggleShelveSelected(faults = Object.values(this.selectedFaults), shelveData = {}) {
const { shelved = true } = shelveData;
if (shelved) {
let title =
faults.length > 1
? `Shelve ${faults.length} selected faults`
: `Shelve fault: ${faults[0].name}`;
const formStructure = {
title,
sections: [
{
rows: [
{
key: 'comment',
control: 'textarea',
name: 'Optional comment',
pattern: '\\S+',
required: false,
cssClass: 'l-input-lg',
value: ''
},
{
key: 'shelveDuration',
control: 'select',
name: 'Shelve duration',
options: FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS,
required: false,
cssClass: 'l-input-lg',
value: FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS[0].value
}
]
}
],
buttons: {
submit: {
label: 'Shelve'
}
}
};
let data;
try {
data = await this.openmct.forms.showForm(formStructure);
} catch (e) {
return;
}
shelveData.comment = data.comment || '';
shelveData.shelveDuration =
data.shelveDuration !== undefined
? data.shelveDuration
: FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS[0].value;
} else {
shelveData = {
shelved: false
};
}
Object.values(faults).forEach((selectedFault) => {
this.openmct.faults.shelveFault(selectedFault, shelveData);
});
this.selectedFaults = {};
},
updateFilter(filter) {
this.selectAll();
this.filterIndex = filter.model.options.findIndex((option) => option.value === filter.value);
},
updateSearchTerm(term = '') {
this.searchTerm = term.toLowerCase();
}
}
};
</script>

View File

@@ -24,20 +24,22 @@
<div class="c-fault-mgmt__toolbar">
<button
class="c-icon-button icon-check"
title="Acknowledge selected faults"
:title="acknowledgeButtonLabel"
:aria-label="acknowledgeButtonLabel"
:disabled="disableAcknowledge"
@click="acknowledgeSelected"
>
<div title="Acknowledge selected faults" class="c-icon-button__label">Acknowledge</div>
<div class="c-icon-button__label">Acknowledge</div>
</button>
<button
class="c-icon-button icon-timer"
title="Shelve selected faults"
:title="shelveButtonLabel"
:aria-label="shelveButtonLabel"
:disabled="disableShelve"
@click="shelveSelected"
>
<div title="Shelve selected items" class="c-icon-button__label">Shelve</div>
<div class="c-icon-button__label">Shelve</div>
</button>
</div>
</template>
@@ -60,6 +62,14 @@ export default {
disableShelve: true
};
},
computed: {
acknowledgeButtonLabel() {
return 'Acknowledge selected faults';
},
shelveButtonLabel() {
return 'Shelve selected faults';
}
},
watch: {
selectedFaults(newSelectedFaults) {
const selectedfaults = Object.values(newSelectedFaults);

View File

@@ -21,23 +21,123 @@
-->
<template>
<FaultManagementListView :faults-list="faultsList" />
<div class="c-faults-list-view">
<FaultManagementSearch
:search-term="searchTerm"
@filter-changed="updateFilter"
@update-search-term="updateSearchTerm"
/>
<FaultManagementToolbar
v-if="showToolbar"
:selected-faults="selectedFaults"
@acknowledge-selected="toggleAcknowledgeSelected"
@shelve-selected="toggleShelveSelected"
/>
<div class="c-faults-list-view-header-item-container-wrapper">
<div class="c-faults-list-view-header-item-container">
<FaultManagementListHeader
class="header"
:selected-faults="selectedFaults"
:total-faults-count="filteredFaultsList.length"
@select-all="selectAll"
@sort-changed="sortChanged"
/>
<div class="c-faults-list-view-item-body">
<template v-if="filteredFaultsList.length > 0">
<FaultManagementListItem
v-for="fault of filteredFaultsList"
:key="fault.id"
:fault="fault"
:is-selected="isSelected(fault)"
@toggle-selected="toggleSelected"
@acknowledge-selected="toggleAcknowledgeSelected"
@shelve-selected="toggleShelveSelected"
@clear-all-selected="resetSelectedFaultMap"
/>
</template>
</div>
</div>
</div>
</div>
</template>
<script>
import { FAULT_MANAGEMENT_ALARMS, FAULT_MANAGEMENT_GLOBAL_ALARMS } from './constants.js';
import FaultManagementListView from './FaultManagementListView.vue';
import {
FAULT_MANAGEMENT_ALARMS,
FAULT_MANAGEMENT_GLOBAL_ALARMS,
FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS,
FILTER_ITEMS,
SORT_ITEMS
} from './constants.js';
import FaultManagementListHeader from './FaultManagementListHeader.vue';
import FaultManagementListItem from './FaultManagementListItem.vue';
import FaultManagementSearch from './FaultManagementSearch.vue';
import FaultManagementToolbar from './FaultManagementToolbar.vue';
const SEARCH_KEYS = [
'id',
'triggerValueInfo',
'currentValueInfo',
'triggerTime',
'severity',
'name',
'shortDescription',
'namespace'
];
export default {
components: {
FaultManagementListView
FaultManagementListHeader,
FaultManagementListItem,
FaultManagementSearch,
FaultManagementToolbar
},
inject: ['openmct', 'domainObject'],
data() {
return {
faultsList: []
faultsList: [],
filterIndex: 0,
searchTerm: '',
selectedFaultMap: {},
sortBy: Object.values(SORT_ITEMS)[0].value
};
},
computed: {
selectedFaults() {
return Object.values(this.selectedFaultMap);
},
filteredFaultsList() {
const filterName = FILTER_ITEMS[this.filterIndex];
let list = this.faultsList;
// Exclude shelved alarms from all views except the Shelved view
if (filterName !== 'Shelved') {
list = list.filter((fault) => fault.shelved !== true);
}
if (filterName === 'Acknowledged') {
list = list.filter((fault) => fault.acknowledged);
} else if (filterName === 'Unacknowledged') {
list = list.filter((fault) => !fault.acknowledged);
} else if (filterName === 'Shelved') {
list = list.filter((fault) => fault.shelved);
}
if (this.searchTerm.length > 0) {
list = list.filter(this.filterUsingSearchTerm);
}
list.sort(SORT_ITEMS[this.sortBy].sortFunction);
return list;
},
showToolbar() {
return this.openmct.faults.supportsActions();
}
},
mounted() {
this.unsubscribe = this.openmct.faults.subscribe(this.domainObject, this.updateFault);
},
@@ -66,6 +166,181 @@ export default {
this.faultsList = [];
}
});
},
filterUsingSearchTerm(fault) {
if (!fault) {
return false;
}
let match = false;
SEARCH_KEYS.forEach((key) => {
if (fault[key]?.toString().toLowerCase().includes(this.searchTerm)) {
match = true;
}
});
return match;
},
isSelected(fault) {
return Boolean(this.selectedFaultMap[fault.id]);
},
selectAll(toggle = false) {
this.faultsList.forEach((fault) => {
const faultData = {
fault,
selected: toggle
};
this.toggleSelected(faultData);
});
},
sortChanged(sort) {
this.sortBy = sort.value;
},
toggleSelected({ fault, selected = false }) {
if (selected) {
this.selectedFaultMap[fault.id] = fault;
} else {
delete this.selectedFaultMap[fault.id];
}
this.openmct.selection.select(
[
{
element: this.$el,
context: {
item: this.openmct.router.path[0]
}
},
{
element: this.$el,
context: {
selectedFaults: this.selectedFaults
}
}
],
false
);
},
async toggleAcknowledgeSelected(faults = this.selectedFaults) {
let title = '';
if (faults.length > 1) {
title = `Acknowledge ${faults.length} selected faults`;
} else if (faults.length === 1) {
title = `Acknowledge fault: ${faults[0].name}`;
}
const formStructure = {
title,
sections: [
{
rows: [
{
key: 'comment',
control: 'textarea',
name: 'Optional comment',
pattern: '\\S+',
required: false,
cssClass: 'l-input-lg',
value: ''
}
]
}
],
buttons: {
submit: {
label: 'Acknowledge'
}
}
};
try {
const data = await this.openmct.forms.showForm(formStructure);
faults.forEach((fault) => {
this.openmct.faults.acknowledgeFault(fault, data);
});
} catch (err) {
console.error(err);
} finally {
this.resetSelectedFaultMap();
}
},
resetSelectedFaultMap() {
Object.keys(this.selectedFaultMap).forEach((key) => {
delete this.selectedFaultMap[key];
});
},
async toggleShelveSelected(faults = this.selectedFaults, shelveData = {}) {
const { shelved = true } = shelveData;
if (shelved) {
let title =
faults.length > 1
? `Shelve ${faults.length} selected faults`
: `Shelve fault: ${faults[0].name}`;
const formStructure = {
title,
sections: [
{
rows: [
{
key: 'comment',
control: 'textarea',
name: 'Optional comment',
pattern: '\\S+',
required: false,
cssClass: 'l-input-lg',
value: ''
},
{
key: 'shelveDuration',
control: 'select',
name: 'Shelve duration',
options: FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS,
required: false,
cssClass: 'l-input-lg',
value: FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS[0].value
}
]
}
],
buttons: {
submit: {
label: 'Shelve'
}
}
};
let data;
try {
data = await this.openmct.forms.showForm(formStructure);
} catch (e) {
return;
}
shelveData.comment = data.comment || '';
shelveData.shelveDuration =
data.shelveDuration !== undefined
? data.shelveDuration
: FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS[0].value;
} else {
shelveData = {
shelved: false
};
}
Object.values(faults).forEach((selectedFault) => {
this.openmct.faults.shelveFault(selectedFault, shelveData);
});
this.selectedFaultMap = {};
},
updateFilter(filter) {
this.selectAll();
this.filterIndex = filter.model.options.findIndex((option) => option.value === filter.value);
},
updateSearchTerm(term = '') {
this.searchTerm = term.toLowerCase();
}
}
};

View File

@@ -220,6 +220,7 @@
lengthAdjust="spacing"
text-anchor="middle"
dominant-baseline="middle"
:aria-label="`gauge value of ${curVal}`"
x="50%"
y="50%"
>

View File

@@ -25,7 +25,18 @@
<div class="c-inspect-properties">
<div class="c-inspect-properties__header">Numeric Data</div>
</div>
<div ref="numericDataView"></div>
<div ref="numericDataView">
<TelemetryFrame
v-for="plotObject of plotObjects"
:key="plotObject.identifier.key"
:bounds="bounds"
:telemetry-object="plotObject"
:path="[plotObject]"
:render-when-visible="plotObject.renderWhenVisible"
>
<Plot />
</TelemetryFrame>
</div>
<div v-if="!hasNumericData">
{{ noNumericDataText }}
@@ -33,13 +44,15 @@
</div>
</template>
<script>
import mount from 'utils/mount';
import VisibilityObserver from '../../utils/visibility/VisibilityObserver.js';
import Plot from '../plot/PlotView.vue';
import TelemetryFrame from './TelemetryFrame.vue';
export default {
components: {
TelemetryFrame,
Plot
},
inject: ['openmct', 'domainObject', 'timeFormatter'],
props: {
bounds: {
@@ -90,16 +103,19 @@ export default {
this.clearPlots();
this.unregisterTimeContextList = [];
this.componentsList = [];
this.elementsList = [];
this.visibilityObservers = [];
this.telemetryKeys.forEach(async (telemetryKey) => {
const plotObject = await this.openmct.objects.get(telemetryKey);
const visibilityObserver = new VisibilityObserver(
this.$refs.numericDataView,
this.openmct.element
);
plotObject.renderWhenVisible = visibilityObserver.renderWhenVisible;
this.visibilityObservers.push(visibilityObserver);
this.plotObjects.push(plotObject);
this.unregisterTimeContextList.push(this.setIndependentTimeContextForComponent(plotObject));
this.renderPlot(plotObject);
});
},
setIndependentTimeContextForComponent(plotObject) {
@@ -110,63 +126,14 @@ export default {
// set the time context of the object to the selected time range
return this.openmct.time.addIndependentContext(keyString, this.bounds);
},
renderPlot(plotObject) {
const wrapper = document.createElement('div');
const visibilityObserver = new VisibilityObserver(wrapper, this.openmct.element);
const { destroy } = mount(
{
components: {
TelemetryFrame,
Plot
},
provide: {
openmct: this.openmct,
path: [plotObject],
renderWhenVisible: visibilityObserver.renderWhenVisible
},
data() {
return {
plotObject,
bounds: this.bounds
};
},
template: `<TelemetryFrame
:bounds="bounds"
:telemetry-object="plotObject"
>
<Plot />
</TelemetryFrame>`
},
{
app: this.openmct.app,
element: wrapper
}
);
this.componentsList.push(destroy);
this.elementsList.push(wrapper);
this.visibilityObservers.push(visibilityObserver);
this.$refs.numericDataView.append(wrapper);
},
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.visibilityObservers?.length) {
this.visibilityObservers.forEach((visibilityObserver) => visibilityObserver.destroy());
delete this.visibilityObservers;
}
if (this.plotObjects?.length) {
this.plotObjects = [];
this.plotObjects.splice(0, this.plotObjects.length);
}
if (this.unregisterTimeContextList?.length) {

View File

@@ -70,7 +70,9 @@ export default {
inject: ['openmct'],
provide() {
return {
domainObject: this.telemetryObject
domainObject: this.telemetryObject,
path: this.path,
renderWhenVisible: this.renderWhenVisible
};
},
props: {
@@ -81,6 +83,14 @@ export default {
telemetryObject: {
type: Object,
default: () => {}
},
path: {
type: Array,
default: () => []
},
renderWhenVisible: {
type: Function,
required: true
}
},
data() {
@@ -110,7 +120,10 @@ export default {
'tc.mode': 'fixed'
};
const newTabAction = this.openmct.actions.getAction('newTab');
newTabAction.invoke([sourceTelemObject], urlParams);
// No view context needed, so pass undefined.
// The urlParams arg will override the global time bounds with the data visualization
// plot bounds.
newTabAction.invoke([sourceTelemObject], undefined, urlParams);
this.showMenu = false;
},
previewTelemetry() {

View File

@@ -52,7 +52,7 @@
import Moment from 'moment';
import mount from 'utils/mount';
import objectPathToUrl from '@/tools/url';
import { objectPathToUrl } from '@/tools/url';
import tooltipHelpers from '../../../api/tooltips/tooltipMixins.js';
import ImageExporter from '../../../exporters/ImageExporter.js';

View File

@@ -23,11 +23,11 @@
<div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left">
<button
class="c-icon-button c-button--menu icon-camera"
aria-label="Take a Notebook Snapshot"
title="Take a Notebook Snapshot"
:aria-label="snapshotMenuLabel"
:title="snapshotMenuLabel"
@click.stop.prevent="showMenu"
>
<span title="Take Notebook Snapshot" class="c-icon-button__label"> Snapshot </span>
<span class="c-icon-button__label">Snapshot</span>
</button>
</div>
</template>
@@ -72,6 +72,11 @@ export default {
notebookTypes: []
};
},
computed: {
snapshotMenuLabel() {
return 'Open the Notebook Snapshot Menu';
}
},
mounted() {
validateNotebookStorageObject();

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 objectPathToUrl from '/src/tools/url.js';
import { objectPathToUrl } from '/src/tools/url.js';
export default class OpenInNewTab {
constructor(openmct) {
this.name = 'Open In New Tab';
@@ -31,8 +31,26 @@ export default class OpenInNewTab {
this._openmct = openmct;
}
invoke(objectPath, urlParams = undefined) {
let url = objectPathToUrl(this._openmct, objectPath, urlParams);
window.open(url);
/**
* Invokes the "Open in New Tab" action. This will open the object in a new
* browser tab. The URL for the new tab is determined by the current object
* path and any custom time bounds.
*
* @param {import('@/api/objects/ObjectAPI').DomainObject[]} objectPath The current object path
* @param {ViewContext} _view The view context for the object being opened (unused)
* @param {Object<string, string | number>} customUrlParams Provides the ability to override
* the global time conductor bounds. It is an object with the following key/value pairs:
* ```
* {
* 'tc.start': <number>,
* 'tc.end': <number>,
* 'tc.mode': 'fixed' | 'local' | <string>
* }
* ```
*/
invoke(objectPath, _view, customUrlParams) {
const url = objectPathToUrl(this._openmct, objectPath, customUrlParams);
window.open(url, undefined, 'noopener');
}
}

View File

@@ -201,10 +201,15 @@ export default {
if (this.compositionCollection) {
this.compositionCollection.on('add', this.subscribeToStaleness);
this.compositionCollection.on('remove', this.triggerUnsubscribeFromStaleness);
this.compositionCollection.on('remove', this.removeSubscription);
this.compositionCollection.load();
}
},
removeSubscription(identifier) {
this.triggerUnsubscribeFromStaleness({
identifier
});
},
loadingUpdated(loading) {
this.loading = loading;
this.$emit('loading-updated', ...arguments);
@@ -212,7 +217,7 @@ export default {
destroy() {
if (this.compositionCollection) {
this.compositionCollection.off('add', this.subscribeToStaleness);
this.compositionCollection.off('remove', this.triggerUnsubscribeFromStaleness);
this.compositionCollection.off('remove', this.removeSubscription);
}
this.imageExporter = null;

View File

@@ -39,13 +39,13 @@
<div ref="limitArea" class="js-limit-area" aria-hidden="true">
<limit-label
v-for="(limitLabel, index) in visibleLimitLabels"
:key="index"
:key="`limitLabel-${limitLabel.limit.seriesKey}-${index}`"
:point="limitLabel.point"
:limit="limitLabel.limit"
></limit-label>
<limit-line
v-for="(limitLine, index) in visibleLimitLines"
:key="index"
:key="`limitLine-${limitLine.limit.seriesKey}${index}`"
:point="limitLine.point"
:limit="limitLine.limit"
></limit-line>
@@ -201,9 +201,8 @@ export default {
handler() {
this.hiddenYAxisIds.forEach((id) => {
this.resetYOffsetAndSeriesDataForYAxis(id);
this.updateLimitLines();
});
this.scheduleDraw();
this.scheduleDraw(true);
},
deep: true
}
@@ -271,12 +270,19 @@ export default {
this.listenTo(this.config.xAxis, 'change', this.redrawIfNotAlreadyHandled);
this.config.series.forEach(this.onSeriesAdd, this);
this.$emit('chart-loaded');
this.handleWindowResize = _.debounce(this.handleWindowResize, 250);
this.chartResizeObserver = new ResizeObserver(this.handleWindowResize);
this.chartResizeObserver.observe(this.$parent.$refs.chartContainer);
},
beforeUnmount() {
this.destroy();
this.visibilityObserver.unobserve(this.chartContainer);
},
methods: {
handleWindowResize() {
this.scheduleDraw(true);
},
getConfig() {
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
let config = configStore.get(configId);
@@ -445,7 +451,6 @@ export default {
this.makeLimitLines(series);
this.updateLimitLines();
this.scheduleDraw();
},
resetAxisAndRedraw(newYAxisId, oldYAxisId, series) {
if (!oldYAxisId) {
@@ -459,8 +464,7 @@ export default {
//Make the chart elements again for the new y-axis and offset
this.makeChartElement(series);
this.makeLimitLines(series);
this.updateLimitLines();
this.scheduleDraw();
this.scheduleDraw(true);
},
destroy() {
this.destroyCanvas();
@@ -469,6 +473,10 @@ export default {
this.limitLines.forEach((line) => line.destroy());
this.pointSets.forEach((pointSet) => pointSet.destroy());
this.alarmSets.forEach((alarmSet) => alarmSet.destroy());
DrawLoader.releaseDrawAPI(this.drawAPI);
if (this.chartResizeObserver) {
this.chartResizeObserver.disconnect();
}
},
resetYOffsetAndSeriesDataForYAxis(yAxisId) {
delete this.offset[yAxisId].y;
@@ -703,12 +711,11 @@ export default {
return;
}
this.updateLimitLines();
this.scheduleDraw();
this.scheduleDraw(true);
},
scheduleDraw() {
scheduleDraw(updateLimitLines) {
if (!this.drawScheduled) {
const called = this.renderWhenVisible(this.draw);
const called = this.renderWhenVisible(this.draw.bind(this, updateLimitLines));
this.drawScheduled = called;
if (!this.drawnOnce && called) {
this.drawnOnce = true;
@@ -716,7 +723,7 @@ export default {
}
}
},
draw() {
draw(updateLimitLines) {
this.drawScheduled = false;
if (this.isDestroyed || !this.chartVisible) {
return;
@@ -744,6 +751,11 @@ export default {
this.prepareToDrawAnnotationSelections(id);
}
});
// We must do the limit line drawing after the drawAPI has been cleared (which sets the height and width of the draw API)
// and the viewport is updated so that we have the right height/width for limit line x and y calculations
if (updateLimitLines) {
this.updateLimitLines();
}
},
updateViewport(yAxisId) {
if (!this.chartVisible) {
@@ -799,9 +811,12 @@ export default {
pointSets.forEach(this.drawPoints, this);
const alarmSets = this.alarmSets.filter(this.matchByYAxisId.bind(this, id));
alarmSets.forEach(this.drawAlarmPoints, this);
//console.timeEnd('📈 drawSeries');
},
updateLimitLines() {
//reset
this.visibleLimitLabels = [];
this.visibleLimitLines = [];
this.config.series.models.forEach((series) => {
const yAxisId = series.get('yAxisId');
@@ -820,11 +835,7 @@ export default {
if (!this.drawAPI.origin) {
return;
}
let limitPointOverlap = [];
//reset
this.visibleLimitLabels = [];
this.visibleLimitLines = [];
this.limitLines.forEach((limitLine) => {
limitLine.limits.forEach((limit) => {

View File

@@ -91,9 +91,16 @@
</div>
</li>
<li class="grid-row">
<div class="grid-cell label" title="Display limit lines">Limit lines</div>
<div id="limit-lines-checkbox" class="grid-cell label" title="Display limit lines">
Limit lines
</div>
<div class="grid-cell value">
<input v-model="limitLines" type="checkbox" @change="updateForm('limitLines')" />
<input
v-model="limitLines"
aria-labelledby="limit-lines-checkbox"
type="checkbox"
@change="updateForm('limitLines')"
/>
</div>
</li>
<li v-show="markers || alarmMarkers" class="grid-row">

View File

@@ -165,7 +165,6 @@ export default {
this.registerListeners(this.config);
}
this.listenTo(this.config.legend, 'change:expandByDefault', this.changeExpandDefault, this);
this.initialize();
},
mounted() {
this.loaded = true;
@@ -182,16 +181,6 @@ export default {
this.stopListening();
},
methods: {
initialize() {
if (this.domainObject.type === 'telemetry.plot.stacked') {
this.objectComposition = this.openmct.composition.get(this.domainObject);
this.objectComposition.on('add', this.addTelemetryObject);
this.objectComposition.on('remove', this.removeTelemetryObject);
this.objectComposition.load();
} else {
this.registerListeners(this.config);
}
},
changeExpandDefault() {
this.isLegendExpanded = this.config.legend.model.expandByDefault;
this.legend.set('expanded', this.isLegendExpanded);

View File

@@ -22,7 +22,7 @@
<template>
<div
class="plot-legend-item"
:aria-label="`Plot Legend Item for ${domainObject?.name}`"
:aria-label="`Plot Legend Item for ${seriesName}`"
:class="{
'is-stale': isStale,
'is-status--missing': isMissing
@@ -36,9 +36,8 @@
@mouseover.ctrl="showToolTip"
@mouseleave="hideToolTip"
>
<span class="plot-series-color-swatch" :style="{ 'background-color': colorAsHexString }">
</span>
<span class="is-status__indicator" title="This item is missing or suspect"></span>
<span class="plot-series-color-swatch" :style="{ 'background-color': colorAsHexString }" />
<span class="is-status__indicator" title="This item is missing or suspect" />
<span class="plot-series-name">{{ nameWithUnit }}</span>
</div>
<div
@@ -89,6 +88,7 @@ export default {
isMissing: false,
colorAsHexString: '',
nameWithUnit: '',
seriesName: '',
formattedYValue: '',
formattedXValue: '',
mctLimitStateClass: '',
@@ -206,6 +206,9 @@ export default {
const seriesIndexToRemove = this.seriesModels.findIndex(
(series) => series.keyString === seriesToRemove.keyString
);
if (seriesIndexToRemove === -1) {
return;
}
this.seriesModels.splice(seriesIndexToRemove, 1);
},
getSeries(keyStringToFind) {
@@ -220,6 +223,7 @@ export default {
this.isMissing = seriesObject.domainObject.status === 'missing';
this.colorAsHexString = seriesObject.get('color').asHexString();
this.seriesName = seriesObject.domainObject.name;
this.nameWithUnit = seriesObject.nameWithUnit();
const closest = seriesObject.closest;

View File

@@ -135,7 +135,7 @@ export default {
this.openmct.editor.off('isEditing', this.setEditState);
if (this.composition) {
this.composition.off('add', this.subscribeToStaleness);
this.composition.off('remove', this.triggerUnsubscribeFromStaleness);
this.composition.off('remove', this.removeSubscription);
}
if (this.removeSelectable) {
@@ -161,6 +161,11 @@ export default {
}
}
},
removeSubscription(identifier) {
this.triggerUnsubscribeFromStaleness({
identifier
});
},
updateView() {
//If this object is not persistable, then package it with it's parent
const plotObject = this.getPlotObject();
@@ -172,7 +177,7 @@ export default {
this.composition = this.openmct.composition.get(plotObject);
this.composition.on('add', this.subscribeToStaleness);
this.composition.on('remove', this.triggerUnsubscribeFromStaleness);
this.composition.on('remove', this.removeSubscription);
this.composition.load();
}

View File

@@ -287,7 +287,7 @@
<script>
import _ from 'lodash';
import { toRaw } from 'vue';
import { onMounted, ref, toRaw } from 'vue';
import stalenessMixin from '@/ui/mixins/staleness-mixin';
@@ -295,7 +295,7 @@ import CSVExporter from '../../../exporters/CSVExporter.js';
import ProgressBar from '../../../ui/components/ProgressBar.vue';
import Search from '../../../ui/components/SearchComponent.vue';
import ToggleSwitch from '../../../ui/components/ToggleSwitch.vue';
import throttle from '../../../utils/throttle';
import { useResizeObserver } from '../../../ui/composables/resize.js';
import SizingRow from './SizingRow.vue';
import TableColumnHeader from './TableColumnHeader.vue';
import TableFooterIndicator from './TableFooterIndicator.vue';
@@ -303,7 +303,6 @@ import TelemetryTableRow from './TableRow.vue';
const VISIBLE_ROW_COUNT = 100;
const ROW_HEIGHT = 17;
const RESIZE_POLL_INTERVAL = 200;
const AUTO_SCROLL_TRIGGER_HEIGHT = ROW_HEIGHT * 3;
export default {
@@ -354,6 +353,15 @@ export default {
}
},
emits: ['marked-rows-updated', 'filter'],
setup() {
const root = ref(null);
const { size: containerSize, startObserving } = useResizeObserver();
onMounted(() => {
startObserving(root.value);
});
return { containerSize, root };
},
data() {
let configuration = this.table.configuration.getConfiguration();
@@ -441,6 +449,13 @@ export default {
}
},
watch: {
//This should be refactored so that it doesn't require an explicit watch. Should be doable.
containerSize: {
handler() {
this.debouncedRescaleToContainer();
},
deep: true
},
loading: {
handler(isLoading) {
if (isLoading) {
@@ -500,9 +515,10 @@ export default {
this.filterTelemetry = _.debounce(this.filterTelemetry, 500);
},
mounted() {
this.throttledUpdateVisibleRows = _.throttle(this.updateVisibleRows, 1000, { leading: true });
this.debouncedRescaleToContainer = _.debounce(this.rescaleToContainer, 300);
this.csvExporter = new CSVExporter();
this.rowsAdded = _.throttle(this.rowsAdded, 200);
this.rowsRemoved = _.throttle(this.rowsRemoved, 200);
this.scroll = _.throttle(this.scroll, 100);
if (!this.marking.useAlternateControlBar && !this.enableLegacyToolbar) {
@@ -515,8 +531,6 @@ export default {
});
}
this.updateVisibleRows = throttle(this.updateVisibleRows, 1000);
this.table.on('object-added', this.addObject);
this.table.on('object-removed', this.removeObject);
this.table.on('refresh', this.clearRowsAndRerender);
@@ -526,8 +540,8 @@ export default {
this.table.tableRows.on('add', this.rowsAdded);
this.table.tableRows.on('remove', this.rowsRemoved);
this.table.tableRows.on('sort', this.updateVisibleRows);
this.table.tableRows.on('filter', this.updateVisibleRows);
this.table.tableRows.on('sort', this.throttledUpdateVisibleRows);
this.table.tableRows.on('filter', this.throttledUpdateVisibleRows);
this.openmct.time.on('bounds', this.boundsChanged);
@@ -540,10 +554,10 @@ export default {
this.table.configuration.on('change', this.updateConfiguration);
this.calculateTableSize();
this.pollForResize();
this.calculateScrollbarWidth();
this.table.initialize();
this.rescaleToContainer();
},
beforeUnmount() {
this.table.off('object-added', this.addObject);
@@ -555,15 +569,13 @@ export default {
this.table.tableRows.off('add', this.rowsAdded);
this.table.tableRows.off('remove', this.rowsRemoved);
this.table.tableRows.off('sort', this.updateVisibleRows);
this.table.tableRows.off('filter', this.updateVisibleRows);
this.table.tableRows.off('sort', this.throttledUpdateVisibleRows);
this.table.tableRows.off('filter', this.throttledUpdateVisibleRows);
this.table.configuration.off('change', this.updateConfiguration);
this.openmct.time.off('bounds', this.boundsChanged);
clearInterval(this.resizePollHandle);
this.table.configuration.destroy();
this.table.destroy();
@@ -684,7 +696,7 @@ export default {
this.table.sortBy(this.sortOptions);
},
scroll() {
this.updateVisibleRows();
this.throttledUpdateVisibleRows();
this.synchronizeScrollX();
if (this.shouldAutoScroll()) {
@@ -757,11 +769,11 @@ export default {
this.initiateAutoScroll();
}
this.updateVisibleRows();
this.throttledUpdateVisibleRows();
},
rowsRemoved(rows) {
this.setHeight();
this.updateVisibleRows();
this.throttledUpdateVisibleRows();
},
/**
* Calculates height based on total number of rows, and sets table height.
@@ -880,35 +892,27 @@ export default {
dropTargetActive(isActive) {
this.isDropTargetActive = isActive;
},
pollForResize() {
let el = this.$refs.root;
let width = el.clientWidth;
let height = el.clientHeight;
rescaleToContainer() {
let scrollTop = this.scrollable.scrollTop;
this.resizePollHandle = setInterval(() => {
this.renderWhenVisible(() => {
if ((el.clientWidth !== width || el.clientHeight !== height) && this.isAutosizeEnabled) {
this.calculateTableSize();
// On some resize events scrollTop is reset to 0. Possibly due to a transition we're using?
// Need to preserve scroll position in this case.
if (this.autoScroll) {
this.initiateAutoScroll();
} else {
this.scrollable.scrollTop = scrollTop;
}
width = el.clientWidth;
height = el.clientHeight;
this.renderWhenVisible(() => {
if (this.isAutosizeEnabled) {
this.calculateTableSize();
// On some resize events scrollTop is reset to 0. Possibly due to a transition we're using?
// Need to preserve scroll position in this case.
if (this.autoScroll) {
this.initiateAutoScroll();
} else {
this.scrollable.scrollTop = scrollTop;
}
}
scrollTop = this.scrollable.scrollTop;
});
}, RESIZE_POLL_INTERVAL);
scrollTop = this.scrollable.scrollTop;
});
},
clearRowsAndRerender() {
this.visibleRows = [];
this.$nextTick().then(this.updateVisibleRows);
this.$nextTick().then(this.throttledUpdateVisibleRows);
},
pause(byButton) {
if (byButton) {

View File

@@ -35,7 +35,6 @@ import utcMultiTimeFormat from './utcMultiTimeFormat.js';
const PADDING = 1;
const DEFAULT_DURATION_FORMATTER = 'duration';
const RESIZE_POLL_INTERVAL = 200;
const PIXELS_PER_TICK = 100;
const PIXELS_PER_TICK_WIDE = 200;
@@ -92,7 +91,6 @@ export default {
//Respond to changes in conductor
this.openmct.time.on(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setViewFromTimeSystem);
this.resizeTimer = setInterval(this.resize, RESIZE_POLL_INTERVAL);
},
beforeUnmount() {
clearInterval(this.resizeTimer);

View File

@@ -148,7 +148,10 @@ export default {
}
},
mounted() {
this.handleNewBounds = _.throttle(this.handleNewBounds, 300);
this.handleNewBounds = _.throttle(this.handleNewBounds, 300, {
leading: true,
trailing: false
});
this.setTimeSystem(this.copy(this.openmct.time.getTimeSystem()));
this.openmct.time.on(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setTimeSystem);
this.setTimeContext();

View File

@@ -1,14 +1,14 @@
<template>
<form ref="fixedDeltaInput">
<div class="c-tc-input-popup__input-grid">
<div class="pr-time-label"><em>Start</em> Date</div>
<div class="pr-time-label">Time</div>
<div class="pr-time-label"></div>
<div class="pr-time-label"><em>End</em> Date</div>
<div class="pr-time-label">Time</div>
<div class="pr-time-label"></div>
<div class="pr-time-label pr-time-label-start-date"><em>Start</em> Date</div>
<div class="pr-time-label pr-time-label-start-time">Time</div>
<div class="pr-time-label pr-time-label-end-date"><em>End</em> Date</div>
<div class="pr-time-label pr-time-label-end-time">Time</div>
<div class="pr-time-input pr-time-input--date pr-time-input--input-and-button">
<div
class="pr-time-input pr-time-input--date pr-time-input--input-and-button pr-time-input-start-date"
>
<input
ref="startDate"
v-model="formattedBounds.start"
@@ -28,7 +28,7 @@
/>
</div>
<div class="pr-time-input pr-time-input--time">
<div class="pr-time-input pr-time-input--time pr-time-input-start-time">
<input
ref="startTime"
v-model="formattedBounds.startTime"
@@ -43,7 +43,9 @@
<div class="pr-time-input pr-time-input__start-end-sep icon-arrows-right-left"></div>
<div class="pr-time-input pr-time-input--date pr-time-input--input-and-button">
<div
class="pr-time-input pr-time-input--date pr-time-input--input-and-button pr-time-input-end-date"
>
<input
ref="endDate"
v-model="formattedBounds.end"
@@ -63,7 +65,7 @@
/>
</div>
<div class="pr-time-input pr-time-input--time">
<div class="pr-time-input pr-time-input--time pr-time-input-end-time">
<input
ref="endTime"
v-model="formattedBounds.endTime"

View File

@@ -1,16 +1,14 @@
<template>
<form ref="deltaInput">
<div class="c-tc-input-popup__input-grid">
<div class="pr-time-label icon-minus">Hrs</div>
<div class="pr-time-label">Mins</div>
<div class="pr-time-label">Secs</div>
<div class="pr-time-label"></div>
<div class="pr-time-label icon-plus">Hrs</div>
<div class="pr-time-label">Mins</div>
<div class="pr-time-label">Secs</div>
<div class="pr-time-label"></div>
<div class="pr-time-label icon-minus pr-time-label-minus-hrs">Hrs</div>
<div class="pr-time-label pr-time-label-minus-mins">Mins</div>
<div class="pr-time-label pr-time-label-minus-secs">Secs</div>
<div class="pr-time-label icon-plus pr-time-label-plus-hrs">Hrs</div>
<div class="pr-time-label pr-time-label-plus-mins">Mins</div>
<div class="pr-time-label pr-time-label-plus-secs">Secs</div>
<div class="pr-time-input">
<div class="pr-time-input pr-time-input-minus-hrs">
<input
ref="startInputHrs"
v-model="startInputHrs"
@@ -29,7 +27,7 @@
/>
<b>:</b>
</div>
<div class="pr-time-input">
<div class="pr-time-input pr-time-input-minus-mins">
<input
ref="startInputMins"
v-model="startInputMins"
@@ -48,7 +46,7 @@
/>
<b>:</b>
</div>
<div class="pr-time-input">
<div class="pr-time-input pr-time-input-minus-secs">
<input
ref="startInputSecs"
v-model="startInputSecs"
@@ -69,7 +67,7 @@
<div class="pr-time-input pr-time-input__start-end-sep icon-arrows-right-left"></div>
<div class="pr-time-input">
<div class="pr-time-input pr-time-input-plus-hrs">
<input
ref="endInputHrs"
v-model="endInputHrs"
@@ -88,7 +86,7 @@
/>
<b>:</b>
</div>
<div class="pr-time-input">
<div class="pr-time-input pr-time-input-plus-mins">
<input
ref="endInputMins"
v-model="endInputMins"
@@ -106,7 +104,7 @@
/>
<b>:</b>
</div>
<div class="pr-time-input">
<div class="pr-time-input pr-time-input-plus-secs">
<input
ref="endInputSecs"
v-model="endInputSecs"

View File

@@ -604,23 +604,170 @@
padding: cButtonPadding($compact: true);
}
}
.pr-time{
// FIXED TIME MODE
&-label-start-date{
grid-area: sDate;
}
&-label-start-time{
grid-area: sTime;
}
&-input-start-date{
grid-area: sDateInput;
}
&-input-start-time{
grid-area: sTimeInput;
}
&-label-end-date{
grid-area: eDate;
}
&-label-end-time{
grid-area: eTime;
}
&-input-end-date{
grid-area: eDateInput;
}
&-input-end-time{
grid-area: eTimeInput;
}
&-label-blank-grid{
grid-area: blank;
}
//REAL TIME MODE
&-label-minus-hrs{
grid-area: labelMinusHrs;
}
&-label-minus-mins{
grid-area: labelMinusMins;
}
&-label-minus-secs{
grid-area: labelMinusSecs;
}
&-label-plus-hrs{
grid-area: labelPlusHrs;
}
&-label-plus-mins{
grid-area: labelPlusMins;
}
&-label-plus-secs{
grid-area: labelPlusSecs;
}
&-input-minus-hrs{
grid-area: inputMinusHrs;
}
&-input-minus-mins{
grid-area: inputMinusMins;
}
&-input-minus-secs{
grid-area: inputMinusSecs;
}
&-input-plus-hrs{
grid-area: inputPlusHrs;
}
&-input-plus-mins{
grid-area: inputPlusMins;
}
&-input-plus-secs{
grid-area: inputPlusSecs;
}
// USED FOR BOTH
&-label-blank-grid{
grid-area: empty;
}
&-input__start-end-sep{
grid-area: arrowIcon;
}
&-input--buttons{
grid-area: buttons;
}
}
&--fixed-mode {
.c-tc-input-popup__input-grid {
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 2fr;
grid-template-areas:
"sDate sTime . eDate eTime ."
"sDateInput sTimeInput arrowIcon eDateInput eTimeInput buttons";
}
@include phonePortrait(){
.c-tc-input-popup__input-grid {
grid-template-columns: repeat(2, max-content) 1fr;
grid-template-areas:
"sDate sTime ."
"sDateInput sTimeInput ."
"eDate eTime ."
"eDateInput eTimeInput buttons";
padding: 2px;
overflow: hidden;
}
}
}
&--realtime-mode {
.c-tc-input-popup__input-grid {
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 2fr;
grid-template-areas:
"labelMinusHrs labelMinusMins labelMinusSecs . labelPlusHrs labelPlusMins labelPlusSecs ."
"inputMinusHrs inputMinusMins inputMinusSecs arrowIcon inputPlusHrs inputPlusMins inputPlusSecs buttons";
}
@include phonePortrait(){
.c-tc-input-popup__input-grid {
grid-template-columns: repeat(3, max-content) 1fr;
grid-template-areas:
"labelMinusHrs labelMinusMins labelMinusSecs ."
"inputMinusHrs inputMinusMins inputMinusSecs ."
"labelPlusHrs labelPlusMins labelPlusSecs ."
"inputPlusHrs inputPlusMins inputPlusSecs buttons";
padding: 2px;
overflow: hidden;
}
}
}
&__input-grid {
display: grid;
grid-column-gap: 3px;
grid-row-gap: $interiorMargin;
grid-column-gap: $interiorMarginSm;
align-items: start;
}
}
@include phonePortrait(){ // Additional styling for mobile portrait.
.c-tc-input-popup{
width: 100%;
&__options{
> * {
overflow: hidden;
[class*= 'ctrl-wrapper']{
[class*='--menu'] {
width: 100%;
[class*='__label'] {
@include ellipsize();
}
}
}
}
}
}
.pr-time-input-end-time, .pr-time-input-start-time{
> * {
margin-right: $interiorMargin;
}
}
.pr-time-input--buttons{
justify-content: flex-end;
}
.pr-time-input__start-end-sep{
margin: auto;
}
.pr-time-input__start-end-sep{
display: none;
}
.pr-time-input-start-date, .pr-time-input-end-date{
width: max-content;
}
}

View File

@@ -27,6 +27,7 @@
isFixed ? 'is-fixed-mode' : independentTCEnabled ? 'is-realtime-mode' : 'is-fixed-mode',
{ 'is-expanded': independentTCEnabled }
]"
aria-label="Independent Time Conductor Panel"
>
<ToggleSwitch
id="independentTCToggle"

View File

@@ -37,6 +37,18 @@
</div>
<div class="c-tli__graphic">
<svg viewBox="0 0 100 100">
<g aria-label="Activity in progress" class="c-tli__graphic__pie">
<circle class="c-svg-progress__bg" r="50" cx="50" cy="50"></circle>
<path ref="progressElement" class="c-svg-progress__progress"></path>
<circle
class="c-svg-progress__ticks"
r="40"
cx="50"
cy="50"
stroke-dasharray="3 7.472"
></circle>
<rect class="c-svg-progress__sweep-hand" x="48" y="18" width="4" height="27"></rect>
</g>
<path
aria-label="Activity complete"
class="c-tli__graphic__check"
@@ -80,6 +92,7 @@ import _ from 'lodash';
import { TIME_CONTEXT_EVENTS } from '../../api/time/constants.js';
import { CURRENT_CSS_SUFFIX, FUTURE_CSS_SUFFIX, PAST_CSS_SUFFIX } from './constants.js';
import { updateProgress } from './svg-progress.js';
const ITEM_COLORS = {
[CURRENT_CSS_SUFFIX]: '#ffcc00',
@@ -212,6 +225,7 @@ export default {
},
followTimeContext() {
this.timeContext.on(TIME_CONTEXT_EVENTS.tick, this.updateTimestamp);
this.updateTimestamp(this.timeContext.now());
},
stopFollowingTimeContext() {
if (this.timeContext) {
@@ -220,6 +234,10 @@ export default {
},
updateTimestamp(time) {
this.timestamp = time;
const progressElement = this.$refs.progressElement;
if (this.isInProgress && progressElement) {
updateProgress(this.start, this.end, this.timestamp, progressElement);
}
this.formatItemLabel();
},
formatItemLabel() {

View File

@@ -63,6 +63,7 @@
<list-item
v-for="item in sortedItems"
:key="item.key"
:class="{ '--is-in-progress': persistedActivityStates[item.id] === 'in-progress' }"
:item="item"
:item-properties="itemProperties"
@click.stop="setSelectionForActivity(item, $event.currentTarget)"

View File

@@ -0,0 +1,49 @@
const PI = Math.PI; // Use the built-in constant directly
const DEGREES_TO_RADIANS = PI / 180; // Calculate the conversion factor
import { arc } from 'd3-shape';
const SVG_VB_SIZE = 100;
const UPDATE_RATE_MS = 1000; // 1 Hz
function progToDegrees(progVal) {
return (progVal / 100) * 360;
}
function renderProgress(progressPercent, element) {
let startAngleInDegrees = 0;
let endAngleInDegrees = progToDegrees(progressPercent);
// Convert angles to radians for calculations
const startAngleInRadians = startAngleInDegrees * DEGREES_TO_RADIANS;
const endAngleInRadians = endAngleInDegrees * DEGREES_TO_RADIANS;
// d3's arc API does the work for us
const progressArc = arc();
progressArc.innerRadius(0);
progressArc.outerRadius(SVG_VB_SIZE / 2);
progressArc.startAngle(startAngleInRadians);
progressArc.endAngle(endAngleInRadians);
element.setAttribute('d', progressArc());
}
export function updateProgress(start, end, timestamp, element) {
const duration = end - start;
const update_per_cycle = 100 / (duration / UPDATE_RATE_MS);
let progressPercent = 0;
if (timestamp > start) {
// Now is after activity start datetime
if (timestamp > end) {
progressPercent = 100;
} else {
progressPercent = (1 - (end - timestamp) / duration) * 100;
}
}
if (progressPercent < 100 && progressPercent > 0) {
// If the remaining percent is less than update_per_cycle, round up to 100%.
// Otherwise, increment by update_per_cycle.
progressPercent =
100 - progressPercent < update_per_cycle ? 100 : (progressPercent += update_per_cycle);
}
renderProgress(progressPercent, element);
}

View File

@@ -29,7 +29,15 @@
}
.c-list-item {
/* Time Lists */
/* Compact Time Lists; is a <tr> element */
@mixin sSelected($bgColor, $fgColor) {
&[s-selected] {
background: $bgColor !important;
border: 1px solid $colorSelectedFg !important;
color: $fgColor !important;
}
}
td {
$p: $interiorMarginSm;
@@ -37,19 +45,26 @@
padding-bottom: $p;
}
&.--is-past {
@include sSelected(transparent, $colorPastFgEm);
}
&.--is-current {
@include sSelected($colorCurrentBg, $colorCurrentFgEm);
background-color: $colorCurrentBg;
border-top: 1px solid $colorCurrentBorder !important;
color: $colorCurrentFgEm;
}
&.--is-future {
@include sSelected($colorFutureBg, $colorFutureFgEm);
background-color: $colorFutureBg;
border-top-color: $colorFutureBorder !important;
color: $colorFutureFgEm;
}
&.--is-in-progress {
@include sSelected($colorInProgressBg, $colorInProgressFgEm);
background-color: $colorInProgressBg;
}
@@ -105,9 +120,10 @@
grid-column-gap: $interiorMargin;
&[s-selected] {
background: $colorSelectedBg !important;
box-shadow: inset rgba($colorSelectedFg, 0.1) 0 0 0 1px;
box-shadow: inset rgba($colorSelectedFg, 0.8) 0 0 0 1px;
color: $colorSelectedFg !important;
@include styleTliEm($colorSelectedFg);
}
@include styleTliEm($baseFgEm);
@@ -308,6 +324,7 @@
&__progress {
fill: $colorInProgressFgEm;
transform: translateX(50%) translateY(50%);
}
&__sweep-hand {

View File

@@ -68,6 +68,18 @@ div {
flex: 1 1 auto;
}
.visually-hidden {
// Provides a way to add accessible text to elements
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
/******************************************************** BROWSER ELEMENTS */
body.desktop {
::-webkit-scrollbar {
@@ -361,7 +373,7 @@ body.desktop .has-local-controls {
}
}
[aria-disabled = 'true'],
[aria-disabled='true'],
*[disabled],
.disabled {
opacity: $controlDisabledOpacity;
@@ -397,7 +409,7 @@ body.desktop .has-local-controls {
position: absolute;
top: 0;
bottom: 0;
content: "";
content: '';
right: 0;
width: $fadeTruncateW * 1.5;
z-index: 2;

View File

@@ -24,10 +24,17 @@
* Module defining url handling.
*/
function getUrlParams(openmct, customUrlParams = {}) {
/**
* Convert the current URL parameters to an array of strings.
* @param {import('../../openmct').OpenMCT} openmct
* @returns {Array<string>} newTabParams
*/
export function paramsToArray(openmct, customUrlParams = {}) {
let urlParams = openmct.router.getParams();
Object.entries(customUrlParams).forEach((urlParam) => {
const [key, value] = urlParam;
// Merge the custom URL parameters with the current URL parameters.
Object.entries(customUrlParams).forEach((param) => {
const [key, value] = param;
urlParams[key] = value;
});
@@ -39,21 +46,7 @@ function getUrlParams(openmct, customUrlParams = {}) {
delete urlParams['tc.endBound'];
}
return urlParams;
}
export function paramsToArray(openmct, customUrlParams = {}) {
// parse urlParams from an object to an array.
let urlParams = getUrlParams(openmct, customUrlParams);
let newTabParams = [];
for (let key in urlParams) {
if ({}.hasOwnProperty.call(urlParams, key)) {
let param = `${key}=${urlParams[key]}`;
newTabParams.push(param);
}
}
return newTabParams;
return Object.entries(urlParams).map(([key, value]) => `${key}=${value}`);
}
export function identifierToString(openmct, objectPath) {
@@ -66,7 +59,7 @@ export function identifierToString(openmct, objectPath) {
* @param {any} customUrlParams
* @returns {string} url
*/
export default function objectPathToUrl(openmct, objectPath, customUrlParams = {}) {
export function objectPathToUrl(openmct, objectPath, customUrlParams = {}) {
let url = identifierToString(openmct, objectPath);
let urlParams = paramsToArray(openmct, customUrlParams);

View File

@@ -1,5 +1,5 @@
import { createOpenMct, resetApplicationState } from '../utils/testing.js';
import { default as objectPathToUrl, identifierToString, paramsToArray } from './url.js';
import { identifierToString, objectPathToUrl, paramsToArray } from './url.js';
describe('the url tool', function () {
let openmct;

View File

@@ -32,6 +32,7 @@
'has-complex-content': complexContent
}
]"
:aria-label="ariaLabel"
>
<div class="c-so-view__header">
<div class="c-object-label" :class="[statusClass]">
@@ -45,6 +46,8 @@
<div
ref="objectName"
class="c-object-label__name"
aria-label="object name"
:title="domainObject && domainObject.name"
@mouseover.ctrl="showToolTip"
@mouseleave="hideToolTip"
>
@@ -58,6 +61,7 @@
'c-so-view__frame-controls--no-frame': !hasFrame,
'has-complex-content': complexContent
}"
:aria-label="`${ariaLabel} Controls`"
>
<div v-if="supportsIndependentTime" class="c-conductor-holder--compact">
<independent-time-conductor :domain-object="domainObject" :object-path="objectPath" />
@@ -163,6 +167,9 @@ export default {
};
},
computed: {
ariaLabel() {
return `${this.domainObject.name} Frame`;
},
statusClass() {
return this.status ? `is-status--${this.status}` : '';
}

View File

@@ -105,7 +105,9 @@ export default {
return this.status ? `is-status--${this.status}` : '';
},
ariaLabel() {
return `${this.isEditing ? 'Preview' : 'Navigate to'} ${this.domainObject.name} ${this.domainObject.type} Object`;
return `${this.isEditing ? 'Preview' : 'Navigate to'} ${this.domainObject.name} ${
this.domainObject.type
} Object`;
}
},
mounted() {

View File

@@ -29,12 +29,14 @@
import { axisTop } from 'd3-axis';
import { scaleLinear, scaleUtc } from 'd3-scale';
import { select } from 'd3-selection';
import { onMounted, ref } from 'vue';
import utcMultiTimeFormat from '@/plugins/timeConductor/utcMultiTimeFormat';
import { useResizeObserver } from '../composables/resize';
//TODO: UI direction needed for the following property values
const PADDING = 1;
const RESIZE_POLL_INTERVAL = 200;
const PIXELS_PER_TICK = 100;
const PIXELS_PER_TICK_WIDE = 200;
//This offset needs to be re-considered
@@ -73,6 +75,16 @@ export default {
}
}
},
setup() {
const axisHolder = ref(null);
const { size, startObserving } = useResizeObserver();
onMounted(() => {
startObserving(axisHolder.value);
});
return {
containerSize: size
};
},
watch: {
bounds(newBounds) {
this.drawAxis(newBounds, this.timeSystem);
@@ -82,6 +94,9 @@ export default {
},
contentHeight() {
this.updateNowMarker();
},
containerSize() {
this.resize();
}
},
mounted() {
@@ -100,7 +115,7 @@ export default {
this.setDimensions();
this.drawAxis(this.bounds, this.timeSystem);
this.resizeTimer = setInterval(this.resize, RESIZE_POLL_INTERVAL);
this.resize();
},
unmounted() {
clearInterval(this.resizeTimer);

View File

@@ -145,7 +145,11 @@
:show-edit-view="true"
@change-action-collection="setActionCollection"
/>
<component :is="conductorComponent" class="l-shell__time-conductor" />
<component
:is="conductorComponent"
class="l-shell__time-conductor"
aria-label="Global Time Conductor"
/>
</pane>
<pane
class="l-shell__pane-inspector l-pane--holds-multipane"

View File

@@ -24,9 +24,10 @@
<button
class="c-create-button c-button--menu c-button--major icon-plus"
:aria-disabled="isEditing"
aria-labelledby="create-button-label"
@click.prevent.stop="showCreateMenu"
>
<span class="c-button__label">Create</span>
<span id="create-button-label" class="c-button__label">Create</span>
</button>
</div>
</template>

View File

@@ -27,7 +27,8 @@
<button
class="c-icon-button c-button--menu"
:class="currentView.cssClass"
title="Change the current view"
:title="viewSwitcherLabel"
:aria-label="viewSwitcherLabel"
@click.prevent.stop="showMenu"
>
<span class="c-icon-button__label">
@@ -51,6 +52,11 @@ export default {
}
},
emits: ['set-view'],
computed: {
viewSwitcherLabel() {
return 'Open the View Switcher Menu';
}
},
methods: {
setView(view) {
this.$emit('set-view', view);

View File

@@ -55,7 +55,7 @@
<script>
import tooltipHelpers from '../../../api/tooltips/tooltipMixins.js';
import identifierToString from '../../../tools/url.js';
import { objectPathToUrl } from '../../../tools/url.js';
import ObjectPath from '../../components/ObjectPath.vue';
import PreviewAction from '../../preview/PreviewAction.js';
@@ -101,7 +101,7 @@ export default {
event.preventDefault();
this.preview(objectPath);
} else {
let resultUrl = identifierToString(this.openmct, objectPath);
let resultUrl = objectPathToUrl(this.openmct, objectPath);
// Remove the vestigial 'ROOT' identifier from url if it exists
if (resultUrl.includes('/ROOT')) {

View File

@@ -61,7 +61,8 @@ export default {
let sortedActions = this.openmct.actions._groupAndSortActions(actions);
const menuOptions = {
onDestroy: this.onContextMenuDestroyed
onDestroy: this.onContextMenuDestroyed,
label: this.objectPath[0].name
};
const menuItems = this.openmct.menus.actionsToMenuItems(

View File

@@ -1,4 +1,4 @@
import objectPathToUrl from '../../tools/url.js';
import { objectPathToUrl } from '../../tools/url.js';
export default {
inject: ['openmct'],