Compare commits

...

20 Commits

Author SHA1 Message Date
b59ed09274 changing how we pass in the options, so you can do one at a time, as well as return possible roles after login promise has resolved 2024-05-13 15:36:07 -07:00
810d580b18 [Telemetry Tables] Make sure tables auto scroll correctly on first load (#7720)
* run scroll method to scroll to top after initial load of historical data

* clarifying comment

* added e2e test to make sure tables auto scroll on mount

* adding descriptive comments

* adding in ascending check as well

* added new appAction for navigating to/in realtime, using it in table scroll test

* lint

---------

Co-authored-by: David Tsay <3614296+davetsay@users.noreply.github.com>
2024-05-09 11:53:11 -07:00
977792fae8 this is 2024. * observers no more. (#7715)
* this is 2024. `*` observers no more.

* add edit and save domain object helper functions

* add aria-labels and fix e2e tests to use new labels

* generate and save in local storage a condition set with telemetry and condition

* rename const

* move creation code out of generateLocalStorage since it is immutable

* remove function abstractions

* remove @localStorage test label

* Update e2e/tests/functional/plugins/conditionSet/conditionSet.e2e.spec.js

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

* Update e2e/tests/functional/plugins/conditionSet/conditionSet.e2e.spec.js

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

* Update e2e/tests/functional/plugins/conditionSet/conditionSet.e2e.spec.js

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

* Update e2e/tests/functional/plugins/conditionSet/conditionSet.e2e.spec.js

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

* remove unneccesary aria text

* remove unneccessary aria text

* use recommended playwright locators

* lint fix

* remove unneccesary steps now that child created directly in parent

---------

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2024-05-09 11:19:55 -07:00
a69e300f1c chore: bump playwright to v1.44.0 (#7716) 2024-05-09 08:20:31 -07:00
17bc6cb722 chore(webpack): destruct version from pkgJson (#7713) 2024-05-07 08:52:41 -07:00
b3d3465734 [Darkmatter] Create new darkmatter theme (#7682)
* initial theme plugin setup, changes to layout frames

* update visual tests

* Changes to gauge, layout borders, and background

* Make background image a DIY theme variable. Fixes made to gauges. Deleted custom font.

* More changes to overall background colors. Added glass layer effect to menus

* changes to menu

* Fix to make theme easy to run

* Fix tab colors and add glass background to menus

* make highlightd corners longer

* Initial changes to font styles

* Add temporary numeric font style. Test numeric font in gauges.

* Initial changes to alphanumerics in layouts

* Updated variables

* update plugin.js file

* Fix highlighted corners on frames such that it uses outermost frame

* renaming theme plugin and rename branch

* fix button colors to be more readable

* change background image

* Fix bad merges from other theme files. Fix gauge and alphanumerics such that they dont have darkmatter borders

* more fixes

* Fix where mixin is used such that when an object's frame is hidden, highlgihts disappear

* remove blur from meter gauges

* Add comment about this theme being in beta mode

* Delete draft .scss file that is no longer needed

* Fix major accessibility issues

* Fix PR review comments

*  fix: Correct import file name for DarkMatter theme.

* Fix other theme code that was failing e2e tests

* Revert index.html

* Fix linting error

* Fix for failing percy test regarding padding

* Fix for failing percy test regarding padding part 2

* Fix for failing percy test regarding padding part 3

* Remove mixin that may be causing percy issue

* Another fix to resolve percy issue

* Add back some code that was deleted during debugging, and create new variables for the object padding

* Fix gradient clipping in inspector

* Restructure all constants-.scss files

* Change bg image to be square and NASA official picture

* Final fixes to darkmatter variable layouts

* Address PR comments

* Change darkmatter to darkmatterTheme

---------

Co-authored-by: John Hill <john.c.hill@nasa.gov>
Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
2024-04-25 16:06:07 -07:00
fb0d74e87f chore(deps-dev): bump vue from 3.4.19 to 3.4.24 (#7702)
Bumps [vue](https://github.com/vuejs/core) from 3.4.19 to 3.4.24.
- [Release notes](https://github.com/vuejs/core/releases)
- [Changelog](https://github.com/vuejs/core/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vuejs/core/compare/v3.4.19...v3.4.24)

---
updated-dependencies:
- dependency-name: vue
  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-04-22 13:19:00 -07:00
a961d7e3bf Fix nested Flexible Layout direction problem (#7637)
* Closes #7635
- More specific approach to CSS class application for column vs. row layouts.
- Added layout direction CSS classing to `c-fl-container__frames-holder`.
- Switched toolbar icon and titling for better parity with
'toggle' approach used elsewhere.
- Cleaned up duped property def in mixin.

* Addressing PR change requests
- Updated e2e test.
- New computed properties for layout direction.
- CSS code cleanup.

* fix selector in test

* fix more bad selectors

* fix changed title

---------

Co-authored-by: David Tsay <david.e.tsay@nasa.gov>
2024-04-18 23:38:11 +00:00
5a06b51c5a refactor: remove the final .bounds() call (#7695)
 refactor: Use getBounds method instead of bounds in IndependentTimeContext.
2024-04-17 16:19:21 +00:00
ef8b353d01 Improve performance of JSON import and add progress bar (#7654)
* add progress bar

* improves performance a lot

* fix test

* remove old function

* fix legit bug from test - well done tests

* add constant, better comments

---------

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2024-04-16 21:23:31 +00:00
6c5b925454 chore: remove all usage of deprecated Time API methods (#7688)
* chore: remove all usage of deprecated Time API methods

* test: update unit test

* docs: Fix spacing and add clarity to TimeConductorBounds definition.

* test: add unit test coverage for new time api methods
2024-04-16 21:12:09 +00:00
e91aba2e37 Handle paste events for images and text properly (#7679)
* enable eval source maps for debugging

* split image and text paste handling
better event handling

* change back source maps

* image takes precedence over text

* break up notebook entry functions for re-use

* create hotkeys utils
add clipboard functions

* add notebook paste test

* add test for pasting to selected but not editing entry

* link tests to issue

* jsdoc addition

* jsdocs

* no need to import then export

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

* fix changed path

---------

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2024-04-16 13:54:40 -07:00
b18aa48141 Revert "Handle the case where the pasted data is not an image" (#7668)
Revert "Handle the case where the pasted data is not an image (#7628)"

This reverts commit d33da65dae.
2024-04-04 15:03:45 -07:00
d33da65dae Handle the case where the pasted data is not an image (#7628)
* Handle the case where the pasted data is not an image or it is image AND text
* Change method name for paste handling

---------

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2024-04-04 21:30:12 +00:00
e3adeb6a75 Do not add unused created attribute to metadata of couch documents on create (#7656)
this isn't used anywhere

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2024-04-04 13:33:36 -07:00
de3dad02b5 [Telemetry Tables] Don't mutate configuration if object is not able to be persisted (#7626)
* source maps

* do not persist if obj is not persistable

* nope

* prevent mutation of configuration

* static roots are read only by nature

* helps to use functions correctly

* update persistModeChange logic

* remove debug

* remove unnecessary change
2024-04-04 00:38:59 +00:00
311ad0b87a fix(e2e): specify .nyc_output path as custom config setting (#7658)
* fix: specify .nyc_output path as custom config setting

* fix: coverage for mobile suite

* fix: pathing in playwright configs
2024-04-01 18:29:47 +00:00
f98eb31956 fix: move file to correct folder (#7652) 2024-03-28 17:32:46 -07:00
1671a585fb Mct7636 (#7645)
* setting order for sort to descending if in performance mode and no sort set

---------

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2024-03-28 17:32:03 -07:00
a3fb84ad43 chore: remove type: module, create openmct-e2e subpackage (#7590)
* fix: remove mystery webpack code

* fix: remove type:module and specify exports

- we aren't a module... yet

* fix: rename webpack*.js to webpack*.mjs so we can use import/export. fix imports

* fix: exports format

* fix: woops, need to add `start` script back

* chore: split e2e into its own module

* fix: use normal Painterro import

* fix: update e2e pathing

* fix: copy over helper functions

* chore: specify `cwd` for playwright configs so that openmct npm commands work as intended in any environment

* chore: add pretest script to e2e package.json

* chore: don't package e2e

* refactor: tidy up webpack common config

* chore: compile types to a single file

* chore: fix visual test npm scripts

* chore: fix import pathing

* chore: define package exports, move test specific dependencies to the subpackage

* chore: export test framework from openmct-e2e

* chore: export baseFixtures also

* chore: let `openmct` and `openmct-e2e` share `node_modules/`

* chore: use `--workspace`, remove pretest script

* Revert "fix: remove mystery webpack code"

This reverts commit eb14d52569ffa27ab1a090b883694f4707b59cd0.

* chore: update package-lock

* chore: add `.npmignore`

* fix: *js -> mjs
2024-03-28 14:49:00 -07:00
121 changed files with 3701 additions and 654 deletions

View File

@ -5,7 +5,7 @@ orbs:
executors:
pw-focal-development:
docker:
- image: mcr.microsoft.com/playwright:v1.42.1-focal
- image: mcr.microsoft.com/playwright:v1.44.0-focal
environment:
NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed
PERCY_POSTINSTALL_BROWSER: 'true' # Needed to store the percy browser in cache deps
@ -159,7 +159,7 @@ jobs:
steps:
- build_and_install:
node-version: lts/hydrogen
- run: npx playwright@1.42.1 install #Necessary for bare ubuntu machine
- run: npx playwright@1.44.0 install #Necessary for bare ubuntu machine
- run: |
export $(cat src/plugins/persistence/couch/.env.ci | xargs)
docker-compose -f src/plugins/persistence/couch/couchdb-compose.yaml up --detach

View File

@ -497,7 +497,8 @@
"checksnapshots",
"specced",
"composables",
"countup"
"countup",
"darkmatter"
],
"dictionaries": ["npm", "softwareTerms", "node", "html", "css", "bash", "en_US", "en-gb", "misc"],
"ignorePaths": [

View File

@ -37,7 +37,7 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- run: npx playwright@1.42.1 install
- run: npx playwright@1.44.0 install
- name: Start CouchDB Docker Container and Init with Setup Scripts
run: |

View File

@ -30,7 +30,7 @@ jobs:
restore-keys: |
${{ runner.os }}-node-
- run: npx playwright@1.42.1 install
- run: npx playwright@1.44.0 install
- run: npm ci --no-audit --progress=false
- name: Run E2E Tests (Repeated 10 Times)

View File

@ -28,7 +28,7 @@ jobs:
restore-keys: |
${{ runner.os }}-node-
- run: npx playwright@1.42.1 install
- run: npx playwright@1.44.0 install
- run: npm ci --no-audit --progress=false
- run: npm run test:perf:localhost
- run: npm run test:perf:contract

View File

@ -33,7 +33,7 @@ jobs:
restore-keys: |
${{ runner.os }}-node-
- run: npx playwright@1.42.1 install
- run: npx playwright@1.44.0 install
- run: npx playwright install chrome-beta
- run: npm ci --no-audit --progress=false
- run: npm run test:e2e:full -- --max-failures=40

View File

@ -22,9 +22,3 @@
!index.html
!openmct.js
!SECURITY.md
# Add e2e tests to npm package
!/e2e/**/*
# ... except our test-data folder files.
/e2e/test-data/*.json

View File

@ -1,8 +1,8 @@
/*
This is the OpenMCT common webpack file. It is imported by the other three webpack configurations:
- webpack.prod.js - the production configuration for OpenMCT (default)
- webpack.dev.js - the development configuration for OpenMCT
- webpack.coverage.js - imports webpack.dev.js and adds code coverage
- webpack.prod.mjs - the production configuration for OpenMCT (default)
- webpack.dev.mjs - the development configuration for OpenMCT
- webpack.coverage.mjs - imports webpack.dev.js and adds code coverage
There are separate npm scripts to use these configurations, though simply running `npm install`
will use the default production configuration.
*/
@ -15,10 +15,11 @@ import CopyWebpackPlugin from 'copy-webpack-plugin';
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import { VueLoaderPlugin } from 'vue-loader';
import webpack from 'webpack';
import { merge } from 'webpack-merge';
let gitRevision = 'error-retrieving-revision';
let gitBranch = 'error-retrieving-branch';
const packageDefinition = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url)));
const { version } = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url)));
try {
gitRevision = execSync('git rev-parse HEAD').toString().trim();
@ -48,15 +49,18 @@ const config = {
couchDBChangesFeed: './src/plugins/persistence/couch/CouchChangesFeed.js',
inMemorySearchWorker: './src/api/objects/InMemorySearchWorker.js',
espressoTheme: './src/plugins/themes/espresso-theme.scss',
snowTheme: './src/plugins/themes/snow-theme.scss'
snowTheme: './src/plugins/themes/snow-theme.scss',
darkmatterTheme: './src/plugins/themes/darkmatter-theme.scss'
},
output: {
globalObject: 'this',
filename: '[name].js',
path: path.resolve(projectRootDir, 'dist'),
library: 'openmct',
libraryExport: 'default',
libraryTarget: 'umd',
library: {
name: 'openmct',
type: 'umd',
export: 'default'
},
publicPath: '',
hashFunction: 'xxhash64',
clean: true
@ -81,7 +85,7 @@ const config = {
},
plugins: [
new webpack.DefinePlugin({
__OPENMCT_VERSION__: `'${packageDefinition.version}'`,
__OPENMCT_VERSION__: `'${version}'`,
__OPENMCT_BUILD_DATE__: `'${new Date()}'`,
__OPENMCT_REVISION__: `'${gitRevision}'`,
__OPENMCT_BUILD_BRANCH__: `'${gitBranch}'`,

View File

@ -1,10 +1,10 @@
/*
This file extends the webpack.dev.js config to add babel istanbul coverage.
This file extends the webpack.dev.mjs config to add babel istanbul coverage.
OpenMCT Continuous Integration servers use this configuration to add code coverage
information to pull requests.
*/
import config from './webpack.dev.js';
import config from './webpack.dev.mjs';
config.devtool = 'source-map';
config.devServer.hot = false;
@ -16,7 +16,6 @@ config.module.rules.push({
loader: 'babel-loader',
options: {
retainLines: true,
// eslint-disable-next-line no-undef
plugins: [
[
'babel-plugin-istanbul',

View File

@ -1,14 +1,15 @@
/*
This configuration should be used for development purposes. It contains full source map, a
devServer (which be invoked using by `npm start`), and a non-minified Vue.js distribution.
If OpenMCT is to be used for a production server, use webpack.prod.js instead.
If OpenMCT is to be used for a production server, use webpack.prod.mjs instead.
*/
import { fileURLToPath } from 'node:url';
import path from 'path';
import webpack from 'webpack';
import { merge } from 'webpack-merge';
import { fileURLToPath } from 'node:url';
import common from './webpack.common.js';
import common from './webpack.common.mjs';
export default merge(common, {
mode: 'development',

View File

@ -6,7 +6,7 @@ It is the default webpack configuration.
import webpack from 'webpack';
import { merge } from 'webpack-merge';
import common from './webpack.common.js';
import common from './webpack.common.mjs';
export default merge(common, {
mode: 'production',

View File

@ -63,7 +63,7 @@ Once the file is generated, it can be published to codecov with
### e2e
The e2e line coverage is a bit more complex than the karma implementation. This is the general sequence of events:
1. Each e2e suite will start webpack with the ```npm run start:coverage``` command with config `webpack.coverage.js` and the `babel-plugin-istanbul` plugin to generate code coverage during e2e test execution using our custom [baseFixture](./baseFixtures.js).
1. Each e2e suite will start webpack with the ```npm run start:coverage``` command with config `webpack.coverage.mjs` and the `babel-plugin-istanbul` plugin to generate code coverage during e2e test execution using our custom [baseFixture](./baseFixtures.js).
1. During testcase execution, each e2e shard will generate its piece of the larger coverage suite. **This coverage file is not merged**. The raw coverage file is stored in a `.nyc_report` directory.
1. [nyc](https://github.com/istanbuljs/nyc) converts this directory into a `lcov` file with the following command `npm run cov:e2e:report`
1. Most of the tests are run in the '@stable' configuration and focus on chrome/ubuntu at a single resolution. This coverage is published to codecov with `npm run cov:e2e:stable:publish`.

7
e2e/.npmignore Normal file
View File

@ -0,0 +1,7 @@
*
!appActions.js
!baseFixtures.js
!pluginFixtures.js
!avpFixtures.js
!index.js
!*.md

View File

@ -167,9 +167,9 @@ When an a11y test fails, the result must be interpreted in the html test report
The open source performance tests function in three ways which match their naming and folder structure:
`./e2e/tests/performance` - The tests at the root of this folder path detect functional changes which are mostly apparent with large performance regressions like [this](https://github.com/nasa/openmct/issues/6879). These tests run against openmct webpack in `production-mode` with the `npm run test:perf:localhost` script.
`./e2e/tests/performance/contract/` - These tests serve as [contracts](https://martinfowler.com/bliki/ContractTest.html) for the locator logic, functionality, and assumptions will work in our downstream, closed source test suites. These tests run against openmct webpack in `dev-mode` with the `npm run test:perf:contract` script.
`./e2e/tests/performance/memory/` - These tests execute memory leak detection checks in various ways. This is expected to evolve as we move to the `memlab` project. These tests run against openmct webpack in `production-mode` with the `npm run test:perf:memory` script.
`tests/performance` - The tests at the root of this folder path detect functional changes which are mostly apparent with large performance regressions like [this](https://github.com/nasa/openmct/issues/6879). These tests run against openmct webpack in `production-mode` with the `npm run test:perf:localhost` script.
`tests/performance/contract/` - These tests serve as [contracts](https://martinfowler.com/bliki/ContractTest.html) for the locator logic, functionality, and assumptions will work in our downstream, closed source test suites. These tests run against openmct webpack in `dev-mode` with the `npm run test:perf:contract` script.
`tests/performance/memory/` - These tests execute memory leak detection checks in various ways. This is expected to evolve as we move to the `memlab` project. These tests run against openmct webpack in `production-mode` with the `npm run test:perf:memory` script.
These tests are expected to become blocking and gating with assertions as we extend the capabilities of Playwright.

View File

@ -275,6 +275,17 @@ async function navigateToObjectWithFixedTimeBounds(page, url, start, end) {
);
}
/**
* Navigates directly to a given object url, in real-time mode.
* @param {import('@playwright/test').Page} page
* @param {string} url The url to the domainObject
*/
async function navigateToObjectWithRealTime(page, url, start = '1800000', end = '30000') {
await page.goto(
`${url}?tc.mode=local&tc.startDelta=${start}&tc.endDelta=${end}&tc.timeSystem=utc`
);
}
/**
* Open the given `domainObject`'s context menu from the object tree.
* Expands the path to the object and scrolls to it if necessary.
@ -656,6 +667,7 @@ export {
getFocusedObjectUuid,
getHashUrlToDomainObject,
navigateToObjectWithFixedTimeBounds,
navigateToObjectWithRealTime,
openObjectTreeContextMenu,
renameObjectFromContextMenu,
setEndOffset,

View File

@ -60,14 +60,16 @@ function waitForAnimations(locator) {
);
}
/**
* This is part of our codecoverage shim.
const istanbulCLIOutput = fileURLToPath(new URL('.nyc_output', import.meta.url));
const extendedTest = test.extend({
/**
* Path to output raw coverage files. Can be overridden in Playwright config file.
* @see {@link https://github.com/mxschmitt/playwright-test-coverage Github Example Project}
* @constant {string}
*/
const istanbulCLIOutput = path.join(process.cwd(), '.nyc_output');
const extendedTest = test.extend({
coveragePath: [istanbulCLIOutput, { option: true }],
/**
* This allows the test to manipulate the browser clock. This is useful for Visual and Snapshot tests which need
* the Time Indicator Clock to be in a specific state.
@ -148,17 +150,17 @@ const extendedTest = test.extend({
* Extends the base context class to add codecoverage shim.
* @see {@link https://github.com/mxschmitt/playwright-test-coverage Github Project}
*/
context: async ({ context }, use) => {
context: async ({ context, coveragePath }, use) => {
await context.addInitScript(() =>
window.addEventListener('beforeunload', () =>
window.collectIstanbulCoverage(JSON.stringify(window.__coverage__))
)
);
await fs.promises.mkdir(istanbulCLIOutput, { recursive: true });
await fs.promises.mkdir(coveragePath, { recursive: true });
await context.exposeFunction('collectIstanbulCoverage', (coverageJSON) => {
if (coverageJSON) {
fs.writeFileSync(
path.join(istanbulCLIOutput, `playwright_coverage_${uuid()}.json`),
path.join(coveragePath, `playwright_coverage_${uuid()}.json`),
coverageJSON
);
}
@ -166,9 +168,9 @@ const extendedTest = test.extend({
await use(context);
for (const page of context.pages()) {
await page.evaluate(() =>
window.collectIstanbulCoverage(JSON.stringify(window.__coverage__))
);
await page.evaluate(() => {
window.collectIstanbulCoverage(JSON.stringify(window.__coverage__));
});
}
},
/**

View File

@ -0,0 +1,47 @@
/*****************************************************************************
* 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.
*****************************************************************************/
const isMac = process.platform === 'darwin';
const modifier = isMac ? 'Meta' : 'Control';
/**
* @param {import('@playwright/test').Page} page
*/
async function selectAll(page) {
await page.keyboard.press(`${modifier}+KeyA`);
}
/**
* @param {import('@playwright/test').Page} page
*/
async function copy(page) {
await page.keyboard.press(`${modifier}+KeyC`);
}
/**
* @param {import('@playwright/test').Page} page
*/
async function paste(page) {
await page.keyboard.press(`${modifier}+KeyV`);
}
export { copy, paste, selectAll };

View File

@ -0,0 +1,23 @@
/*****************************************************************************
* 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.
*****************************************************************************/
export * from './clipboard.js';

View File

@ -28,16 +28,28 @@ import { fileURLToPath } from 'url';
/**
* @param {import('@playwright/test').Page} page
* @param {string} text
*/
async function enterTextEntry(page, text) {
// Click the 'Add Notebook Entry' area
await page.locator(NOTEBOOK_DROP_AREA).click();
// enter text
await page.getByLabel('Notebook Entry Input').last().fill(text);
await addNotebookEntry(page);
await enterTextInLastEntry(page, text);
await commitEntry(page);
}
/**
* @param {import('@playwright/test').Page} page
*/
async function addNotebookEntry(page) {
await page.locator(NOTEBOOK_DROP_AREA).click();
}
/**
* @param {import('@playwright/test').Page} page
*/
async function enterTextInLastEntry(page, text) {
await page.getByLabel('Notebook Entry Input').last().fill(text);
}
/**
* @param {import('@playwright/test').Page} page
*/
@ -140,10 +152,13 @@ async function createNotebookEntryAndTags(page, iterations = 1) {
}
export {
addNotebookEntry,
commitEntry,
createNotebookAndEntry,
createNotebookEntryAndTags,
dragAndDropEmbed,
enterTextEntry,
enterTextInLastEntry,
lockPage,
startAndAddRestrictedNotebookObject
};

8
e2e/index.js Normal file
View File

@ -0,0 +1,8 @@
// Import everything from the specific fixture files
import * as appActions from './appActions.js';
import * as avpFixtures from './avpFixtures.js';
import * as baseFixtures from './baseFixtures.js';
import * as pluginFixtures from './pluginFixtures.js';
// Export these as named exports
export { appActions, avpFixtures, baseFixtures, pluginFixtures };

1449
e2e/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

27
e2e/package.json Normal file
View File

@ -0,0 +1,27 @@
{
"name": "openmct-e2e",
"version": "4.0.0-next",
"description": "The Open MCT e2e framework",
"type": "module",
"module": "index.js",
"exports": {
".": {
"import": "./index.js"
}
},
"scripts": {
"pretest:visual": "npm install",
"test": "npx playwright test",
"test:visual": "percy exec"
},
"devDependencies": {
"@types/sinonjs__fake-timers": "8.1.5",
"@percy/cli": "1.27.4",
"@percy/playwright": "1.0.4",
"@playwright/test": "1.44.0",
"@axe-core/playwright": "4.8.5",
"sinon": "17.0.0"
},
"author": "NASA Ames Research Center",
"license": "Apache-2.0"
}

View File

@ -3,6 +3,7 @@
// eslint-disable-next-line no-unused-vars
import { devices } from '@playwright/test';
import { fileURLToPath } from 'url';
const MAX_FAILURES = 5;
const NUM_WORKERS = 2;
@ -15,6 +16,7 @@ const config = {
timeout: 60 * 1000,
webServer: {
command: 'npm run start:coverage',
cwd: fileURLToPath(new URL('../', import.meta.url)), // Provide cwd for the root of the project
url: 'http://localhost:8080/#',
timeout: 200 * 1000,
reuseExistingServer: true //This was originally disabled to prevent differences in local debugging vs. CI. However, it significantly speeds up local debugging.
@ -27,7 +29,9 @@ const config = {
ignoreHTTPSErrors: true,
screenshot: 'only-on-failure',
trace: 'on-first-retry',
video: 'off'
video: 'off',
// @ts-ignore - custom configuration option for nyc codecoverage output path
coveragePath: fileURLToPath(new URL('../.nyc_output', import.meta.url))
},
projects: [
{

View File

@ -1,6 +1,6 @@
// playwright.config.js
// @ts-check
import { fileURLToPath } from 'url';
/** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = {
retries: 0,
@ -10,6 +10,7 @@ const config = {
timeout: 30 * 1000,
webServer: {
command: 'npm run start:coverage',
cwd: fileURLToPath(new URL('../', import.meta.url)), // Provide cwd for the root of the project
url: 'http://localhost:8080/#',
timeout: 120 * 1000,
reuseExistingServer: true

View File

@ -14,6 +14,7 @@ const config = {
timeout: 30 * 1000,
webServer: {
command: 'npm run start:coverage',
cwd: fileURLToPath(new URL('../', import.meta.url)), // Provide cwd for the root of the project
url: 'http://localhost:8080/#',
timeout: 200 * 1000,
reuseExistingServer: true //This was originally disabled to prevent differences in local debugging vs. CI. However, it significantly speeds up local debugging.
@ -27,7 +28,9 @@ const config = {
ignoreHTTPSErrors: true,
screenshot: 'only-on-failure',
trace: 'on-first-retry',
video: 'off'
video: 'off',
// @ts-ignore - custom configuration option for nyc codecoverage output path
coveragePath: fileURLToPath(new URL('../.nyc_output', import.meta.url))
},
projects: [
{

View File

@ -1,6 +1,6 @@
// playwright.config.js
// @ts-check
import { fileURLToPath } from 'url';
/** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = {
retries: 1, //Only for debugging purposes for trace: 'on-first-retry'
@ -10,6 +10,7 @@ const config = {
workers: 1, //Only run in serial with 1 worker
webServer: {
command: 'npm run start', //need development mode for performance.marks and others
cwd: fileURLToPath(new URL('../', import.meta.url)), // Provide cwd for the root of the project
url: 'http://localhost:8080/#',
timeout: 200 * 1000,
reuseExistingServer: false

View File

@ -1,6 +1,6 @@
// playwright.config.js
// @ts-check
import { fileURLToPath } from 'url';
/** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = {
retries: 0, //Only for debugging purposes for trace: 'on-first-retry'
@ -10,6 +10,7 @@ const config = {
workers: 1, //Only run in serial with 1 worker
webServer: {
command: 'npm run start:prod', //Production mode
cwd: fileURLToPath(new URL('../', import.meta.url)), // Provide cwd for the root of the project
url: 'http://localhost:8080/#',
timeout: 200 * 1000,
reuseExistingServer: false //Must be run with this option to prevent dev mode

View File

@ -1,6 +1,6 @@
// playwright.config.js
// @ts-check
import { fileURLToPath } from 'url';
/** @type {import('@playwright/test').PlaywrightTestConfig<{ theme: string }>} */
const config = {
retries: 0, // Visual tests should never retry due to snapshot comparison errors. Leaving as a shim
@ -10,6 +10,7 @@ const config = {
workers: 1, //Lower stress on Circle CI Agent for Visual tests https://github.com/percy/cli/discussions/1067
webServer: {
command: 'npm run start:coverage',
cwd: fileURLToPath(new URL('../', import.meta.url)), // Provide cwd for the root of the project
url: 'http://localhost:8080/#',
timeout: 200 * 1000,
reuseExistingServer: !process.env.CI
@ -35,6 +36,13 @@ const config = {
browserName: 'chromium',
theme: 'snow'
}
},
{
name: 'darkmatter-theme', //Runs the same visual tests but with darkmatter-theme
use: {
browserName: 'chromium',
theme: 'darkmatter-theme'
}
}
],
reporter: [

View File

@ -1,6 +1,5 @@
// playwright.config.js
// @ts-check
import { devices } from '@playwright/test';
import { fileURLToPath } from 'url';
@ -11,6 +10,7 @@ const config = {
timeout: 60 * 1000,
webServer: {
command: 'npm run start', //Start in dev mode for hot reloading
cwd: fileURLToPath(new URL('../', import.meta.url)), // Provide cwd for the root of the project
url: 'http://localhost:8080/#',
timeout: 200 * 1000,
reuseExistingServer: true //This was originally disabled to prevent differences in local debugging vs. CI. However, it significantly speeds up local debugging.

View File

@ -31,8 +31,8 @@ import { createDomainObjectWithDefaults } from '../../appActions.js';
import { expect, test } from '../../pluginFixtures.js';
const TEST_FOLDER = 'test folder';
const jsonFilePath = 'e2e/test-data/ExampleLayouts.json';
const imageFilePath = 'e2e/test-data/rick.jpg';
const jsonFilePath = 'test-data/ExampleLayouts.json';
const imageFilePath = 'test-data/rick.jpg';
test.describe('Form Validation Behavior', () => {
test('Required Field indicators appear if title is empty and can be corrected', async ({

View File

@ -21,7 +21,6 @@
*****************************************************************************/
import fs from 'fs';
import { getPreciseDuration } from '../../../../src/utils/duration.js';
import { createDomainObjectWithDefaults, createPlanFromJSON } from '../../../appActions.js';
import {
assertPlanActivities,
@ -132,3 +131,58 @@ test.describe('Gantt Chart', () => {
);
});
});
const ONE_SECOND = 1000;
const ONE_MINUTE = 60 * ONE_SECOND;
const ONE_HOUR = ONE_MINUTE * 60;
const ONE_DAY = ONE_HOUR * 24;
function normalizeAge(num) {
const hundredtized = num * 100;
const isWhole = hundredtized % 100 === 0;
return isWhole ? hundredtized / 100 : num;
}
function padLeadingZeros(num, numOfLeadingZeros) {
return num.toString().padStart(numOfLeadingZeros, '0');
}
function toDoubleDigits(num) {
return padLeadingZeros(num, 2);
}
function toTripleDigits(num) {
return padLeadingZeros(num, 3);
}
function getPreciseDuration(value, { excludeMilliSeconds, useDayFormat } = {}) {
let preciseDuration;
const ms = value || 0;
const duration = [
Math.floor(normalizeAge(ms / ONE_DAY)),
toDoubleDigits(Math.floor(normalizeAge((ms % ONE_DAY) / ONE_HOUR))),
toDoubleDigits(Math.floor(normalizeAge((ms % ONE_HOUR) / ONE_MINUTE))),
toDoubleDigits(Math.floor(normalizeAge((ms % ONE_MINUTE) / ONE_SECOND)))
];
if (!excludeMilliSeconds) {
duration.push(toTripleDigits(Math.floor(normalizeAge(ms % ONE_SECOND))));
}
if (useDayFormat) {
// Format days as XD
const days = duration.shift();
if (days > 0) {
preciseDuration = `${days}D ${duration.join(':')}`;
} else {
preciseDuration = duration.join(':');
}
} else {
const days = toDoubleDigits(duration.shift());
duration.unshift(days);
preciseDuration = duration.join(':');
}
return preciseDuration;
}

View File

@ -371,7 +371,7 @@ test.describe('Basic Condition Set Use', () => {
// Validate that the condition set is evaluating and outputting
// the correct value when the underlying telemetry subscription is active.
let outputValue = page.locator('[aria-label="Current Output Value"]');
let outputValue = page.getByLabel('Current Output Value');
await expect(outputValue).toHaveText('false');
await page.goto(exampleTelemetry.url);
@ -462,7 +462,7 @@ test.describe('Basic Condition Set Use', () => {
// Validate that the condition set is evaluating and outputting
// the correct value when the underlying telemetry subscription is active.
let outputValue = page.locator('[aria-label="Current Output Value"]');
let outputValue = page.getByLabel('Current Output Value');
await expect(outputValue).toHaveText('false');
await page.goto(exampleTelemetry.url);
@ -475,3 +475,81 @@ test.describe('Basic Condition Set Use', () => {
});
});
});
test.describe('Condition Set Composition', () => {
let conditionSet;
let exampleTelemetry;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
// Create Condition Set
conditionSet = await createDomainObjectWithDefaults(page, {
type: 'Condition Set'
});
// Create Telemetry Object as child to Condition Set
exampleTelemetry = await createExampleTelemetryObject(page, conditionSet.uuid);
// Edit Condition Set
await page.goto(conditionSet.url);
await page.getByRole('button', { name: 'Edit Object' }).click();
// Add Condition to Condition Set
await page.getByRole('button', { name: 'Add Condition' }).click();
// Enter Condition Output
await page.getByLabel('Condition Name Input').first().fill('Negative');
await page.getByLabel('Condition Output Type').first().selectOption({ value: 'string' });
await page.getByLabel('Condition Output String').first().fill('Negative');
// Condition Trigger default is okay so no change needed to form
// Enter Condition Criterion
await page.getByLabel('Criterion Telemetry Selection').first().selectOption({ value: 'all' });
await page.getByLabel('Criterion Metadata Selection').first().selectOption({ value: 'sin' });
await page
.locator('select[aria-label="Criterion Comparison Selection"]')
.first()
.selectOption({ value: 'lessThan' });
await page.getByLabel('Criterion Input').first().fill('0');
// Save the Condition Set
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
});
test('You can remove telemetry from a condition set with existing conditions', async ({
page
}) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7710'
});
await page.getByLabel('Expand My Items folder').click();
await page.getByLabel(`Expand ${conditionSet.name} conditionSet`).click();
await page
.getByLabel(`Navigate to ${exampleTelemetry.name}`, { exact: false })
.click({ button: 'right' });
await page
.getByLabel(`${exampleTelemetry.name} Context Menu`)
.getByRole('menuitem', { name: 'Remove' })
.click();
await page.getByRole('button', { name: 'OK', exact: true }).click();
await page
.getByLabel(`Navigate to ${conditionSet.name} conditionSet Object`, { exact: true })
.click();
await page.getByRole('button', { name: 'Edit Object' }).click();
await page.getByRole('tab', { name: 'Elements' }).click();
expect(
await page
.getByRole('tabpanel', { name: 'Inspector Views' })
.getByRole('listitem', { name: exampleTelemetry.name })
.count()
).toEqual(0);
});
});

View File

@ -78,8 +78,8 @@ test.describe('Flexible Layout', () => {
// Expand the 'My Items' folder in the left tree
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').first().click();
// Add the Sine Wave Generator and Clock to the Flexible Layout
await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first());
await clockTreeItem.dragTo(page.locator('.c-fl__container.is-empty'));
await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl-container.is-empty').first());
await clockTreeItem.dragTo(page.locator('.c-fl-container.is-empty'));
// Check that panes can be dragged while Flexible Layout is in Edit mode
let dragWrapper = page
.locator('.c-fl-container__frames-holder .c-fl-frame__drag-wrapper')
@ -105,8 +105,8 @@ test.describe('Flexible Layout', () => {
// Expand the 'My Items' folder in the left tree
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').first().click();
// Add the Sine Wave Generator and Clock to the Flexible Layout
await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first());
await clockTreeItem.dragTo(page.locator('.c-fl__container.is-empty'));
await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl-container.is-empty').first());
await clockTreeItem.dragTo(page.locator('.c-fl-container.is-empty'));
// Click on the first frame to select it
await page.locator('.c-fl-container__frame').first().click();
@ -122,7 +122,7 @@ test.describe('Flexible Layout', () => {
expect(await page.locator('.c-fl--rows').count()).toEqual(0);
// Change the layout to rows orientation
await page.getByTitle('Columns layout').click();
await page.getByTitle('Switch to rows layout').click();
// Assert the layout is in rows orientation
expect(await page.locator('.c-fl--rows').count()).toBeGreaterThan(0);
@ -171,7 +171,7 @@ test.describe('Flexible Layout', () => {
// Expand the 'My Items' folder in the left tree
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').first().click();
// Add the Sine Wave Generator to the Flexible Layout and save changes
await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first());
await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl-container.is-empty').first());
await page.locator('button[title="Save"]').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
@ -202,7 +202,7 @@ test.describe('Flexible Layout', () => {
// Expand the 'My Items' folder in the left tree
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();
// Add the Sine Wave Generator to the Flexible Layout and save changes
await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first());
await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl-container.is-empty').first());
await page.locator('button[title="Save"]').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
@ -242,7 +242,7 @@ test.describe('Flexible Layout', () => {
name: new RegExp(exampleImageryObject.name)
});
// Add the Sine Wave Generator to the Flexible Layout and save changes
await exampleImageryTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first());
await exampleImageryTreeItem.dragTo(page.locator('.c-fl-container.is-empty').first());
await page.locator('button[title="Save"]').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
@ -309,9 +309,9 @@ test.describe('Flexible Layout Toolbar Actions @localStorage', () => {
await page.getByRole('columnheader', { name: 'Container Handle 1' }).click();
const flexRows = page.getByLabel('Flexible Layout Row');
expect(await flexRows.count()).toEqual(0);
await page.getByTitle('Columns layout').click();
await page.getByTitle('Switch to rows layout').click();
expect(await flexRows.count()).toEqual(1);
await page.getByTitle('Rows layout').click();
await page.getByTitle('Switch to columns layout').click();
expect(await flexRows.count()).toEqual(0);
});
});

View File

@ -27,6 +27,7 @@ This test suite is dedicated to tests which verify the basic operations surround
import { fileURLToPath } from 'url';
import { createDomainObjectWithDefaults } from '../../../../appActions.js';
import { copy, paste, selectAll } from '../../../../helper/hotkeys/hotkeys.js';
import * as nbUtils from '../../../../helper/notebookUtils.js';
import { expect, streamToString, test } from '../../../../pluginFixtures.js';
@ -546,4 +547,53 @@ test.describe('Notebook entry tests', () => {
);
await expect(secondLineOfBlockquoteText).toBeVisible();
});
/**
* Paste into notebook entry tests
*/
test('Can paste text into a notebook entry', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7686'
});
const TEST_TEXT = 'This is a test';
const iterations = 20;
const EXPECTED_TEXT = TEST_TEXT.repeat(iterations);
await page.goto(notebookObject.url);
await nbUtils.addNotebookEntry(page);
await nbUtils.enterTextInLastEntry(page, TEST_TEXT);
await selectAll(page);
await copy(page);
for (let i = 0; i < iterations; i++) {
await paste(page);
}
await nbUtils.commitEntry(page);
await expect(page.locator(`text="${EXPECTED_TEXT}"`)).toBeVisible();
});
test('Prevents pasting text into selected notebook entry if not editing', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7686'
});
const TEST_TEXT = 'This is a test';
await page.goto(notebookObject.url);
await nbUtils.addNotebookEntry(page);
await nbUtils.enterTextInLastEntry(page, TEST_TEXT);
await selectAll(page);
await copy(page);
await paste(page);
await nbUtils.commitEntry(page);
// This should not paste text into the entry
await paste(page);
await expect(await page.locator(`text="${TEST_TEXT.repeat(1)}"`).count()).toEqual(1);
await expect(await page.locator(`text="${TEST_TEXT.repeat(2)}"`).count()).toEqual(0);
});
});

View File

@ -22,8 +22,8 @@
import {
createDomainObjectWithDefaults,
setTimeConductorBounds,
setTimeConductorMode
navigateToObjectWithRealTime,
setTimeConductorBounds
} from '../../../../appActions.js';
import { expect, test } from '../../../../pluginFixtures.js';
@ -39,12 +39,52 @@ test.describe('Telemetry Table', () => {
type: 'Sine Wave Generator',
parent: table.uuid
});
await page.goto(table.url);
await setTimeConductorMode(page, false);
await navigateToObjectWithRealTime(page, table.url);
const rows = page.getByLabel('table content').getByLabel('Table Row');
await expect(rows).toHaveCount(50);
});
test('on load, auto scrolls to top for descending, and to bottom for ascending', async ({
page
}) => {
const sineWaveGenerator = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: table.uuid
});
// verify in telemetry table object view
await navigateToObjectWithRealTime(page, table.url);
expect(await getScrollPosition(page)).toBe(0);
// verify in telemetry table view
await page.goto(sineWaveGenerator.url);
await page.getByLabel('Open the View Switcher Menu').click();
await page.getByText('Telemetry Table', { exact: true }).click();
expect(await getScrollPosition(page)).toBe(0);
// navigate back to table
await page.goto(table.url);
// go into edit mode
await page.getByLabel('Edit Object').click();
// change sort direction
await page.locator('thead div').filter({ hasText: 'Time' }).click();
// save view
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// navigate away and back
await page.goto(sineWaveGenerator.url);
await page.goto(table.url);
// verify scroll position
expect(await getScrollPosition(page, false)).toBeLessThan(1);
});
test('unpauses and filters data when paused by button and user changes bounds', async ({
page
}) => {
@ -183,3 +223,42 @@ test.describe('Telemetry Table', () => {
await page.click('button[title="Pause"]');
});
});
async function getScrollPosition(page, top = true) {
const tableBody = page.locator('.c-table__body-w');
// Wait for the scrollbar to appear
await tableBody.evaluate((node) => {
return new Promise((resolve) => {
function checkScroll() {
if (node.scrollHeight > node.clientHeight) {
resolve();
} else {
setTimeout(checkScroll, 100);
}
}
checkScroll();
});
});
// make sure there are rows
const rows = page.getByLabel('table content').getByLabel('Table Row');
await rows.first().waitFor();
// Using this to allow for rows to come and go, so we can truly test the scroll position
// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(1000);
const { scrollTop, clientHeight, scrollHeight } = await tableBody.evaluate((node) => ({
scrollTop: node.scrollTop,
clientHeight: node.clientHeight,
scrollHeight: node.scrollHeight
}));
// eslint-disable-next-line playwright/no-conditional-in-test
if (top) {
return scrollTop;
} else {
return Math.abs(scrollHeight - (scrollTop + clientHeight));
}
}

View File

@ -265,8 +265,8 @@ test.describe('Verify tooltips', () => {
name: 'Test Flexible Layout'
});
await page.dragAndDrop(`text=${sineWaveObject1.name}`, '.c-fl__container >> nth=0');
await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-fl__container >> nth=1');
await page.dragAndDrop(`text=${sineWaveObject1.name}`, '.c-fl-container >> nth=0');
await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-fl-container >> nth=1');
await page.locator('button[title="Save"]').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();

View File

@ -34,7 +34,7 @@ TODO:
import { expect, test } from '@playwright/test';
const filePath = 'e2e/test-data/PerformanceDisplayLayout.json';
const filePath = 'test-data/PerformanceDisplayLayout.json';
test.describe('Performance tests', () => {
test.beforeEach(async ({ page, browser }, testInfo) => {

View File

@ -33,7 +33,7 @@ TODO:
import { expect, test } from '@playwright/test';
const notebookFilePath = 'e2e/test-data/PerformanceNotebook.json';
const notebookFilePath = 'test-data/PerformanceNotebook.json';
test.describe('Performance tests', () => {
test.beforeEach(async ({ page, browser }, testInfo) => {

View File

@ -33,7 +33,7 @@ test.describe('Visual - Inspector @ally @clock', () => {
await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });
});
test.use({
storageState: './e2e/test-data/overlay_plot_with_delay_storage.json',
storageState: 'test-data/overlay_plot_with_delay_storage.json',
clockOptions: {
now: MISSION_TIME,
shouldAdvanceTime: true

View File

@ -35,7 +35,7 @@ test.describe('Visual - Controlled Clock @clock', () => {
await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });
});
test.use({
storageState: './e2e/test-data/overlay_plot_with_delay_storage.json',
storageState: 'test-data/overlay_plot_with_delay_storage.json',
clockOptions: {
now: MISSION_TIME,
shouldAdvanceTime: false //Don't advance the clock

View File

@ -23,7 +23,7 @@
/*
Collection of Visual Tests set to run in a default context with default Plugins. The tests within this suite
are only meant to run against openmct's app.js started by `npm run start` within the
`./e2e/playwright-visual.config.js` file.
`playwright-visual.config.js` file.
*/
import percySnapshot from '@percy/playwright';

View File

@ -141,7 +141,7 @@ export default class ExampleUserProvider extends EventEmitter {
}
getPossibleRoles() {
return this.user.getRoles();
return this.loginPromise.then(() => this.user.getRoles());
}
getPossibleMissionActions() {

View File

@ -24,12 +24,11 @@ import ExampleUserProvider from './ExampleUserProvider.js';
const AUTO_LOGIN_USER = 'mct-user';
const STATUS_ROLES = ['flight', 'driver'];
export default function ExampleUserPlugin(
{ autoLoginUser, statusRoles } = {
autoLoginUser: AUTO_LOGIN_USER,
statusRoles: STATUS_ROLES
}
) {
export default function ExampleUserPlugin(options = {}) {
const {
autoLoginUser = AUTO_LOGIN_USER,
statusRoles = STATUS_ROLES
} = options;
return function install(openmct) {
const userProvider = new ExampleUserProvider(openmct, {
statusRoles

View File

@ -24,13 +24,13 @@
const loadWebpackConfig = async () => {
if (process.env.KARMA_DEBUG) {
return {
config: (await import('./.webpack/webpack.dev.js')).default,
config: (await import('./.webpack/webpack.dev.mjs')).default,
browsers: ['ChromeDebugging'],
singleRun: false
};
} else {
return {
config: (await import('./.webpack/webpack.coverage.js')).default,
config: (await import('./.webpack/webpack.coverage.mjs')).default,
browsers: ['ChromeHeadless'],
singleRun: true
};

267
package-lock.json generated
View File

@ -8,13 +8,12 @@
"name": "openmct",
"version": "4.0.0-next",
"license": "Apache-2.0",
"workspaces": [
"e2e"
],
"devDependencies": {
"@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.42.1",
"@types/d3-axis": "3.0.6",
"@types/d3-scale": "4.0.8",
"@types/d3-selection": "3.0.10",
@ -22,7 +21,6 @@
"@types/eventemitter3": "1.2.0",
"@types/jasmine": "5.1.2",
"@types/lodash": "4.17.0",
"@types/sinonjs__fake-timers": "8.1.5",
"@vue/compiler-sfc": "3.4.3",
"babel-loader": "9.1.0",
"babel-plugin-istanbul": "6.1.1",
@ -81,13 +79,12 @@
"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",
"tiny-emitter": "2.1.0",
"typescript": "5.3.3",
"uuid": "9.0.1",
"vue": "3.4.19",
"vue": "3.4.24",
"vue-eslint-parser": "9.4.2",
"vue-loader": "16.8.3",
"webpack": "5.90.3",
@ -99,6 +96,19 @@
"node": ">=18.14.2 <22"
}
},
"e2e": {
"name": "openmct-e2e",
"version": "4.0.0-next",
"license": "Apache-2.0",
"devDependencies": {
"@axe-core/playwright": "4.8.5",
"@percy/cli": "1.27.4",
"@percy/playwright": "1.0.4",
"@playwright/test": "1.44.0",
"@types/sinonjs__fake-timers": "8.1.5",
"sinon": "17.0.0"
}
},
"node_modules/@aashutoshrathi/word-wrap": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz",
@ -388,9 +398,9 @@
}
},
"node_modules/@babel/parser": {
"version": "7.24.0",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.0.tgz",
"integrity": "sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==",
"version": "7.24.4",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.4.tgz",
"integrity": "sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==",
"dev": true,
"bin": {
"parser": "bin/babel-parser.js"
@ -1496,9 +1506,9 @@
}
},
"node_modules/@percy/sdk-utils": {
"version": "1.28.1",
"resolved": "https://registry.npmjs.org/@percy/sdk-utils/-/sdk-utils-1.28.1.tgz",
"integrity": "sha512-joS3i5wjFYXRSVL/NbUvip+bB7ErgwNjoDcID31l61y/QaSYUVCOxl/Fy4nvePJtHVyE1hpV0O7XO3tkoG908g==",
"version": "1.28.2",
"resolved": "https://registry.npmjs.org/@percy/sdk-utils/-/sdk-utils-1.28.2.tgz",
"integrity": "sha512-cMFz8AjZ2KunN0dVwzA+Wosk4B+6G9dUkh2YPhYvqs0KLcCyYs3s91IzOQmtBOYwAUVja/W/u6XmBHw0jaxg0A==",
"dev": true,
"engines": {
"node": ">=14"
@ -1549,12 +1559,12 @@
}
},
"node_modules/@playwright/test": {
"version": "1.42.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.42.1.tgz",
"integrity": "sha512-Gq9rmS54mjBL/7/MvBaNOBwbfnh7beHvS6oS4srqXFcQHpQCV1+c8JXWE8VLPyRDhgS3H8x8A7hztqI9VnwrAQ==",
"version": "1.44.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.0.tgz",
"integrity": "sha512-rNX5lbNidamSUorBhB4XZ9SQTjAqfe5M+p37Z8ic0jPFBMo5iCtQz1kRWkEMg+rYOKSlVycpQmpqjSFq7LXOfg==",
"dev": true,
"dependencies": {
"playwright": "1.42.1"
"playwright": "1.44.0"
},
"bin": {
"playwright": "cli.js"
@ -1929,16 +1939,6 @@
"@types/node": "*"
}
},
"node_modules/@types/yauzl": {
"version": "2.10.3",
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
"integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
"dev": true,
"optional": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@ungap/structured-clone": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
@ -1996,103 +1996,103 @@
}
},
"node_modules/@vue/reactivity": {
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.19.tgz",
"integrity": "sha512-+VcwrQvLZgEclGZRHx4O2XhyEEcKaBi50WbxdVItEezUf4fqRh838Ix6amWTdX0CNb/b6t3Gkz3eOebfcSt+UA==",
"version": "3.4.24",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.24.tgz",
"integrity": "sha512-nup3fSYg4i4LtNvu9slF/HF/0dkMQYfepUdORBcMSsankzRPzE7ypAFurpwyRBfU1i7Dn1kcwpYsE1wETSh91g==",
"dev": true,
"dependencies": {
"@vue/shared": "3.4.19"
"@vue/shared": "3.4.24"
}
},
"node_modules/@vue/reactivity/node_modules/@vue/shared": {
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.19.tgz",
"integrity": "sha512-/KliRRHMF6LoiThEy+4c1Z4KB/gbPrGjWwJR+crg2otgrf/egKzRaCPvJ51S5oetgsgXLfc4Rm5ZgrKHZrtMSw==",
"version": "3.4.24",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.24.tgz",
"integrity": "sha512-BW4tajrJBM9AGAknnyEw5tO2xTmnqgup0VTnDAMcxYmqOX0RG0b9aSUGAbEKolD91tdwpA6oCwbltoJoNzpItw==",
"dev": true
},
"node_modules/@vue/runtime-core": {
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.19.tgz",
"integrity": "sha512-/Z3tFwOrerJB/oyutmJGoYbuoadphDcJAd5jOuJE86THNZji9pYjZroQ2NFsZkTxOq0GJbb+s2kxTYToDiyZzw==",
"version": "3.4.24",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.24.tgz",
"integrity": "sha512-c7iMfj6cJMeAG3s5yOn9Rc5D9e2/wIuaozmGf/ICGCY3KV5H7mbTVdvEkd4ZshTq7RUZqj2k7LMJWVx+EBiY1g==",
"dev": true,
"dependencies": {
"@vue/reactivity": "3.4.19",
"@vue/shared": "3.4.19"
"@vue/reactivity": "3.4.24",
"@vue/shared": "3.4.24"
}
},
"node_modules/@vue/runtime-core/node_modules/@vue/shared": {
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.19.tgz",
"integrity": "sha512-/KliRRHMF6LoiThEy+4c1Z4KB/gbPrGjWwJR+crg2otgrf/egKzRaCPvJ51S5oetgsgXLfc4Rm5ZgrKHZrtMSw==",
"version": "3.4.24",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.24.tgz",
"integrity": "sha512-BW4tajrJBM9AGAknnyEw5tO2xTmnqgup0VTnDAMcxYmqOX0RG0b9aSUGAbEKolD91tdwpA6oCwbltoJoNzpItw==",
"dev": true
},
"node_modules/@vue/runtime-dom": {
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.19.tgz",
"integrity": "sha512-IyZzIDqfNCF0OyZOauL+F4yzjMPN2rPd8nhqPP2N1lBn3kYqJpPHHru+83Rkvo2lHz5mW+rEeIMEF9qY3PB94g==",
"version": "3.4.24",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.24.tgz",
"integrity": "sha512-uXKzuh/Emfad2Y7Qm0ABsLZZV6H3mAJ5ZVqmAOlrNQRf+T5mxpPGZBfec1hkP41t6h6FwF6RSGCs/gd8WbuySQ==",
"dev": true,
"dependencies": {
"@vue/runtime-core": "3.4.19",
"@vue/shared": "3.4.19",
"@vue/runtime-core": "3.4.24",
"@vue/shared": "3.4.24",
"csstype": "^3.1.3"
}
},
"node_modules/@vue/runtime-dom/node_modules/@vue/shared": {
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.19.tgz",
"integrity": "sha512-/KliRRHMF6LoiThEy+4c1Z4KB/gbPrGjWwJR+crg2otgrf/egKzRaCPvJ51S5oetgsgXLfc4Rm5ZgrKHZrtMSw==",
"version": "3.4.24",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.24.tgz",
"integrity": "sha512-BW4tajrJBM9AGAknnyEw5tO2xTmnqgup0VTnDAMcxYmqOX0RG0b9aSUGAbEKolD91tdwpA6oCwbltoJoNzpItw==",
"dev": true
},
"node_modules/@vue/server-renderer": {
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.19.tgz",
"integrity": "sha512-eAj2p0c429RZyyhtMRnttjcSToch+kTWxFPHlzGMkR28ZbF1PDlTcmGmlDxccBuqNd9iOQ7xPRPAGgPVj+YpQw==",
"version": "3.4.24",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.24.tgz",
"integrity": "sha512-H+DLK4sQF6sRgzKyofmlEVBIV/9KrQU6HIV7nt6yIwSGGKvSwlV8pqJlebUKLpbXaNHugdSfAbP6YmXF69lxow==",
"dev": true,
"dependencies": {
"@vue/compiler-ssr": "3.4.19",
"@vue/shared": "3.4.19"
"@vue/compiler-ssr": "3.4.24",
"@vue/shared": "3.4.24"
},
"peerDependencies": {
"vue": "3.4.19"
"vue": "3.4.24"
}
},
"node_modules/@vue/server-renderer/node_modules/@vue/compiler-core": {
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.19.tgz",
"integrity": "sha512-gj81785z0JNzRcU0Mq98E56e4ltO1yf8k5PQ+tV/7YHnbZkrM0fyFyuttnN8ngJZjbpofWE/m4qjKBiLl8Ju4w==",
"version": "3.4.24",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.24.tgz",
"integrity": "sha512-vbW/tgbwJYj62N/Ww99x0zhFTkZDTcGh3uwJEuadZ/nF9/xuFMC4693P9r+3sxGXISABpDKvffY5ApH9pmdd1A==",
"dev": true,
"dependencies": {
"@babel/parser": "^7.23.9",
"@vue/shared": "3.4.19",
"@babel/parser": "^7.24.4",
"@vue/shared": "3.4.24",
"entities": "^4.5.0",
"estree-walker": "^2.0.2",
"source-map-js": "^1.0.2"
"source-map-js": "^1.2.0"
}
},
"node_modules/@vue/server-renderer/node_modules/@vue/compiler-dom": {
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.19.tgz",
"integrity": "sha512-vm6+cogWrshjqEHTzIDCp72DKtea8Ry/QVpQRYoyTIg9k7QZDX6D8+HGURjtmatfgM8xgCFtJJaOlCaRYRK3QA==",
"version": "3.4.24",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.24.tgz",
"integrity": "sha512-4XgABML/4cNndVsQndG6BbGN7+EoisDwi3oXNovqL/4jdNhwvP8/rfRMTb6FxkxIxUUtg6AI1/qZvwfSjxJiWA==",
"dev": true,
"dependencies": {
"@vue/compiler-core": "3.4.19",
"@vue/shared": "3.4.19"
"@vue/compiler-core": "3.4.24",
"@vue/shared": "3.4.24"
}
},
"node_modules/@vue/server-renderer/node_modules/@vue/compiler-ssr": {
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.19.tgz",
"integrity": "sha512-P0PLKC4+u4OMJ8sinba/5Z/iDT84uMRRlrWzadgLA69opCpI1gG4N55qDSC+dedwq2fJtzmGald05LWR5TFfLw==",
"version": "3.4.24",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.24.tgz",
"integrity": "sha512-ZsAtr4fhaUFnVcDqwW3bYCSDwq+9Gk69q2r/7dAHDrOMw41kylaMgOP4zRnn6GIEJkQznKgrMOGPMFnLB52RbQ==",
"dev": true,
"dependencies": {
"@vue/compiler-dom": "3.4.19",
"@vue/shared": "3.4.19"
"@vue/compiler-dom": "3.4.24",
"@vue/shared": "3.4.24"
}
},
"node_modules/@vue/server-renderer/node_modules/@vue/shared": {
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.19.tgz",
"integrity": "sha512-/KliRRHMF6LoiThEy+4c1Z4KB/gbPrGjWwJR+crg2otgrf/egKzRaCPvJ51S5oetgsgXLfc4Rm5ZgrKHZrtMSw==",
"version": "3.4.24",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.24.tgz",
"integrity": "sha512-BW4tajrJBM9AGAknnyEw5tO2xTmnqgup0VTnDAMcxYmqOX0RG0b9aSUGAbEKolD91tdwpA6oCwbltoJoNzpItw==",
"dev": true
},
"node_modules/@vue/shared": {
@ -7778,15 +7778,12 @@
}
},
"node_modules/magic-string": {
"version": "0.30.8",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz",
"integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==",
"version": "0.30.10",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz",
"integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==",
"dev": true,
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15"
},
"engines": {
"node": ">=12"
}
},
"node_modules/make-dir": {
@ -8549,6 +8546,10 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/openmct-e2e": {
"resolved": "e2e",
"link": true
},
"node_modules/optionator": {
"version": "0.9.3",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
@ -8832,12 +8833,12 @@
}
},
"node_modules/playwright": {
"version": "1.42.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.42.1.tgz",
"integrity": "sha512-PgwB03s2DZBcNRoW+1w9E+VkLBxweib6KTXM0M3tkiT4jVxKSi6PmVJ591J+0u10LUrgxB7dLRbiJqO5s2QPMg==",
"version": "1.44.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.0.tgz",
"integrity": "sha512-F9b3GUCLQ3Nffrfb6dunPOkE5Mh68tR7zN32L4jCk4FjQamgesGay7/dAAe1WaMEGV04DkdJfcJzjoCKygUaRQ==",
"dev": true,
"dependencies": {
"playwright-core": "1.42.1"
"playwright-core": "1.44.0"
},
"bin": {
"playwright": "cli.js"
@ -8850,9 +8851,9 @@
}
},
"node_modules/playwright-core": {
"version": "1.42.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.42.1.tgz",
"integrity": "sha512-mxz6zclokgrke9p1vtdy/COWBH+eOZgYUVVU34C73M+4j4HLlQJHtfcqiqqxpP0o8HhMkflvfbquLX5dg6wlfA==",
"version": "1.44.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.0.tgz",
"integrity": "sha512-ZTbkNpFfYcGWohvTTl+xewITm7EOuqIqex0c7dNZ+aXsbrLj0qI8XlGKfPpipjm0Wny/4Lt4CJsWJk1stVS5qQ==",
"dev": true,
"bin": {
"playwright-core": "cli.js"
@ -8897,9 +8898,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.35",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz",
"integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==",
"version": "8.4.38",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
"integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==",
"dev": true,
"funding": [
{
@ -8918,7 +8919,7 @@
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
"source-map-js": "^1.2.0"
},
"engines": {
"node": "^10 || ^12 || >=14"
@ -10362,9 +10363,9 @@
}
},
"node_modules/source-map-js": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
"integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
"dev": true,
"engines": {
"node": ">=0.10.0"
@ -11237,16 +11238,16 @@
"dev": true
},
"node_modules/vue": {
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.4.19.tgz",
"integrity": "sha512-W/7Fc9KUkajFU8dBeDluM4sRGc/aa4YJnOYck8dkjgZoXtVsn3OeTGni66FV1l3+nvPA7VBFYtPioaGKUmEADw==",
"version": "3.4.24",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.4.24.tgz",
"integrity": "sha512-NPdx7dLGyHmKHGRRU5bMRYVE+rechR+KDU5R2tSTNG36PuMwbfAJ+amEvOAw7BPfZp5sQulNELSLm5YUkau+Sg==",
"dev": true,
"dependencies": {
"@vue/compiler-dom": "3.4.19",
"@vue/compiler-sfc": "3.4.19",
"@vue/runtime-dom": "3.4.19",
"@vue/server-renderer": "3.4.19",
"@vue/shared": "3.4.19"
"@vue/compiler-dom": "3.4.24",
"@vue/compiler-sfc": "3.4.24",
"@vue/runtime-dom": "3.4.24",
"@vue/server-renderer": "3.4.24",
"@vue/shared": "3.4.24"
},
"peerDependencies": {
"typescript": "*"
@ -11436,59 +11437,59 @@
}
},
"node_modules/vue/node_modules/@vue/compiler-core": {
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.19.tgz",
"integrity": "sha512-gj81785z0JNzRcU0Mq98E56e4ltO1yf8k5PQ+tV/7YHnbZkrM0fyFyuttnN8ngJZjbpofWE/m4qjKBiLl8Ju4w==",
"version": "3.4.24",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.24.tgz",
"integrity": "sha512-vbW/tgbwJYj62N/Ww99x0zhFTkZDTcGh3uwJEuadZ/nF9/xuFMC4693P9r+3sxGXISABpDKvffY5ApH9pmdd1A==",
"dev": true,
"dependencies": {
"@babel/parser": "^7.23.9",
"@vue/shared": "3.4.19",
"@babel/parser": "^7.24.4",
"@vue/shared": "3.4.24",
"entities": "^4.5.0",
"estree-walker": "^2.0.2",
"source-map-js": "^1.0.2"
"source-map-js": "^1.2.0"
}
},
"node_modules/vue/node_modules/@vue/compiler-dom": {
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.19.tgz",
"integrity": "sha512-vm6+cogWrshjqEHTzIDCp72DKtea8Ry/QVpQRYoyTIg9k7QZDX6D8+HGURjtmatfgM8xgCFtJJaOlCaRYRK3QA==",
"version": "3.4.24",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.24.tgz",
"integrity": "sha512-4XgABML/4cNndVsQndG6BbGN7+EoisDwi3oXNovqL/4jdNhwvP8/rfRMTb6FxkxIxUUtg6AI1/qZvwfSjxJiWA==",
"dev": true,
"dependencies": {
"@vue/compiler-core": "3.4.19",
"@vue/shared": "3.4.19"
"@vue/compiler-core": "3.4.24",
"@vue/shared": "3.4.24"
}
},
"node_modules/vue/node_modules/@vue/compiler-sfc": {
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.19.tgz",
"integrity": "sha512-LQ3U4SN0DlvV0xhr1lUsgLCYlwQfUfetyPxkKYu7dkfvx7g3ojrGAkw0AERLOKYXuAGnqFsEuytkdcComei3Yg==",
"version": "3.4.24",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.24.tgz",
"integrity": "sha512-nRAlJUK02FTWfA2nuvNBAqsDZuERGFgxZ8sGH62XgFSvMxO2URblzulExsmj4gFZ8e+VAyDooU9oAoXfEDNxTA==",
"dev": true,
"dependencies": {
"@babel/parser": "^7.23.9",
"@vue/compiler-core": "3.4.19",
"@vue/compiler-dom": "3.4.19",
"@vue/compiler-ssr": "3.4.19",
"@vue/shared": "3.4.19",
"@babel/parser": "^7.24.4",
"@vue/compiler-core": "3.4.24",
"@vue/compiler-dom": "3.4.24",
"@vue/compiler-ssr": "3.4.24",
"@vue/shared": "3.4.24",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.6",
"postcss": "^8.4.33",
"source-map-js": "^1.0.2"
"magic-string": "^0.30.10",
"postcss": "^8.4.38",
"source-map-js": "^1.2.0"
}
},
"node_modules/vue/node_modules/@vue/compiler-ssr": {
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.19.tgz",
"integrity": "sha512-P0PLKC4+u4OMJ8sinba/5Z/iDT84uMRRlrWzadgLA69opCpI1gG4N55qDSC+dedwq2fJtzmGald05LWR5TFfLw==",
"version": "3.4.24",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.24.tgz",
"integrity": "sha512-ZsAtr4fhaUFnVcDqwW3bYCSDwq+9Gk69q2r/7dAHDrOMw41kylaMgOP4zRnn6GIEJkQznKgrMOGPMFnLB52RbQ==",
"dev": true,
"dependencies": {
"@vue/compiler-dom": "3.4.19",
"@vue/shared": "3.4.19"
"@vue/compiler-dom": "3.4.24",
"@vue/shared": "3.4.24"
}
},
"node_modules/vue/node_modules/@vue/shared": {
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.19.tgz",
"integrity": "sha512-/KliRRHMF6LoiThEy+4c1Z4KB/gbPrGjWwJR+crg2otgrf/egKzRaCPvJ51S5oetgsgXLfc4Rm5ZgrKHZrtMSw==",
"version": "3.4.24",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.24.tgz",
"integrity": "sha512-BW4tajrJBM9AGAknnyEw5tO2xTmnqgup0VTnDAMcxYmqOX0RG0b9aSUGAbEKolD91tdwpA6oCwbltoJoNzpItw==",
"dev": true
},
"node_modules/watchpack": {

View File

@ -2,15 +2,21 @@
"name": "openmct",
"version": "4.0.0-next",
"description": "The Open MCT core platform",
"type": "module",
"module": "dist/openmct.js",
"main": "dist/openmct.js",
"types": "dist/types/index.d.ts",
"exports": {
".": {
"import": "./dist/openmct.js",
"require": "./dist/openmct.js"
}
},
"workspaces": [
"e2e"
],
"devDependencies": {
"@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.42.1",
"@types/d3-axis": "3.0.6",
"@types/d3-scale": "4.0.8",
"@types/d3-selection": "3.0.10",
@ -18,7 +24,6 @@
"@types/eventemitter3": "1.2.0",
"@types/jasmine": "5.1.2",
"@types/lodash": "4.17.0",
"@types/sinonjs__fake-timers": "8.1.5",
"@vue/compiler-sfc": "3.4.3",
"babel-loader": "9.1.0",
"babel-plugin-istanbul": "6.1.1",
@ -77,13 +82,12 @@
"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",
"tiny-emitter": "2.1.0",
"typescript": "5.3.3",
"uuid": "9.0.1",
"vue": "3.4.19",
"vue": "3.4.24",
"vue-eslint-parser": "9.4.2",
"vue-loader": "16.8.3",
"webpack": "5.90.3",
@ -92,39 +96,39 @@
"webpack-merge": "5.10.0"
},
"scripts": {
"clean": "rm -rf ./dist ./node_modules ./coverage ./html-test-results ./test-results ./.nyc_output ",
"start": "npx webpack serve --config ./.webpack/webpack.dev.js",
"start:prod": "npx webpack serve --config ./.webpack/webpack.prod.js",
"start:coverage": "npx webpack serve --config ./.webpack/webpack.coverage.js",
"clean": "rm -rf ./dist ./node_modules ./coverage ./html-test-results ./test-results ./.nyc_output",
"start": "npx webpack serve --config ./.webpack/webpack.dev.mjs",
"start:prod": "npx webpack serve --config ./.webpack/webpack.prod.mjs",
"start:coverage": "npx webpack serve --config ./.webpack/webpack.coverage.mjs",
"lint:js": "eslint \"example/**/*.js\" \"src/**/*.js\" \"e2e/**/*.js\" \"openmct.js\" --max-warnings=0",
"lint:vue": "eslint \"src/**/*.vue\"",
"lint:spelling": "cspell \"**/*.{js,md,vue}\" --show-context --gitignore --quiet",
"lint": "run-p \"lint:js -- {1}\" \"lint:vue -- {1}\" \"lint:spelling -- {1}\" --",
"lint:fix": "eslint example src e2e --ext .js,.vue openmct.js --fix",
"build:prod": "webpack --config ./.webpack/webpack.prod.js",
"build:dev": "webpack --config ./.webpack/webpack.dev.js",
"build:coverage": "webpack --config ./.webpack/webpack.coverage.js",
"build:watch": "webpack --config ./.webpack/webpack.dev.js --watch",
"build:prod": "webpack --config ./.webpack/webpack.prod.mjs",
"build:dev": "webpack --config ./.webpack/webpack.dev.mjs",
"build:coverage": "webpack --config ./.webpack/webpack.coverage.mjs",
"build:watch": "webpack --config ./.webpack/webpack.dev.mjs --watch",
"info": "npx envinfo --system --browsers --npmPackages --binaries --languages --markdown",
"test": "karma start karma.conf.cjs",
"test:debug": "KARMA_DEBUG=true karma start karma.conf.cjs",
"test:e2e": "npx playwright test",
"test:e2e:a11y": "npx playwright test --config=e2e/playwright-visual-a11y.config.js --project=chrome --grep @a11y",
"test:e2e:mobile": "npx playwright test --config=e2e/playwright-mobile.config.js",
"test:e2e:couchdb": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @couchdb --workers=1",
"test:e2e:stable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep-invert \"@unstable|@couchdb|@generatedata\"",
"test:e2e:unstable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @unstable",
"test:e2e:local": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome",
"test:e2e:generatedata": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @generatedata",
"test:e2e:checksnapshots": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @snapshot --retries=0",
"test:e2e:updatesnapshots": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @snapshot --update-snapshots",
"test:e2e:visual:ci": "percy exec --config ./e2e/.percy.ci.yml --partial -- npx playwright test --config=e2e/playwright-visual-a11y.config.js --project=chrome --grep-invert @unstable",
"test:e2e:visual:full": "percy exec --config ./e2e/.percy.nightly.yml -- npx playwright test --config=e2e/playwright-visual-a11y.config.js --grep-invert @unstable",
"test:e2e:full": "npx playwright test --config=e2e/playwright-ci.config.js --grep-invert @couchdb",
"test:e2e:watch": "npx playwright test --ui --config=e2e/playwright-watch.config.js",
"test:perf:contract": "npx playwright test --config=e2e/playwright-performance-dev.config.js",
"test:perf:localhost": "npx playwright test --config=e2e/playwright-performance-prod.config.js --project=chrome",
"test:perf:memory": "npx playwright test --config=e2e/playwright-performance-prod.config.js --project=chrome-memory",
"test:e2e": "npm test --workspace e2e",
"test:e2e:a11y": "npm test --workspace e2e -- --config=playwright-visual-a11y.config.js --project=chrome --grep @a11y",
"test:e2e:mobile": "npm test --workspace e2e -- --config=playwright-mobile.config.js",
"test:e2e:couchdb": "npm test --workspace e2e -- --config=playwright-ci.config.js --project=chrome --grep @couchdb --workers=1",
"test:e2e:stable": "npm test --workspace e2e -- --config=playwright-ci.config.js --project=chrome --grep-invert \"@unstable|@couchdb|@generatedata\"",
"test:e2e:unstable": "npm test --workspace e2e -- --config=playwright-ci.config.js --project=chrome --grep @unstable",
"test:e2e:local": "npm test --workspace e2e -- --config=playwright-local.config.js --project=chrome",
"test:e2e:generatedata": "npm test --workspace e2e -- --config=playwright-ci.config.js --project=chrome --grep @generatedata",
"test:e2e:checksnapshots": "npm test --workspace e2e -- --config=playwright-ci.config.js --project=chrome --grep @snapshot --retries=0",
"test:e2e:updatesnapshots": "npm test --workspace e2e -- --config=playwright-ci.config.js --project=chrome --grep @snapshot --update-snapshots",
"test:e2e:visual:ci": "npm run test:visual --workspace e2e -- --config .percy.ci.yml --partial -- npx playwright test --config=playwright-visual-a11y.config.js --project=chrome --grep-invert @unstable",
"test:e2e:visual:full": "npm run test:visual --workspace e2e -- --config .percy.nightly.yml -- npx playwright test --config=playwright-visual-a11y.config.js --grep-invert @unstable",
"test:e2e:full": "npm test --workspace e2e -- --config=playwright-ci.config.js --grep-invert @couchdb",
"test:e2e:watch": "npm test --workspace e2e -- --ui --config=playwright-watch.config.js",
"test:perf:contract": "npm test --workspace e2e -- --config=playwright-performance-dev.config.js",
"test:perf:localhost": "npm test --workspace e2e -- --config=playwright-performance-prod.config.js --project=chrome",
"test:perf:memory": "npm test --workspace e2e -- --config=playwright-performance-prod.config.js --project=chrome-memory",
"update-about-dialog-copyright": "perl -pi -e 's/20\\d\\d\\-202\\d/2014\\-2023/gm' ./src/ui/layout/AboutDialog.vue",
"update-copyright-date": "npm run update-about-dialog-copyright && grep -lr --null --include=*.{js,scss,vue,ts,sh,html,md,frag} 'Copyright (c) 20' . | xargs -r0 perl -pi -e 's/Copyright\\s\\(c\\)\\s20\\d\\d\\-20\\d\\d/Copyright \\(c\\)\\ 2014\\-2024/gm'",
"cov:e2e:report": "nyc report --reporter=lcovonly --report-dir=./coverage/e2e",

View File

@ -22,6 +22,10 @@
import TimeContext from './TimeContext.js';
/**
* @typedef {import('./TimeAPI').TimeConductorBounds} TimeConductorBounds
*/
/**
* The GlobalContext handles getting and setting time of the openmct application in general.
* Views will use this context unless they specify an alternate/independent time context
@ -38,12 +42,10 @@ class GlobalTimeContext extends TimeContext {
* Get or set the start and end time of the time conductor. Basic validation
* of bounds is performed.
*
* @param {module:openmct.TimeAPI~TimeConductorBounds} newBounds
* @param {TimeConductorBounds} newBounds
* @throws {Error} Validation error
* @fires module:openmct.TimeAPI~bounds
* @returns {module:openmct.TimeAPI~TimeConductorBounds}
* @memberof module:openmct.TimeAPI#
* @method bounds
* @returns {TimeConductorBounds}
* @override
*/
bounds(newBounds) {
if (arguments.length > 0) {
@ -61,9 +63,9 @@ class GlobalTimeContext extends TimeContext {
/**
* Update bounds based on provided time and current offsets
* @private
* @param {number} timestamp A time from which bounds will be calculated
* using current offsets.
* @override
*/
tick(timestamp) {
super.tick.call(this, ...arguments);
@ -81,11 +83,8 @@ class GlobalTimeContext extends TimeContext {
* be manipulated by the user from the time conductor or from other views.
* The time of interest can effectively be unset by assigning a value of
* 'undefined'.
* @fires module:openmct.TimeAPI~timeOfInterest
* @param newTOI
* @returns {number} the current time of interest
* @memberof module:openmct.TimeAPI#
* @method timeOfInterest
*/
timeOfInterest(newTOI) {
if (arguments.length > 0) {
@ -93,8 +92,7 @@ class GlobalTimeContext extends TimeContext {
/**
* The Time of Interest has moved.
* @event timeOfInterest
* @memberof module:openmct.TimeAPI~
* @property {number} Current time of interest
* @property {number} timeOfInterest time of interest
*/
this.emit('timeOfInterest', this.toi);
}

View File

@ -23,19 +23,36 @@
import { MODES, REALTIME_MODE_KEY, TIME_CONTEXT_EVENTS } from './constants.js';
import TimeContext from './TimeContext.js';
/**
* @typedef {import('./TimeAPI.js').default} TimeAPI
* @typedef {import('./GlobalTimeContext.js').default} GlobalTimeContext
* @typedef {import('./TimeAPI.js').TimeSystem} TimeSystem
* @typedef {import('./TimeContext.js').Mode} Mode
* @typedef {import('./TimeContext.js').TimeConductorBounds} TimeConductorBounds
* @typedef {import('./TimeAPI.js').ClockOffsets} ClockOffsets
*/
/**
* The IndependentTimeContext handles getting and setting time of the openmct application in general.
* Views will use the GlobalTimeContext unless they specify an alternate/independent time context here.
*/
class IndependentTimeContext extends TimeContext {
/**
* @param {import('openmct').OpenMCT} openmct - The Open MCT application instance.
* @param {TimeAPI & GlobalTimeContext} globalTimeContext - The global time context.
* @param {import('openmct').ObjectPath} objectPath - The path of objects.
*/
constructor(openmct, globalTimeContext, objectPath) {
super();
/** @type {any} */
this.openmct = openmct;
/** @type {Function[]} */
this.unlisteners = [];
/** @type {TimeAPI & GlobalTimeContext | undefined} */
this.globalTimeContext = globalTimeContext;
// We always start with the global time context.
// This upstream context will be undefined when an independent time context is added later.
/** @type {TimeAPI & GlobalTimeContext | undefined} */
this.upstreamTimeContext = this.globalTimeContext;
/** @type {Array<any>} */
this.objectPath = objectPath;
this.refreshContext = this.refreshContext.bind(this);
this.resetContext = this.resetContext.bind(this);
@ -47,6 +64,10 @@ class IndependentTimeContext extends TimeContext {
this.globalTimeContext.on('removeOwnContext', this.removeIndependentContext);
}
/**
* @deprecated
* @override
*/
bounds() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.bounds(...arguments);
@ -55,6 +76,9 @@ class IndependentTimeContext extends TimeContext {
}
}
/**
* @override
*/
getBounds() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.getBounds();
@ -63,6 +87,9 @@ class IndependentTimeContext extends TimeContext {
}
}
/**
* @override
*/
setBounds() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.setBounds(...arguments);
@ -71,6 +98,9 @@ class IndependentTimeContext extends TimeContext {
}
}
/**
* @override
*/
tick() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.tick(...arguments);
@ -79,6 +109,9 @@ class IndependentTimeContext extends TimeContext {
}
}
/**
* @override
*/
clockOffsets() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.clockOffsets(...arguments);
@ -87,6 +120,9 @@ class IndependentTimeContext extends TimeContext {
}
}
/**
* @override
*/
getClockOffsets() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.getClockOffsets();
@ -95,6 +131,9 @@ class IndependentTimeContext extends TimeContext {
}
}
/**
* @override
*/
setClockOffsets() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.setClockOffsets(...arguments);
@ -103,12 +142,24 @@ class IndependentTimeContext extends TimeContext {
}
}
/**
*
* @param {number} newTOI
* @returns {number}
*/
timeOfInterest(newTOI) {
return this.globalTimeContext.timeOfInterest(...arguments);
}
/**
*
* @param {TimeSystem | string} timeSystemOrKey
* @param {TimeConductorBounds} bounds
* @returns {TimeSystem}
* @override
*/
timeSystem(timeSystemOrKey, bounds) {
return this.globalTimeContext.timeSystem(...arguments);
return this.globalTimeContext.setTimeSystem(...arguments);
}
/**
@ -116,6 +167,7 @@ class IndependentTimeContext extends TimeContext {
* @returns {TimeSystem} The currently applied time system
* @memberof module:openmct.TimeAPI#
* @method getTimeSystem
* @override
*/
getTimeSystem() {
return this.globalTimeContext.getTimeSystem();
@ -246,6 +298,7 @@ class IndependentTimeContext extends TimeContext {
/**
* Get the current mode.
* @return {Mode} the current mode;
* @override
*/
getMode() {
if (this.upstreamTimeContext) {
@ -259,9 +312,8 @@ class IndependentTimeContext extends TimeContext {
* Set the mode to either fixed or realtime.
*
* @param {Mode} mode The mode to activate
* @param {TimeBounds | ClockOffsets} offsetsOrBounds A time window of a fixed width
* @fires module:openmct.TimeAPI~clock
* @return {Mode} the currently active mode;
* @param {TimeConductorBounds | ClockOffsets} offsetsOrBounds A time window of a fixed width
* @return {Mode | undefined} the currently active mode;
*/
setMode(mode, offsetsOrBounds) {
if (!mode) {
@ -299,6 +351,10 @@ class IndependentTimeContext extends TimeContext {
return this.mode;
}
/**
* @returns {boolean}
* @override
*/
isRealTime() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.isRealTime(...arguments);
@ -307,6 +363,10 @@ class IndependentTimeContext extends TimeContext {
}
}
/**
* @returns {number}
* @override
*/
now() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.now(...arguments);
@ -343,6 +403,9 @@ class IndependentTimeContext extends TimeContext {
this.unlisteners = [];
}
/**
* Reset the time context to the global time context
*/
resetContext() {
if (this.upstreamTimeContext) {
this.stopFollowingTimeContext();
@ -352,6 +415,7 @@ class IndependentTimeContext extends TimeContext {
/**
* Refresh the time context, following any upstream time contexts as necessary
* @param {string} [viewKey] The key of the view to refresh
*/
refreshContext(viewKey) {
const key = this.openmct.objects.makeKeyString(this.objectPath[0].identifier);
@ -366,14 +430,21 @@ class IndependentTimeContext extends TimeContext {
this.followTimeContext();
// Emit bounds so that views that are changing context get the upstream bounds
this.emit('bounds', this.bounds());
this.emit('bounds', this.getBounds());
this.emit(TIME_CONTEXT_EVENTS.boundsChanged, this.getBounds());
}
/**
* @returns {boolean} True if this time context has an independent context, false otherwise
*/
hasOwnContext() {
return this.upstreamTimeContext === undefined;
}
/**
* Get the upstream time context of this time context
* @returns {TimeAPI & GlobalTimeContext | undefined} The upstream time context
*/
getUpstreamContext() {
// If a view has an independent context, don't return an upstream context
// Be aware that when a new independent time context is created, we assign the global context as default

View File

@ -26,32 +26,18 @@ import IndependentTimeContext from '@/api/time/IndependentTimeContext';
import GlobalTimeContext from './GlobalTimeContext.js';
/**
* The public API for setting and querying the temporal state of the
* application. The concept of time is integral to Open MCT, and at least
* one {@link TimeSystem}, as well as some default time bounds must be
* registered and enabled via {@link TimeAPI.addTimeSystem} and
* {@link TimeAPI.timeSystem} respectively for Open MCT to work.
*
* Time-sensitive views will typically respond to changes to bounds or other
* properties of the time conductor and update the data displayed based on
* the temporal state of the application. The current time bounds are also
* used in queries for historical data.
*
* The TimeAPI extends the GlobalTimeContext which in turn extends the TimeContext/EventEmitter class. A number of events are
* fired when properties of the time conductor change, which are documented
* below.
*
* @interface
* @memberof module:openmct
* @typedef {import('./TimeContext.js').default} TimeContext
*/
class TimeAPI extends GlobalTimeContext {
constructor(openmct) {
super();
this.openmct = openmct;
this.independentContexts = new Map();
}
/**
/**
* @typedef {import('./TimeContext.js').TimeConductorBounds} TimeConductorBounds
*/
/**
* @typedef {import('./TimeContext.js').ClockOffsets} ClockOffsets
*/
/**
* A TimeSystem provides meaning to the values returned by the TimeAPI. Open
* MCT supports multiple different types of time values, although all are
* intrinsically represented by numbers, the meaning of those numbers can
@ -74,10 +60,35 @@ class TimeAPI extends GlobalTimeContext {
* displaying a duration or relative span of time in this time system.
*/
/**
* The public API for setting and querying the temporal state of the
* application. The concept of time is integral to Open MCT, and at least
* one {@link TimeSystem}, as well as some default time bounds must be
* registered and enabled via {@link TimeAPI.addTimeSystem} and
* {@link TimeAPI.timeSystem} respectively for Open MCT to work.
*
* Time-sensitive views will typically respond to changes to bounds or other
* properties of the time conductor and update the data displayed based on
* the temporal state of the application. The current time bounds are also
* used in queries for historical data.
*
* The TimeAPI extends the GlobalTimeContext which in turn extends the TimeContext/EventEmitter class. A number of events are
* fired when properties of the time conductor change, which are documented
* below.
*
* @class
* @extends {GlobalTimeContext}
*/
class TimeAPI extends GlobalTimeContext {
constructor(openmct) {
super();
this.openmct = openmct;
this.independentContexts = new Map();
}
/**
* Register a new time system. Once registered it can activated using
* {@link TimeAPI.timeSystem}, and can be referenced via its key in [Time Conductor configuration](@link https://github.com/nasa/openmct/blob/master/API.md#time-conductor).
* @memberof module:openmct.TimeAPI#
* @param {TimeSystem} timeSystem A time system object.
*/
addTimeSystem(timeSystem) {
@ -109,7 +120,6 @@ class TimeAPI extends GlobalTimeContext {
/**
* Register a new Clock.
* @memberof module:openmct.TimeAPI#
* @param {Clock} clock
*/
addClock(clock) {
@ -117,9 +127,7 @@ class TimeAPI extends GlobalTimeContext {
}
/**
* @memberof module:openmct.TimeAPI#
* @returns {Clock[]}
* @memberof module:openmct.TimeAPI#
*/
getAllClocks() {
return Array.from(this.clocks.values());
@ -128,11 +136,9 @@ class TimeAPI extends GlobalTimeContext {
/**
* Get or set an independent time context which follows the TimeAPI timeSystem,
* but with different offsets for a given domain object
* @param {key | string} key The identifier key of the domain object these offsets are set for
* @param {ClockOffsets | TimeBounds} value This maintains a sliding time window of a fixed width that automatically updates
* @param {string} key The identifier key of the domain object these offsets are set for
* @param {ClockOffsets | TimeConductorBounds} value This maintains a sliding time window of a fixed width that automatically updates
* @param {key | string} clockKey the real time clock key currently in use
* @memberof module:openmct.TimeAPI#
* @method addIndependentTimeContext
*/
addIndependentContext(key, value, clockKey) {
let timeContext = this.getIndependentContext(key);
@ -159,9 +165,8 @@ class TimeAPI extends GlobalTimeContext {
/**
* Get the independent time context which follows the TimeAPI timeSystem,
* but with different offsets.
* @param {key | string} key The identifier key of the domain object these offsets
* @memberof module:openmct.TimeAPI#
* @method getIndependentTimeContext
* @param {string} key The identifier key of the domain object these offsets
* @returns {IndependentTimeContext} The independent time context
*/
getIndependentContext(key) {
return this.independentContexts.get(key);
@ -170,9 +175,8 @@ class TimeAPI extends GlobalTimeContext {
/**
* Get the a timeContext for a view based on it's objectPath. If there is any object in the objectPath with an independent time context, it will be returned.
* Otherwise, the global time context will be returned.
* @param { Array } objectPath The view's objectPath
* @memberof module:openmct.TimeAPI#
* @method getContextForView
* @param {Array} objectPath The view's objectPath
* @returns {TimeContext | GlobalTimeContext} The time context
*/
getContextForView(objectPath) {
if (!objectPath || !Array.isArray(objectPath)) {

View File

@ -57,7 +57,7 @@ describe('The Time API', function () {
expect(api.timeOfInterest()).toBe(toi);
});
it('Allows setting of valid bounds', function () {
it('[Legacy TimeAPI]: Allows setting of valid bounds', function () {
bounds = {
start: 0,
end: 1
@ -67,7 +67,17 @@ describe('The Time API', function () {
expect(api.bounds()).toEqual(bounds);
});
it('Disallows setting of invalid bounds', function () {
it('Allows setting of valid bounds', function () {
bounds = {
start: 0,
end: 1
};
expect(api.getBounds()).not.toBe(bounds);
expect(api.setBounds.bind(api, bounds)).not.toThrow();
expect(api.getBounds()).toEqual(bounds);
});
it('[Legacy TimeAPI]: Disallows setting of invalid bounds', function () {
bounds = {
start: 1,
end: 0
@ -82,7 +92,22 @@ describe('The Time API', function () {
expect(api.bounds()).not.toEqual(bounds);
});
it('Allows setting of previously registered time system with bounds', function () {
it('Disallows setting of invalid bounds', function () {
bounds = {
start: 1,
end: 0
};
expect(api.getBounds()).not.toEqual(bounds);
expect(api.setBounds.bind(api, bounds)).toThrow();
expect(api.getBounds()).not.toEqual(bounds);
bounds = { start: 1 };
expect(api.getBounds()).not.toEqual(bounds);
expect(api.setBounds.bind(api, bounds)).toThrow();
expect(api.getBounds()).not.toEqual(bounds);
});
it('[Legacy TimeAPI]: Allows setting of previously registered time system with bounds', function () {
api.addTimeSystem(timeSystem);
expect(api.timeSystem()).not.toBe(timeSystem);
expect(function () {
@ -91,7 +116,16 @@ describe('The Time API', function () {
expect(api.timeSystem()).toEqual(timeSystem);
});
it('Disallows setting of time system without bounds', function () {
it('Allows setting of previously registered time system with bounds', function () {
api.addTimeSystem(timeSystem);
expect(api.getTimeSystem()).not.toBe(timeSystem);
expect(function () {
api.setTimeSystem(timeSystem, bounds);
}).not.toThrow();
expect(api.getTimeSystem()).toEqual(timeSystem);
});
it('[Legacy TimeAPI]: Disallows setting of time system without bounds', function () {
api.addTimeSystem(timeSystem);
expect(api.timeSystem()).not.toBe(timeSystem);
expect(function () {
@ -100,6 +134,32 @@ describe('The Time API', function () {
expect(api.timeSystem()).not.toBe(timeSystem);
});
it('Allows setting of time system without bounds', function () {
api.addTimeSystem(timeSystem);
expect(api.getTimeSystem()).not.toBe(timeSystem);
expect(function () {
api.setTimeSystem(timeSystemKey);
}).not.toThrow();
expect(api.getTimeSystem()).not.toBe(timeSystem);
});
it('Disallows setting of invalid time system', function () {
expect(function () {
api.setTimeSystem();
}).toThrow();
expect(function () {
api.setTimeSystem('invalidTimeSystemKey');
}).toThrow();
expect(function () {
api.setTimeSystem({
key: 'invalidTimeSystemKey'
});
}).toThrow();
expect(function () {
api.setTimeSystem(42);
}).toThrow();
});
it('allows setting of timesystem without bounds with clock', function () {
api.addTimeSystem(timeSystem);
api.addClock(clock);
@ -114,7 +174,7 @@ describe('The Time API', function () {
expect(api.timeSystem()).toEqual(timeSystem);
});
it('Emits an event when time system changes', function () {
it('Emits a legacy event when time system changes', function () {
api.addTimeSystem(timeSystem);
expect(eventListener).not.toHaveBeenCalled();
api.on('timeSystem', eventListener);
@ -122,6 +182,14 @@ describe('The Time API', function () {
expect(eventListener).toHaveBeenCalledWith(timeSystem);
});
it('Emits an event when time system changes', function () {
api.addTimeSystem(timeSystem);
expect(eventListener).not.toHaveBeenCalled();
api.on('timeSystemChanged', eventListener);
api.timeSystem(timeSystemKey, bounds);
expect(eventListener).toHaveBeenCalledWith(timeSystem);
});
it('Emits an event when time of interest changes', function () {
expect(eventListener).not.toHaveBeenCalled();
api.on('timeOfInterest', eventListener);
@ -129,13 +197,20 @@ describe('The Time API', function () {
expect(eventListener).toHaveBeenCalledWith(toi);
});
it('Emits an event when bounds change', function () {
it('Emits a legacy event when bounds change', function () {
expect(eventListener).not.toHaveBeenCalled();
api.on('bounds', eventListener);
api.bounds(bounds);
expect(eventListener).toHaveBeenCalledWith(bounds, false);
});
it('Emits an event when bounds change', function () {
expect(eventListener).not.toHaveBeenCalled();
api.on('boundsChanged', eventListener);
api.bounds(bounds);
expect(eventListener).toHaveBeenCalledWith(bounds, false);
});
it('If bounds are set and TOI lies inside them, do not change TOI', function () {
api.timeOfInterest(6);
api.bounds({
@ -154,13 +229,39 @@ describe('The Time API', function () {
expect(api.timeOfInterest()).toBeUndefined();
});
it('Maintains delta during tick', function () {});
it('Maintains delta during tick', function () {
const initialBounds = { start: 100, end: 200 };
api.bounds(initialBounds);
const mockTickSource = jasmine.createSpyObj('mockTickSource', ['on', 'off', 'currentValue']);
mockTickSource.key = 'mct';
mockTickSource.currentValue.and.returnValue(150);
api.addClock(mockTickSource);
api.clock('mct', { start: 0, end: 100 });
it('Allows registered time system to be activated', function () {});
// Simulate a tick event
const tickCallback = mockTickSource.on.calls.mostRecent().args[1];
tickCallback(150);
const newBounds = api.bounds();
expect(newBounds.end - newBounds.start).toEqual(initialBounds.end - initialBounds.start);
});
it('Allows registered time system to be activated', function () {
api.addClock(clock);
api.clock(clockKey, { start: 0, end: 100 });
api.addTimeSystem(timeSystem);
api.timeSystem(timeSystemKey);
expect(api.timeSystem().key).toEqual(timeSystemKey);
});
it('Allows a registered tick source to be activated', function () {
const mockTickSource = jasmine.createSpyObj('mockTickSource', ['on', 'off', 'currentValue']);
mockTickSource.key = 'mockTickSource';
mockTickSource.currentValue.and.returnValue(50);
api.addClock(mockTickSource);
api.clock(mockTickSource.key, { start: 0, end: 100 });
expect(mockTickSource.on).toHaveBeenCalledWith('tick', jasmine.any(Function));
});
describe(' when enabling a tick source', function () {
@ -184,7 +285,7 @@ describe('The Time API', function () {
api.addClock(anotherMockTickSource);
});
it('sets bounds based on current value', function () {
it('[Legacy TimeAPI]: sets bounds based on current value', function () {
api.clock('mts', mockOffsets);
expect(api.bounds()).toEqual({
start: 10,
@ -192,23 +293,46 @@ describe('The Time API', function () {
});
});
it('a new tick listener is registered', function () {
it('does not set bounds based on current value', function () {
api.setClock('mts');
expect(api.getBounds()).toEqual({});
});
it('does not set invalid clock', function () {
expect(function () {
api.setClock();
}).toThrow();
expect(function () {
api.setClock({});
}).toThrow();
expect(function () {
api.setClock('invalidClockKey');
}).toThrow();
});
it('[Legacy TimeAPI]: a new tick listener is registered', function () {
api.clock('mts', mockOffsets);
expect(mockTickSource.on).toHaveBeenCalledWith('tick', jasmine.any(Function));
});
it('a new tick listener is registered', function () {
api.setClock('mts', mockOffsets);
expect(mockTickSource.on).toHaveBeenCalledWith('tick', jasmine.any(Function));
});
it('listener of existing tick source is reregistered', function () {
api.clock('mts', mockOffsets);
api.clock('amts', mockOffsets);
expect(mockTickSource.off).toHaveBeenCalledWith('tick', jasmine.any(Function));
});
xit('Allows the active clock to be set and unset', function () {
it('[Legacy TimeAPI]: Allows the active clock to be set and unset', function () {
expect(api.clock()).toBeUndefined();
api.clock('mts', mockOffsets);
expect(api.clock()).toBeDefined();
// api.stopClock();
// expect(api.clock()).toBeUndefined();
// Unset the clock
api.stopClock();
expect(api.clock()).toBeUndefined();
});
it('Provides a default time context', () => {

View File

@ -20,26 +20,89 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import EventEmitter from 'EventEmitter';
import EventEmitter from 'eventemitter3';
import { FIXED_MODE_KEY, MODES, REALTIME_MODE_KEY, TIME_CONTEXT_EVENTS } from './constants.js';
/**
* @typedef {import('../../utils/clock/DefaultClock.js').default} Clock
*/
/**
* @typedef {import('./TimeAPI.js').TimeSystem} TimeSystem
*/
/**
* @typedef {Object} TimeConductorBounds
* @property {number} start The start time displayed by the time conductor
* in ms since epoch. Epoch determined by currently active time system
* @property {number} end The end time displayed by the time conductor in ms
* since epoch.
*/
/**
* Clock offsets are used to calculate temporal bounds when the system is
* ticking on a clock source.
*
* @typedef {Object} ClockOffsets
* @property {number} start A time span relative to the current value of the
* ticking clock, from which start bounds will be calculated. This value must
* be < 0. When a clock is active, bounds will be calculated automatically
* based on the value provided by the clock, and the defined clock offsets.
* @property {number} end A time span relative to the current value of the
* ticking clock, from which end bounds will be calculated. This value must
* be >= 0.
*/
/**
* @typedef {Object} ValidationResult
* @property {boolean} valid Result of the validation - true or false.
* @property {string} message An error message if valid is false.
*/
/**
* @typedef {'fixed' | 'realtime'} Mode The time conductor mode.
*/
/**
* @class TimeContext
* @extends EventEmitter
*/
class TimeContext extends EventEmitter {
constructor() {
super();
//The Time System
/**
* The time systems available to the TimeAPI.
* @type {Map<string, TimeSystem>}
*/
this.timeSystems = new Map();
/**
* The currently applied time system.
* @type {TimeSystem | undefined}
*/
this.system = undefined;
/**
* The clocks available to the TimeAPI.
* @type {Map<string, import('../../utils/clock/DefaultClock.js').default>}
*/
this.clocks = new Map();
/**
* The current bounds of the time conductor.
* @type {TimeConductorBounds}
*/
this.boundsVal = {
start: undefined,
end: undefined
};
/**
* The currently active clock.
* @type {Clock | undefined}
*/
this.activeClock = undefined;
this.offsets = undefined;
this.mode = undefined;
@ -51,11 +114,9 @@ class TimeContext extends EventEmitter {
/**
* Get or set the time system of the TimeAPI.
* @param {TimeSystem | string} timeSystemOrKey
* @param {module:openmct.TimeAPI~TimeConductorBounds} bounds
* @fires module:openmct.TimeAPI~timeSystem
* @param {TimeConductorBounds} bounds
* @returns {TimeSystem} The currently applied time system
* @memberof module:openmct.TimeAPI#
* @method timeSystem
* @deprecated This method is deprecated. Use "getTimeSystem" and "setTimeSystem" instead.
*/
timeSystem(timeSystemOrKey, bounds) {
this.#warnMethodDeprecated('"timeSystem"', '"getTimeSystem" and "setTimeSystem"');
@ -101,11 +162,8 @@ class TimeContext extends EventEmitter {
* The time system used by the time
* conductor has changed. A change in Time System will always be
* followed by a bounds event specifying new query bounds.
*
* @event module:openmct.TimeAPI~timeSystem
* @property {TimeSystem} The value of the currently applied
* Time System
* */
* @type {TimeSystem}
*/
const system = this.#copy(this.system);
this.emit('timeSystem', system);
this.emit(TIME_CONTEXT_EVENTS.timeSystemChanged, system);
@ -118,21 +176,11 @@ class TimeContext extends EventEmitter {
return this.system;
}
/**
* Clock offsets are used to calculate temporal bounds when the system is
* ticking on a clock source.
*
* @typedef {Object} ValidationResult
* @property {boolean} valid Result of the validation - true or false.
* @property {string} message An error message if valid is false.
*/
/**
* Validate the given bounds. This can be used for pre-validation of bounds,
* for example by views validating user inputs.
* @param {TimeBounds} bounds The start and end time of the conductor.
* @param {TimeConductorBounds} bounds The start and end time of the conductor.
* @returns {ValidationResult} A validation error, or true if valid
* @memberof module:openmct.TimeAPI#
* @method validateBounds
*/
validateBounds(bounds) {
if (
@ -162,12 +210,10 @@ class TimeContext extends EventEmitter {
* Get or set the start and end time of the time conductor. Basic validation
* of bounds is performed.
*
* @param {module:openmct.TimeAPI~TimeConductorBounds} newBounds
* @param {TimeConductorBounds} [newBounds] The new bounds to set. If not provided, current bounds will be returned.
* @throws {Error} Validation error
* @fires module:openmct.TimeAPI~bounds
* @returns {module:openmct.TimeAPI~TimeConductorBounds}
* @memberof module:openmct.TimeAPI#
* @method bounds
* @returns {TimeConductorBounds} The current bounds of the time conductor.
* @deprecated This method is deprecated. Use "getBounds" and "setBounds" instead.
*/
bounds(newBounds) {
this.#warnMethodDeprecated('"bounds"', '"getBounds" and "setBounds"');
@ -183,7 +229,6 @@ class TimeContext extends EventEmitter {
/**
* The start time, end time, or both have been updated.
* @event bounds
* @memberof module:openmct.TimeAPI~
* @property {TimeConductorBounds} bounds The newly updated bounds
* @property {boolean} [tick] `true` if the bounds update was due to
* a "tick" event (ie. was an automatic update), false otherwise.
@ -200,9 +245,7 @@ class TimeContext extends EventEmitter {
* Validate the given offsets. This can be used for pre-validation of
* offsets, for example by views validating user inputs.
* @param {ClockOffsets} offsets The start and end offsets from a 'now' value.
* @returns { ValidationResult } A validation error, and true/false if valid or not
* @memberof module:openmct.TimeAPI#
* @method validateOffsets
* @returns {ValidationResult} A validation error, and true/false if valid or not
*/
validateOffsets(offsets) {
if (
@ -228,34 +271,13 @@ class TimeContext extends EventEmitter {
};
}
/**
* @typedef {Object} TimeBounds
* @property {number} start The start time displayed by the time conductor
* in ms since epoch. Epoch determined by currently active time system
* @property {number} end The end time displayed by the time conductor in ms
* since epoch.
* @memberof module:openmct.TimeAPI~
*/
/**
* Clock offsets are used to calculate temporal bounds when the system is
* ticking on a clock source.
*
* @typedef {Object} ClockOffsets
* @property {number} start A time span relative to the current value of the
* ticking clock, from which start bounds will be calculated. This value must
* be < 0. When a clock is active, bounds will be calculated automatically
* based on the value provided by the clock, and the defined clock offsets.
* @property {number} end A time span relative to the current value of the
* ticking clock, from which end bounds will be calculated. This value must
* be >= 0.
*/
/**
* Get or set the currently applied clock offsets. If no parameter is provided,
* the current value will be returned. If provided, the new value will be
* used as the new clock offsets.
* @param {ClockOffsets} offsets
* @returns {ClockOffsets}
* @param {ClockOffsets} [offsets] The new clock offsets to set. If not provided, current offsets will be returned.
* @returns {ClockOffsets} The current clock offsets.
* @deprecated This method is deprecated. Use "getClockOffsets" and "setClockOffsets" instead.
*/
clockOffsets(offsets) {
this.#warnMethodDeprecated('"clockOffsets"', '"getClockOffsets" and "setClockOffsets"');
@ -293,6 +315,7 @@ class TimeContext extends EventEmitter {
* Stop following the currently active clock. This will
* revert all views to showing a static time frame defined by the current
* bounds.
* @deprecated This method is deprecated.
*/
stopClock() {
this.#warnMethodDeprecated('"stopClock"');
@ -304,12 +327,14 @@ class TimeContext extends EventEmitter {
* Set the active clock. Tick source will be immediately subscribed to
* and ticking will begin. Offsets from 'now' must also be provided.
*
* @param {Clock || string} keyOrClock The clock to activate, or its key
* @param {string|Clock} keyOrClock The clock to activate, or its key
* @param {ClockOffsets} offsets on each tick these will be used to calculate
* the start and end bounds. This maintains a sliding time window of a fixed
* width that automatically updates.
* @fires module:openmct.TimeAPI~clock
* @return {Clock} the currently active clock;
* (Legacy) Emits a "clock" event with the new clock.
* Emits a "clockChanged" event with the new clock.
* @return {Clock|undefined} the currently active clock; undefined if in fixed mode
* @deprecated This method is deprecated. Use "getClock" and "setClock" instead.
*/
clock(keyOrClock, offsets) {
this.#warnMethodDeprecated('"clock"', '"getClock" and "setClock"');
@ -339,7 +364,6 @@ class TimeContext extends EventEmitter {
/**
* The active clock has changed.
* @event clock
* @memberof module:openmct.TimeAPI~
* @property {Clock} clock The newly activated clock, or undefined
* if the system is no longer following a clock source
*/
@ -361,7 +385,7 @@ class TimeContext extends EventEmitter {
}
/**
* Update bounds based on provided time and current offsets
* Update bounds based on provided time and current offsets.
* @param {number} timestamp A time from which bounds will be calculated
* using current offsets.
*/
@ -385,8 +409,6 @@ class TimeContext extends EventEmitter {
/**
* Get the timestamp of the current clock
* @returns {number} current timestamp of current clock regardless of mode
* @memberof module:openmct.TimeAPI#
* @method now
*/
now() {
@ -396,8 +418,6 @@ class TimeContext extends EventEmitter {
/**
* Get the time system of the TimeAPI.
* @returns {TimeSystem} The currently applied time system
* @memberof module:openmct.TimeAPI#
* @method getTimeSystem
*/
getTimeSystem() {
return this.system;
@ -405,12 +425,9 @@ class TimeContext extends EventEmitter {
/**
* Set the time system of the TimeAPI.
* @param {TimeSystem | string} timeSystemOrKey
* @param {module:openmct.TimeAPI~TimeConductorBounds} bounds
* @fires module:openmct.TimeAPI~timeSystem
* @returns {TimeSystem} The currently applied time system
* @memberof module:openmct.TimeAPI#
* @method setTimeSystem
* Emits a "timeSystem" event with the new time system.
* @param {TimeSystem | string} timeSystemOrKey The time system to set, or its key
* @param {TimeConductorBounds} [bounds] Optional bounds to set
*/
setTimeSystem(timeSystemOrKey, bounds) {
if (timeSystemOrKey === undefined) {
@ -441,7 +458,6 @@ class TimeContext extends EventEmitter {
* conductor has changed. A change in Time System will always be
* followed by a bounds event specifying new query bounds.
*
* @event module:openmct.TimeAPI~timeSystem
* @property {TimeSystem} The value of the currently applied
* Time System
* */
@ -456,9 +472,7 @@ class TimeContext extends EventEmitter {
/**
* Get the start and end time of the time conductor. Basic validation
* of bounds is performed.
* @returns {module:openmct.TimeAPI~TimeConductorBounds}
* @memberof module:openmct.TimeAPI#
* @method bounds
* @returns {TimeConductorBounds} The current bounds of the time conductor.
*/
getBounds() {
//Return a copy to prevent direct mutation of time conductor bounds.
@ -469,12 +483,8 @@ class TimeContext extends EventEmitter {
* Set the start and end time of the time conductor. Basic validation
* of bounds is performed.
*
* @param {module:openmct.TimeAPI~TimeConductorBounds} newBounds
* @throws {Error} Validation error
* @fires module:openmct.TimeAPI~bounds
* @returns {module:openmct.TimeAPI~TimeConductorBounds}
* @memberof module:openmct.TimeAPI#
* @method bounds
* @param {TimeConductorBounds} newBounds The new bounds to set.
* @throws {Error} Validation error if bounds are invalid
*/
setBounds(newBounds) {
const validationResult = this.validateBounds(newBounds);
@ -487,7 +497,6 @@ class TimeContext extends EventEmitter {
/**
* The start time, end time, or both have been updated.
* @event bounds
* @memberof module:openmct.TimeAPI~
* @property {TimeConductorBounds} bounds The newly updated bounds
* @property {boolean} [tick] `true` if the bounds update was due to
* a "tick" event (i.e. was an automatic update), false otherwise.
@ -498,7 +507,7 @@ class TimeContext extends EventEmitter {
/**
* Get the active clock.
* @return {Clock} the currently active clock;
* @return {Clock|undefined} the currently active clock; undefined if in fixed mode.
*/
getClock() {
return this.activeClock;
@ -509,9 +518,7 @@ class TimeContext extends EventEmitter {
* and the currently ticking will begin.
* Offsets from 'now', if provided, will be used to set realtime mode offsets
*
* @param {Clock || string} keyOrClock The clock to activate, or its key
* @fires module:openmct.TimeAPI~clock
* @return {Clock} the currently active clock;
* @param {string|Clock} keyOrClock The clock to activate, or its key
*/
setClock(keyOrClock) {
let clock;
@ -540,7 +547,7 @@ class TimeContext extends EventEmitter {
* The active clock has changed.
* @event clock
* @memberof module:openmct.TimeAPI~
* @property {Clock} clock The newly activated clock, or undefined
* @property {TimeContext} clock The newly activated clock, or undefined
* if the system is no longer following a clock source
*/
this.emit(TIME_CONTEXT_EVENTS.clockChanged, this.activeClock);
@ -549,7 +556,7 @@ class TimeContext extends EventEmitter {
/**
* Get the current mode.
* @return {Mode} the current mode;
* @return {Mode} the current mode
*/
getMode() {
return this.mode;
@ -559,9 +566,9 @@ class TimeContext extends EventEmitter {
* Set the mode to either fixed or realtime.
*
* @param {Mode} mode The mode to activate
* @param {TimeBounds | ClockOffsets} offsetsOrBounds A time window of a fixed width
* @param {TimeConductorBounds|ClockOffsets} offsetsOrBounds A time window of a fixed width
* @fires module:openmct.TimeAPI~clock
* @return {Mode} the currently active mode;
* @return {Mode | undefined} the currently active mode
*/
setMode(mode, offsetsOrBounds) {
if (!mode) {
@ -577,7 +584,6 @@ class TimeContext extends EventEmitter {
/**
* The active mode has changed.
* @event modeChanged
* @memberof module:openmct.TimeAPI~
* @property {Mode} mode The newly activated mode
*/
this.emit(TIME_CONTEXT_EVENTS.modeChanged, this.#copy(this.mode));
@ -610,18 +616,15 @@ class TimeContext extends EventEmitter {
/**
* Get the currently applied clock offsets.
* @returns {ClockOffsets}
* @returns {ClockOffsets} The current clock offsets.
*/
getClockOffsets() {
return this.offsets;
}
/**
* Set the currently applied clock offsets. If no parameter is provided,
* the current value will be returned. If provided, the new value will be
* used as the new clock offsets.
* @param {ClockOffsets} offsets
* @returns {ClockOffsets}
* Set the currently applied clock offsets.
* @param {ClockOffsets} offsets The new clock offsets to set.
*/
setClockOffsets(offsets) {
const validationResult = this.validateOffsets(offsets);
@ -642,13 +645,20 @@ class TimeContext extends EventEmitter {
/**
* Event that is triggered when clock offsets change.
* @event clockOffsets
* @memberof module:openmct.TimeAPI~
* @property {ClockOffsets} clockOffsets The newly activated clock
* offsets.
*/
this.emit(TIME_CONTEXT_EVENTS.clockOffsetsChanged, this.#copy(offsets));
}
/**
* Prints a warning to the console when a deprecated method is used. Limits
* the number of times a warning is printed per unique method and newMethod
* combination.
* @param {string} method the deprecated method
* @param {string} [newMethod] the new method to use instead
* @returns
*/
#warnMethodDeprecated(method, newMethod) {
const MAX_CALLS = 1; // Only warn once per unique method and newMethod combination
@ -673,6 +683,11 @@ class TimeContext extends EventEmitter {
console.warn(message);
}
/**
* Deep copy an object.
* @param {object} object The object to copy
* @returns {object} The copied object
*/
#copy(object) {
return JSON.parse(JSON.stringify(object));
}

View File

@ -212,7 +212,7 @@ export default {
this.openmct.time.on('timeSystem', this.updateTimeSystem);
this.timestampKey = this.openmct.time.timeSystem().key;
this.timestampKey = this.openmct.time.getTimeSystem().key;
this.valueMetadata = undefined;

View File

@ -61,7 +61,7 @@ export default class URLTimeSettingsSynchronizer {
TIME_EVENTS.forEach((event) => {
this.openmct.time.on(event, this.setUrlFromTimeApi);
});
this.openmct.time.on('bounds', this.updateBounds);
this.openmct.time.on('boundsChanged', this.updateBounds);
}
destroy() {
@ -73,7 +73,7 @@ export default class URLTimeSettingsSynchronizer {
TIME_EVENTS.forEach((event) => {
this.openmct.time.off(event, this.setUrlFromTimeApi);
});
this.openmct.time.off('bounds', this.updateBounds);
this.openmct.time.off('boundsChanged', this.updateBounds);
}
updateTimeSettings() {

View File

@ -115,11 +115,11 @@ export default {
this.followTimeContext();
},
followTimeContext() {
this.timeContext.on('bounds', this.refreshData);
this.timeContext.on('boundsChanged', this.refreshData);
},
stopFollowingTimeContext() {
if (this.timeContext) {
this.timeContext.off('bounds', this.refreshData);
this.timeContext.off('boundsChanged', this.refreshData);
}
},
addToComposition(telemetryObject) {
@ -253,7 +253,7 @@ export default {
};
},
getOptions() {
const { start, end } = this.timeContext.bounds();
const { start, end } = this.timeContext.getBounds();
return {
end,
@ -372,13 +372,13 @@ export default {
this.setTrace(key, telemetryObject.name, axisMetadata, xValues, yValues);
},
isDataInTimeRange(datum, key, telemetryObject) {
const timeSystemKey = this.timeContext.timeSystem().key;
const timeSystemKey = this.timeContext.getTimeSystem().key;
const metadata = this.openmct.telemetry.getMetadata(telemetryObject);
let metadataValue = metadata.value(timeSystemKey) || { key: timeSystemKey };
let currentTimestamp = this.parse(key, metadataValue.key, datum);
return currentTimestamp && this.timeContext.bounds().end >= currentTimestamp;
return currentTimestamp && this.timeContext.getBounds().end >= currentTimestamp;
},
format(telemetryObjectKey, metadataKey, data) {
const formats = this.telemetryObjectFormats[telemetryObjectKey];

View File

@ -105,11 +105,11 @@ export default {
this.followTimeContext();
},
followTimeContext() {
this.timeContext.on('bounds', this.reloadTelemetryOnBoundsChange);
this.timeContext.on('boundsChanged', this.reloadTelemetryOnBoundsChange);
},
stopFollowingTimeContext() {
if (this.timeContext) {
this.timeContext.off('bounds', this.reloadTelemetryOnBoundsChange);
this.timeContext.off('boundsChanged', this.reloadTelemetryOnBoundsChange);
}
},
addToComposition(telemetryObject) {
@ -306,7 +306,7 @@ export default {
this.trace = [trace];
},
getTimestampForDatum(datum, key, telemetryObject) {
const timeSystemKey = this.timeContext.timeSystem().key;
const timeSystemKey = this.timeContext.getTimeSystem().key;
const metadata = this.openmct.telemetry.getMetadata(telemetryObject);
let metadataValue = metadata.value(timeSystemKey) || { format: timeSystemKey };
@ -327,7 +327,7 @@ export default {
return formats[metadataKey].parse(datum);
},
getOptions() {
const { start, end } = this.timeContext.bounds();
const { start, end } = this.timeContext.getBounds();
return {
end,

View File

@ -245,7 +245,7 @@ export default class Condition extends EventEmitter {
latestTimestamp,
updatedCriterion.data,
this.timeSystems,
this.openmct.time.timeSystem()
this.openmct.time.getTimeSystem()
);
this.conditionManager.updateCurrentCondition(latestTimestamp);
}
@ -309,7 +309,7 @@ export default class Condition extends EventEmitter {
latestTimestamp,
data,
this.timeSystems,
this.openmct.time.timeSystem()
this.openmct.time.getTimeSystem()
);
});

View File

@ -46,14 +46,6 @@ export default class ConditionManager extends EventEmitter {
applied: false
};
this.initialize();
this.stopObservingForChanges = this.openmct.objects.observe(
this.conditionSetDomainObject,
'*',
(newDomainObject) => {
this.conditionSetDomainObject = newDomainObject;
}
);
}
async requestLatestValue(endpoint) {
@ -113,7 +105,7 @@ export default class ConditionManager extends EventEmitter {
{},
{},
this.timeSystems,
this.openmct.time.timeSystem()
this.openmct.time.getTimeSystem()
);
this.updateConditionResults({ id: id });
this.updateCurrentCondition(latestTimestamp);
@ -383,7 +375,7 @@ export default class ConditionManager extends EventEmitter {
latestTimestamp,
data,
this.timeSystems,
this.openmct.time.timeSystem()
this.openmct.time.getTimeSystem()
);
});
@ -518,10 +510,6 @@ export default class ConditionManager extends EventEmitter {
Object.values(this.subscriptions).forEach((unsubscribe) => unsubscribe());
delete this.subscriptions;
if (this.stopObservingForChanges) {
this.stopObservingForChanges();
}
this.conditions.forEach((condition) => {
condition.destroy();
});

View File

@ -41,7 +41,7 @@ export default class StyleRuleManager extends EventEmitter {
});
this.initialize(styleConfigurationWithNoSelection);
if (styleConfiguration.conditionSetIdentifier) {
this.openmct.time.on('bounds', this.refreshData);
this.openmct.time.on('boundsChanged', this.refreshData);
this.subscribeToConditionSet();
} else {
this.applyStaticStyle();
@ -216,7 +216,7 @@ export default class StyleRuleManager extends EventEmitter {
}
if (!skipEventListeners) {
this.openmct.time.off('bounds', this.refreshData);
this.openmct.time.off('boundsChanged', this.refreshData);
this.openmct.editor.off('isEditing', this.toggleSubscription);
}

View File

@ -21,7 +21,11 @@
-->
<template>
<section id="conditionCollection" :class="{ 'is-expanded': expanded }">
<section
id="conditionCollection"
:class="{ 'is-expanded': expanded }"
aria-label="Condition Set Condition Collection"
>
<div class="c-cs__header c-section__header">
<span
class="c-disclosure-triangle c-tree__item__view-control is-enabled"

View File

@ -24,6 +24,7 @@
<div
class="c-condition-h"
:class="{ 'is-drag-target': draggingOver }"
aria-label="Condition Set Condition"
@dragover.prevent
@drop.prevent="dropCondition($event, conditionIndex)"
@dragenter="dragEnter($event, conditionIndex)"
@ -83,6 +84,7 @@
<input
v-model="condition.configuration.name"
class="t-condition-input__name"
aria-label="Condition Name Input"
type="text"
@change="persist"
/>
@ -91,7 +93,11 @@
<span class="c-cdef__label">Output</span>
<span class="c-cdef__controls">
<span class="c-cdef__control">
<select v-model="selectedOutputSelection" @change="setOutputValue">
<select
v-model="selectedOutputSelection"
aria-label="Condition Output Type"
@change="setOutputValue"
>
<option v-for="option in outputOptions" :key="option" :value="option">
{{ initCap(option) }}
</option>
@ -101,6 +107,7 @@
<input
v-if="selectedOutputSelection === outputOptions[2]"
v-model="condition.configuration.output"
aria-label="Condition Output String"
class="t-condition-name-input"
type="text"
@change="persist"

View File

@ -21,7 +21,7 @@
-->
<template>
<div class="c-cs" :class="{ 'is-stale': isStale }">
<div class="c-cs" :class="{ 'is-stale': isStale }" aria-label="Condition Set">
<section class="c-cs__current-output c-section">
<div class="c-cs__content c-cs__current-output-value">
<span class="c-cs__current-output-value__label">Current Output</span>

View File

@ -21,7 +21,12 @@
-->
<template>
<section v-show="isEditing" id="test-data" :class="{ 'is-expanded': expanded }">
<section
v-show="isEditing"
id="test-data"
:class="{ 'is-expanded': expanded }"
aria-label="Condition Set Test Data"
>
<div class="c-cs__header c-section__header">
<span
class="c-disclosure-triangle c-tree__item__view-control is-enabled"

View File

@ -227,7 +227,7 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
return Promise.all(telemetryRequests).then((telemetryRequestsResults) => {
let latestTimestamp;
const timeSystems = this.openmct.time.getAllTimeSystems();
const timeSystem = this.openmct.time.timeSystem();
const timeSystem = this.openmct.time.getTimeSystem();
telemetryRequestsResults.forEach((results, index) => {
const latestDatum =

View File

@ -280,7 +280,7 @@ export default {
await this.$nextTick();
},
formattedValueForCopy() {
const timeFormatterKey = this.openmct.time.timeSystem().key;
const timeFormatterKey = this.openmct.time.getTimeSystem().key;
const timeFormatter = this.formats[timeFormatterKey];
const unit = this.unit ? ` ${this.unit}` : '';

View File

@ -18,6 +18,7 @@
}
&__value {
@include telemetryView();
@include isLimit();
}

View File

@ -45,7 +45,7 @@
@object-drop-to="moveOrCreateNewFrame"
/>
<div role="row" class="c-fl-container__frames-holder">
<div role="row" class="c-fl-container__frames-holder" :class="flexLayoutCssClass">
<template v-for="(frame, i) in frames" :key="frame.id">
<frame-component
class="c-fl-container__frame"
@ -118,6 +118,9 @@ export default {
},
emits: ['new-frame', 'move-frame', 'persist'],
computed: {
flexLayoutCssClass() {
return this.rowsLayout ? '--layout-rows' : '--layout-cols';
},
frames() {
return this.container.frames;
},

View File

@ -30,10 +30,8 @@
<div
class="c-fl__container-holder u-style-receiver js-style-receiver"
:class="{
'c-fl--rows': rowsLayout === true
}"
:aria-label="`Flexible Layout ${rowsLayout ? 'Row' : 'Column'}`"
:class="flexLayoutCssClass"
:aria-label="`Flexible Layout ${rowsLayout ? 'Rows' : 'Columns'}`"
>
<template v-for="(container, index) in containers" :key="`component-${container.id}`">
<drop-hint
@ -45,7 +43,6 @@
/>
<container-component
class="c-fl__container"
:index="index"
:container="container"
:rows-layout="rowsLayout"
@ -148,15 +145,11 @@ export default {
};
},
computed: {
layoutDirectionStr() {
if (this.rowsLayout) {
return 'Rows';
} else {
return 'Columns';
}
},
allContainersAreEmpty() {
return this.containers.every((container) => container.frames.length === 0);
},
flexLayoutCssClass() {
return this.rowsLayout ? 'c-fl--rows' : 'c-fl--cols';
}
},
created() {

View File

@ -7,7 +7,6 @@
$majorOffset: 35%;
content: '';
display: block;
position: absolute;
@include grippy($c: $editFrameSelectedMovebarColorFg, $dir: $dir);
@if $dir == 'x' {
top: $minorOffset;
@ -35,18 +34,15 @@
flex: 1 1 100%; // Must be 100% to work
overflow: auto;
// Columns by default
// Controls layout of c-fl__container(s)
&[class*='--cols'] {
flex-direction: row;
> * + * {
margin-left: 1px;
column-gap: 1px;
}
&[class*='--rows'] {
flex-direction: column;
> * + * {
margin-left: 0;
margin-top: 1px;
}
row-gap: 1px;
}
}
@ -119,10 +115,18 @@
&__frames-holder {
display: flex;
flex: 1 1 100%; // Must be 100% to work
flex-direction: column; // Default
flex-direction: row; // Default
align-content: stretch;
align-items: stretch;
overflow: hidden; // This sucks, but doing in the short-term
&.--layout-cols {
flex-direction: column !important;
}
&.--layout-rows {
flex-direction: row !important;
}
}
.is-editing & {

View File

@ -61,13 +61,13 @@ function ToolbarProvider(openmct) {
options: [
{
value: true,
icon: 'icon-columns',
title: 'Columns layout'
icon: 'icon-rows',
title: 'Switch to rows layout'
},
{
value: false,
icon: 'icon-rows',
title: 'Rows layout'
icon: 'icon-columns',
title: 'Switch to columns layout'
}
]
};

View File

@ -164,7 +164,7 @@ describe('Gauge plugin', () => {
});
spyOn(openmct.telemetry, 'getLimits').and.returnValue({ limits: () => Promise.resolve() });
spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([randomValue]));
spyOn(openmct.time, 'bounds').and.returnValue({
spyOn(openmct.time, 'getBounds').and.returnValue({
start: 1000,
end: 5000
});
@ -306,7 +306,7 @@ describe('Gauge plugin', () => {
});
spyOn(openmct.telemetry, 'getLimits').and.returnValue({ limits: () => Promise.resolve() });
spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([randomValue]));
spyOn(openmct.time, 'bounds').and.returnValue({
spyOn(openmct.time, 'getBounds').and.returnValue({
start: 1000,
end: 5000
});
@ -448,7 +448,7 @@ describe('Gauge plugin', () => {
});
spyOn(openmct.telemetry, 'getLimits').and.returnValue({ limits: () => Promise.resolve() });
spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([randomValue]));
spyOn(openmct.time, 'bounds').and.returnValue({
spyOn(openmct.time, 'getBounds').and.returnValue({
start: 1000,
end: 5000
});
@ -763,7 +763,7 @@ describe('Gauge plugin', () => {
})
});
spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([randomValue]));
spyOn(openmct.time, 'bounds').and.returnValue({
spyOn(openmct.time, 'getBounds').and.returnValue({
start: 1000,
end: 5000
});

View File

@ -363,7 +363,7 @@ export default {
rangeLow: gaugeController.min,
gaugeType: gaugeController.gaugeType,
showUnits: gaugeController.showUnits,
activeTimeSystem: this.openmct.time.timeSystem(),
activeTimeSystem: this.openmct.time.getTimeSystem(),
units: ''
};
},
@ -545,7 +545,7 @@ export default {
this.composition.load();
this.openmct.time.on('bounds', this.refreshData);
this.openmct.time.on('boundsChanged', this.refreshData);
this.openmct.time.on('timeSystem', this.setTimeSystem);
this.setupClockChangedEvent((domainObject) => {
@ -561,7 +561,7 @@ export default {
this.unsubscribe();
}
this.openmct.time.off('bounds', this.refreshData);
this.openmct.time.off('boundsChanged', this.refreshData);
this.openmct.time.off('timeSystem', this.setTimeSystem);
},
methods: {
@ -726,7 +726,7 @@ export default {
return;
}
const { start, end } = this.openmct.time.bounds();
const { start, end } = this.openmct.time.getBounds();
const parsedValue = this.timeFormatter.parse(this.datum);
const beforeStartOfBounds = parsedValue < start;

View File

@ -84,11 +84,12 @@ $meterNeedleBorderRadius: 5px;
fill: $colorGaugeValue;
}
&__needle-value {
fill: $colorGaugeValue;
fill: $colorGaugeNeedle;
}
&__current-value-text {
fill: $colorGaugeTextValue;
font-family: $heroFont;
font-family: $numericFont;
}
&__units-text,
@ -125,7 +126,8 @@ $meterNeedleBorderRadius: 5px;
// Filled area
position: absolute;
background: $colorGaugeValue;
z-index: 1;
box-shadow: $gaugeMeterValueShadow 0px 2px 10px 1px;
//z-index: 3;
}
&__value-needle {
@ -135,6 +137,7 @@ $meterNeedleBorderRadius: 5px;
content: '';
display: block;
background: $colorGaugeValue;
}
}
@ -158,7 +161,7 @@ $meterNeedleBorderRadius: 5px;
&__current-value-text {
fill: $colorGaugeTextValue;
font-family: $heroFont;
font-family: $numericFont;
}
.c-gauge__curval {

View File

@ -49,7 +49,7 @@ export default {
mixins: [imageryData],
inject: ['openmct', 'domainObject', 'objectPath'],
data() {
let timeSystem = this.openmct.time.timeSystem();
let timeSystem = this.openmct.time.getTimeSystem();
this.metadata = {};
this.requestCount = 0;
@ -109,12 +109,12 @@ export default {
this.stopFollowingTimeContext();
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
this.timeContext.on('timeSystem', this.setScaleAndPlotImagery);
this.timeContext.on('bounds', this.updateViewBounds);
this.timeContext.on('boundsChanged', this.updateViewBounds);
},
stopFollowingTimeContext() {
if (this.timeContext) {
this.timeContext.off('timeSystem', this.setScaleAndPlotImagery);
this.timeContext.off('bounds', this.updateViewBounds);
this.timeContext.off('boundsChanged', this.updateViewBounds);
}
},
expand(imageTimestamp) {
@ -148,10 +148,10 @@ export default {
return clientWidth;
},
updateViewBounds(bounds, isTick) {
this.viewBounds = this.timeContext.bounds();
this.viewBounds = this.timeContext.getBounds();
if (this.timeSystem === undefined) {
this.timeSystem = this.timeContext.timeSystem();
this.timeSystem = this.timeContext.getTimeSystem();
}
this.setScaleAndPlotImagery(this.timeSystem, !isTick);
@ -216,7 +216,7 @@ export default {
}
if (timeSystem === undefined) {
timeSystem = this.timeContext.timeSystem();
timeSystem = this.timeContext.getTimeSystem();
}
if (timeSystem.isUTCBased) {

View File

@ -44,7 +44,7 @@ export default class RelatedTelemetry {
this.keys = telemetryKeys;
this._timeFormatter = undefined;
this._timeSystemChange(this.timeContext.timeSystem());
this._timeSystemChange(this.timeContext.getTimeSystem());
// grab related telemetry metadata
for (let key of this.keys) {
@ -110,10 +110,10 @@ export default class RelatedTelemetry {
// and set bounds.
ephemeralContext.resetContext();
const newBounds = {
start: this.timeContext.bounds().start,
start: this.timeContext.getBounds().start,
end: this._parseTime(datum)
};
ephemeralContext.bounds(newBounds);
ephemeralContext.setBounds(newBounds);
const options = {
start: newBounds.start,

View File

@ -171,7 +171,7 @@ export default {
this.bounds = bounds; // setting bounds for ImageryView watcher
},
timeSystemChanged() {
this.timeSystem = this.timeContext.timeSystem();
this.timeSystem = this.timeContext.getTimeSystem();
this.timeKey = this.timeSystem.key;
this.timeFormatter = this.getFormatter(this.timeKey);
this.durationFormatter = this.getFormatter(

View File

@ -144,24 +144,132 @@ export default class ImportAsJSONAction {
return Array.from(new Set([...objectIdentifiers, ...itemObjectReferences]));
}
/**
* @private
* @param {Object} tree
* @param {string} namespace
* @returns {Object}
*/
_generateNewIdentifiers(tree, newNamespace) {
// For each domain object in the file, generate new ID, replace in tree
Object.keys(tree.openmct).forEach((domainObjectId) => {
const oldId = parseKeyString(domainObjectId);
/**
* Generates a map of old IDs to new IDs for efficient lookup during tree walking.
* This function considers cases where original namespaces are blank and updates those IDs as well.
*
* @param {Object} tree - The object tree containing the old IDs.
* @param {string} newNamespace - The namespace for the new IDs.
* @returns {Object} A map of old IDs to new IDs.
*/
_generateIdMap(tree, newNamespace) {
const idMap = {};
const keys = Object.keys(tree.openmct);
for (const oldIdKey of keys) {
const oldId = parseKeyString(oldIdKey);
const newId = {
namespace: newNamespace,
key: uuid()
};
tree = this._rewriteId(oldId, newId, tree);
}, this);
const newIdKeyString = this.openmct.objects.makeKeyString(newId);
// Update the map with the old and new ID key strings.
idMap[oldIdKey] = newIdKeyString;
// If the old namespace is blank, also map the non-namespaced ID.
if (!oldId.namespace) {
const nonNamespacedOldIdKey = oldId.key;
idMap[nonNamespacedOldIdKey] = newIdKeyString;
}
}
return idMap;
}
/**
* Walks through the object tree and updates IDs according to the provided ID map.
* @param {Object} obj - The current object being visited in the tree.
* @param {Object} idMap - A map of old IDs to new IDs for rewriting.
* @param {Object} importDialog - Optional progress dialog for import.
* @returns {Promise<Object>} The object with updated IDs.
*/
async _walkAndRewriteIds(obj, idMap, importDialog) {
// How many rewrites to do before yielding to the event loop
const UI_UPDATE_INTERVAL = 300;
// The percentage of the progress dialog to allocate to rewriting IDs
const PERCENT_OF_DIALOG = 80;
if (obj === null || obj === undefined) {
return obj;
}
if (typeof obj === 'string') {
const possibleId = idMap[obj];
if (possibleId) {
return possibleId;
} else {
return obj;
}
}
if (Object.hasOwn(obj, 'key') && Object.hasOwn(obj, 'namespace')) {
const oldId = this.openmct.objects.makeKeyString(obj);
const possibleId = idMap[oldId];
if (possibleId) {
const newIdParts = possibleId.split(':');
if (newIdParts.length >= 2) {
// new ID is namespaced, so update both the namespace and key
obj.namespace = newIdParts[0];
obj.key = newIdParts[1];
} else {
// old ID was not namespaced, so update the key only
obj.namespace = '';
obj.key = newIdParts[0];
}
}
return obj;
}
if (Array.isArray(obj)) {
for (let i = 0; i < obj.length; i++) {
obj[i] = await this._walkAndRewriteIds(obj[i], idMap); // Process each item in the array
}
return obj;
}
if (typeof obj === 'object') {
const newObj = {};
const keys = Object.keys(obj);
let processedCount = 0;
for (const key of keys) {
const value = obj[key];
const possibleId = idMap[key];
const newKey = possibleId || key;
newObj[newKey] = await this._walkAndRewriteIds(value, idMap);
// Optionally update the importDialog here, after each property has been processed
if (importDialog) {
processedCount++;
if (processedCount % UI_UPDATE_INTERVAL === 0) {
// yield to the event loop to allow the UI to update
await new Promise((resolve) => setTimeout(resolve, 0));
const percentPersisted = Math.ceil(PERCENT_OF_DIALOG * (processedCount / keys.length));
const message = `Rewriting ${processedCount} of ${keys.length} imported objects.`;
importDialog.updateProgress(percentPersisted, message);
}
}
}
return newObj;
}
// Return the input as-is for types that are not objects, strings, or arrays
return obj;
}
/**
* @private
* @param {Object} tree
* @returns {Promise<Object>}
*/
async _generateNewIdentifiers(tree, newNamespace, importDialog) {
const idMap = this._generateIdMap(tree, newNamespace);
tree.rootId = idMap[tree.rootId];
tree.openmct = await this._walkAndRewriteIds(tree.openmct, idMap, importDialog);
return tree;
}
/**
@ -170,9 +278,16 @@ export default class ImportAsJSONAction {
* @param {Object} objTree
*/
async _importObjectTree(domainObject, objTree) {
// make rewriting objects IDs 80% of the progress bar
const importDialog = this.openmct.overlays.progressDialog({
progressPerc: 0,
message: `Importing ${Object.keys(objTree.openmct).length} objects`,
iconClass: 'info',
title: 'Importing'
});
const objectsToCreate = [];
const namespace = domainObject.identifier.namespace;
const tree = this._generateNewIdentifiers(objTree, namespace);
const tree = await this._generateNewIdentifiers(objTree, namespace, importDialog);
const rootId = tree.rootId;
const rootObj = tree.openmct[rootId];
@ -182,11 +297,24 @@ export default class ImportAsJSONAction {
this._deepInstantiate(rootObj, tree.openmct, [], objectsToCreate);
try {
await Promise.all(objectsToCreate.map(this._instantiate, this));
let persistedObjects = 0;
// make saving objects objects 20% of the progress bar
await Promise.all(
objectsToCreate.map(async (objectToCreate) => {
persistedObjects++;
const percentPersisted =
Math.ceil(20 * (persistedObjects / objectsToCreate.length)) + 80;
const message = `Saving ${persistedObjects} of ${objectsToCreate.length} imported objects.`;
importDialog.updateProgress(percentPersisted, message);
await this._instantiate(objectToCreate);
})
);
} catch (error) {
this.openmct.notifications.error('Error saving objects');
throw error;
} finally {
importDialog.dismiss();
}
const compositionCollection = this.openmct.composition.get(domainObject);
@ -194,7 +322,8 @@ export default class ImportAsJSONAction {
this.openmct.objects.mutate(rootObj, 'location', domainObjectKeyString);
compositionCollection.add(rootObj);
} else {
const dialog = this.openmct.overlays.dialog({
importDialog.dismiss();
const cannotImportDialog = this.openmct.overlays.dialog({
iconClass: 'alert',
message: "We're sorry, but you cannot import that object type into this object.",
buttons: [
@ -202,7 +331,7 @@ export default class ImportAsJSONAction {
label: 'Ok',
emphasis: true,
callback: function () {
dialog.dismiss();
cannotImportDialog.dismiss();
}
}
]
@ -217,43 +346,7 @@ export default class ImportAsJSONAction {
_instantiate(model) {
return this.openmct.objects.save(model);
}
/**
* @private
* @param {Object} oldId
* @param {Object} newId
* @param {Object} tree
* @returns {Object}
*/
_rewriteId(oldId, newId, tree) {
let newIdKeyString = this.openmct.objects.makeKeyString(newId);
let oldIdKeyString = this.openmct.objects.makeKeyString(oldId);
const newTreeString = JSON.stringify(tree).replace(
new RegExp(oldIdKeyString, 'g'),
newIdKeyString
);
const newTree = JSON.parse(newTreeString, (key, value) => {
if (
value !== undefined &&
value !== null &&
Object.prototype.hasOwnProperty.call(value, 'key') &&
Object.prototype.hasOwnProperty.call(value, 'namespace')
) {
// first check if key is messed up from regex and contains a colon
// if it does, repair it
if (value.key.includes(':')) {
const splitKey = value.key.split(':');
value.key = splitKey[1];
value.namespace = splitKey[0];
}
// now check if we need to replace the id
if (value.key === oldId.key && value.namespace === oldId.namespace) {
return newId;
}
}
return value;
});
return newTree;
}
/**
* @private
* @param {Object} domainObject

View File

@ -111,7 +111,6 @@ describe('The import JSON action', function () {
});
it('protects against prototype pollution', (done) => {
spyOn(console, 'warn');
spyOn(openmct.forms, 'showForm').and.callFake(returnResponseWithPrototypePollution);
unObserve = openmct.objects.observe(folderObject, '*', callback);
@ -123,8 +122,6 @@ describe('The import JSON action', function () {
Object.prototype.hasOwnProperty.call(newObject, '__proto__') ||
Object.prototype.hasOwnProperty.call(Object.getPrototypeOf(newObject), 'toString');
// warning from openmct.objects.get
expect(console.warn).not.toHaveBeenCalled();
expect(hasPollutedProto).toBeFalse();
done();
@ -192,6 +189,12 @@ describe('The import JSON action', function () {
type: 'folder'
};
spyOn(openmct.objects, 'save').and.callFake((model) => Promise.resolve(model));
spyOn(openmct.overlays, 'progressDialog').and.callFake(() => {
return {
updateProgress: () => {},
dismiss: () => {}
};
});
try {
await importFromJSONAction.onSave(targetDomainObject, {
selectFile: { body: JSON.stringify(incomingObject) }

View File

@ -680,7 +680,7 @@ export default {
} else if (domainObjectData) {
// plain domain object
const objectPath = JSON.parse(domainObjectData);
const bounds = this.openmct.time.bounds();
const bounds = this.openmct.time.getBounds();
const snapshotMeta = {
bounds,
link: null,

View File

@ -275,10 +275,10 @@ export default {
}
const hash = this.embed.historicLink;
const bounds = this.openmct.time.bounds();
const bounds = this.openmct.time.getBounds();
const isTimeBoundChanged =
this.embed.bounds.start !== bounds.start || this.embed.bounds.end !== bounds.end;
const isFixedTimespanMode = !this.openmct.time.clock();
const isFixedTimespanMode = this.openmct.time.isFixed();
let message = '';
if (isTimeBoundChanged) {

View File

@ -31,7 +31,7 @@
@drop.capture="cancelEditMode"
@drop.prevent="dropOnEntry"
@click="selectAndEmitEntry($event, entry)"
@paste="addImageFromPaste"
@paste="handlePaste"
>
<div class="c-ne__time-and-content">
<div class="c-ne__time-and-creator-and-delete">
@ -368,8 +368,30 @@ export default {
}
},
methods: {
handlePaste(event) {
const clipboardItems = Array.from(
(event.clipboardData || event.originalEvent.clipboardData).items
);
const hasClipboardText = clipboardItems.some(
(clipboardItem) => clipboardItem.kind === 'string'
);
const clipboardImages = clipboardItems.filter(
(clipboardItem) => clipboardItem.kind === 'file' && clipboardItem.type.includes('image')
);
const hasClipboardImages = clipboardImages?.length > 0;
if (hasClipboardImages) {
if (hasClipboardText) {
console.warn('Image and text kinds found in paste. Only processing images.');
}
this.addImageFromPaste(clipboardImages, event);
} else if (hasClipboardText) {
this.addTextFromPaste(event);
}
},
async addNewEmbed(objectPath) {
const bounds = this.openmct.time.bounds();
const bounds = this.openmct.time.getBounds();
const snapshotMeta = {
bounds,
link: null,
@ -384,32 +406,34 @@ export default {
this.manageEmbedLayout();
},
async addImageFromPaste(event) {
const clipboardItems = Array.from(
(event.clipboardData || event.originalEvent.clipboardData).items
);
const hasImage = clipboardItems.some(
(clipboardItem) => clipboardItem.type.includes('image') && clipboardItem.kind === 'file'
);
// If the clipboard contained an image, prevent the paste event from reaching the textarea.
if (hasImage) {
addTextFromPaste(event) {
if (!this.editMode) {
event.preventDefault();
}
},
async addImageFromPaste(clipboardImages, event) {
event?.preventDefault();
let updated = false;
await Promise.all(
Array.from(clipboardItems).map(async (clipboardItem) => {
const isImage = clipboardItem.type.includes('image') && clipboardItem.kind === 'file';
if (isImage) {
const imageFile = clipboardItem.getAsFile();
Array.from(clipboardImages).map(async (clipboardImage) => {
const imageFile = clipboardImage.getAsFile();
const imageEmbed = await createNewImageEmbed(imageFile, this.openmct, imageFile?.name);
if (!this.entry.embeds) {
this.entry.embeds = [];
}
this.entry.embeds.push(imageEmbed);
}
updated = true;
})
);
if (updated) {
this.manageEmbedLayout();
this.timestampAndUpdate();
}
},
convertMarkDownToHtml(text = '') {
let markDownHtml = this.marked.parse(text, {

View File

@ -123,7 +123,7 @@ export default {
const objectPath = this.objectPath || this.openmct.router.path;
const link = this.isPreview ? this.getPreviewObjectLink() : window.location.hash;
const snapshotMeta = {
bounds: this.openmct.time.bounds(),
bounds: this.openmct.time.getBounds(),
link,
objectPath,
openmct: this.openmct

View File

@ -140,7 +140,7 @@ export function createNewImageEmbed(image, openmct, imageName = '') {
};
const embedMetaData = {
bounds: openmct.time.bounds(),
bounds: openmct.time.getBounds(),
link: null,
objectPath: null,
openmct,

View File

@ -46,7 +46,7 @@ export default class PainterroInstance {
this.config.id = this.elementId;
this.config.saveHandler = this.saveHandler.bind(this);
this.painterro = Painterro.default(this.config);
this.painterro = Painterro(this.config);
}
save(callback) {

View File

@ -710,7 +710,7 @@ class CouchObjectProvider {
this.objectQueue[key].pending = true;
const queued = this.objectQueue[key].dequeue();
let couchDocument = new CouchDocument(key, queued.model);
couchDocument.metadata.created = Date.now();
this.#enqueueForPersistence({
key,
document: couchDocument

View File

@ -196,10 +196,10 @@ export default {
this.followTimeContext();
},
followTimeContext() {
this.updateViewBounds(this.timeContext.bounds());
this.updateViewBounds(this.timeContext.getBounds());
this.timeContext.on('timeSystem', this.setScaleAndGenerateActivities);
this.timeContext.on('bounds', this.updateViewBounds);
this.timeContext.on('boundsChanged', this.updateViewBounds);
},
loadComposition() {
if (this.composition) {
@ -211,7 +211,7 @@ export default {
stopFollowingTimeContext() {
if (this.timeContext) {
this.timeContext.off('timeSystem', this.setScaleAndGenerateActivities);
this.timeContext.off('bounds', this.updateViewBounds);
this.timeContext.off('boundsChanged', this.updateViewBounds);
}
},
showReplacePlanDialog(domainObject) {
@ -319,7 +319,7 @@ export default {
}
if (this.timeSystem === null) {
this.timeSystem = this.openmct.time.timeSystem();
this.timeSystem = this.openmct.time.getTimeSystem();
}
this.setScaleAndGenerateActivities();
@ -344,7 +344,7 @@ export default {
}
if (!timeSystem) {
timeSystem = this.openmct.time.timeSystem();
timeSystem = this.openmct.time.getTimeSystem();
}
if (timeSystem.isUTCBased) {

View File

@ -116,7 +116,7 @@ export default {
}
},
setFormatters() {
let timeSystem = this.openmct.time.timeSystem();
let timeSystem = this.openmct.time.getTimeSystem();
this.timeFormatter = this.openmct.telemetry.getValueFormatter({
format: timeSystem.timeFormat
}).formatter;

View File

@ -661,7 +661,7 @@ export default {
this.offsetWidth = this.$parent.$refs.plotWrapper.offsetWidth;
this.startLoading();
const bounds = this.timeContext.bounds();
const bounds = this.timeContext.getBounds();
const options = {
size: this.$parent.$refs.plotWrapper.offsetWidth,
domain: this.config.xAxis.get('key'),
@ -1860,8 +1860,8 @@ export default {
},
showSynchronizeDialog() {
const isLocalClock = this.timeContext.clock();
if (isLocalClock !== undefined) {
const isFixedTimespanMode = this.timeContext.isFixed();
if (!isFixedTimespanMode) {
const message = `
This action will change the Time Conductor to Fixed Timespan mode with this plot view's current time bounds.
Do you want to continue?

View File

@ -614,7 +614,7 @@ export default {
const yAxisId = series.get('yAxisId') || mainYAxisId;
let offset = this.offset[yAxisId];
return new MCTChartAlarmLineSet(series, this, offset, this.openmct.time.bounds());
return new MCTChartAlarmLineSet(series, this, offset, this.openmct.time.getBounds());
},
pointSetForSeries(series) {
const mainYAxisId = this.config.yAxis.get('id');

View File

@ -140,7 +140,7 @@ export default class PlotSeries extends Model {
//this triggers Model.destroy which in turn triggers destroy methods for other classes.
super.destroy();
this.stopListening();
this.openmct.time.off('bounds', this.updateLimits);
this.openmct.time.off('boundsChanged', this.updateLimits);
if (this.unsubscribe) {
this.unsubscribe();
@ -171,7 +171,7 @@ export default class PlotSeries extends Model {
this.limitEvaluator = this.openmct.telemetry.limitEvaluator(options.domainObject);
this.limitDefinition = this.openmct.telemetry.limitDefinition(options.domainObject);
this.limits = [];
this.openmct.time.on('bounds', this.updateLimits);
this.openmct.time.on('boundsChanged', this.updateLimits);
this.removeMutationListener = this.openmct.objects.observe(
this.domainObject,
'name',

View File

@ -93,7 +93,7 @@ export default class XAxisModel extends Model {
* @override
*/
defaultModel(options) {
const bounds = options.openmct.time.bounds();
const bounds = options.openmct.time.getBounds();
const timeSystem = options.openmct.time.getTimeSystem();
const format = options.openmct.telemetry.getFormatter(timeSystem.timeFormat);

View File

@ -72,6 +72,7 @@ import SummaryWidget from './summaryWidget/plugin.js';
import Tabs from './tabs/plugin.js';
import TelemetryMean from './telemetryMean/plugin.js';
import TelemetryTablePlugin from './telemetryTable/plugin.js';
import DarkMatter from './themes/darkmatter.js';
import Espresso from './themes/espresso.js';
import Snow from './themes/snow.js';
import TimeConductorPlugin from './timeConductor/plugin.js';
@ -125,7 +126,6 @@ plugins.Plot = PlotPlugin;
plugins.BarChart = BarChartPlugin;
plugins.ScatterPlot = ScatterPlotPlugin;
plugins.TelemetryTable = TelemetryTablePlugin;
plugins.SummaryWidget = SummaryWidget;
plugins.TelemetryMean = TelemetryMean;
plugins.URLIndicator = URLIndicatorPlugin;
@ -145,6 +145,7 @@ plugins.OpenInNewTabAction = OpenInNewTabAction;
plugins.ReloadAction = ReloadAction;
plugins.ClearData = ClearData;
plugins.WebPage = WebPagePlugin;
plugins.DarkmatterTheme = DarkMatter;
plugins.Espresso = Espresso;
plugins.Snow = Snow;
plugins.Condition = ConditionPlugin;

View File

@ -134,7 +134,7 @@ export default class RemoteClock extends DefaultClock {
* @private
*/
_timeSystemChange() {
let timeSystem = this.openmct.time.timeSystem();
let timeSystem = this.openmct.time.getTimeSystem();
let timeKey = timeSystem.key;
let metadataValue = this.metadata.value(timeKey);
let timeFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
@ -155,7 +155,7 @@ export default class RemoteClock extends DefaultClock {
#waitForReady() {
const waitForInitialTick = (resolve) => {
if (this.lastTick > 0) {
const offsets = this.openmct.time.clockOffsets();
const offsets = this.openmct.time.getClockOffsets();
resolve({
start: this.lastTick + offsets.start,
end: this.lastTick + offsets.end

View File

@ -62,7 +62,7 @@ SummaryWidgetEvaluator.prototype.subscribe = function (callback) {
}
const updateCallback = function () {
const datum = this.evaluateState(realtimeStates, this.openmct.time.timeSystem().key);
const datum = this.evaluateState(realtimeStates, this.openmct.time.getTimeSystem().key);
if (datum) {
callback(datum);
}

View File

@ -611,11 +611,11 @@ describe('The Mean Telemetry Provider', function () {
}
function createMockTimeApi() {
return jasmine.createSpyObj('timeApi', ['timeSystem']);
return jasmine.createSpyObj('timeApi', ['getTimeSystem']);
}
function setTimeSystemTo(timeSystemKey) {
mockApi.time.timeSystem.and.returnValue({
mockApi.time.getTimeSystem.and.returnValue({
key: timeSystemKey
});
}

View File

@ -92,7 +92,7 @@ TelemetryAverager.prototype.calculateMean = function () {
* @private
*/
TelemetryAverager.prototype.setDomainKeyAndFormatter = function () {
const domainKey = this.timeAPI.timeSystem().key;
const domainKey = this.timeAPI.getTimeSystem().key;
if (domainKey !== this.domainKey) {
this.domainKey = domainKey;
this.domainFormatter = this.getFormatter(domainKey);

View File

@ -134,7 +134,7 @@ export default class TelemetryTable extends EventEmitter {
//If no persisted sort order, default to sorting by time system, descending.
sortOptions = sortOptions || {
key: this.openmct.time.timeSystem().key,
key: this.openmct.time.getTimeSystem().key,
direction: 'desc'
};
@ -171,6 +171,10 @@ export default class TelemetryTable extends EventEmitter {
this.removeTelemetryCollection(keyString);
let sortOptions = this.configuration.getConfiguration().sortOptions;
requestOptions.order =
sortOptions?.direction ?? (this.telemetryMode === 'performance' ? 'desc' : 'asc');
if (this.telemetryMode === 'performance') {
requestOptions.size = this.rowLimit;
requestOptions.enforceSize = true;

View File

@ -40,6 +40,8 @@ export default class TelemetryTableConfiguration extends EventEmitter {
'configuration',
this.objectMutated
);
this.notPersistable = !this.openmct.objects.isPersistable(this.domainObject.identifier);
}
getConfiguration() {
@ -52,14 +54,19 @@ export default class TelemetryTableConfiguration extends EventEmitter {
// anything that doesn't have a telemetryMode existed before the change and should
// take the properties of any passed in defaults or the defaults from the plugin
configuration.telemetryMode = configuration.telemetryMode ?? this.defaultOptions.telemetryMode;
configuration.persistModeChange =
configuration.persistModeChange ?? this.defaultOptions.persistModeChange;
configuration.persistModeChange = this.notPersistable
? false
: configuration.persistModeChange ?? this.defaultOptions.persistModeChange;
configuration.rowLimit = configuration.rowLimit ?? this.defaultOptions.rowLimit;
return configuration;
}
updateConfiguration(configuration) {
if (this.notPersistable) {
return;
}
this.openmct.objects.mutate(this.domainObject, 'configuration', configuration);
}

View File

@ -546,7 +546,7 @@ export default {
this.table.tableRows.on('sort', this.throttledUpdateVisibleRows);
this.table.tableRows.on('filter', this.throttledUpdateVisibleRows);
this.openmct.time.on('bounds', this.boundsChanged);
this.openmct.time.on('boundsChanged', this.boundsChanged);
//Default sort
this.sortOptions = this.table.tableRows.sortBy();
@ -561,6 +561,9 @@ export default {
this.table.initialize();
this.rescaleToContainer();
// Scroll to the top of the table after loading
this.addToAfterLoadActions(this.scroll);
},
beforeUnmount() {
this.table.off('object-added', this.addObject);
@ -579,7 +582,7 @@ export default {
this.table.configuration.off('change', this.updateConfiguration);
this.openmct.time.off('bounds', this.boundsChanged);
this.openmct.time.off('boundsChanged', this.boundsChanged);
this.table.configuration.destroy();

View File

@ -0,0 +1,44 @@
/*****************************************************************************
* 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.
*****************************************************************************/
@import '../../styles/vendor/normalize-min';
@import '../../styles/constants';
@import '../../styles/constants-mobile.scss';
@import '../../styles/constants-darkmatter';
@import '../../styles/mixins';
@import '../../styles/animations';
@import '../../styles/about';
@import '../../styles/glyphs';
@import '../../styles/global';
@import '../../styles/status';
@import '../../styles/limits';
@import '../../styles/controls';
@import '../../styles/forms';
@import '../../styles/table';
@import '../../styles/legacy';
@import '../../styles/legacy-plots';
@import '../../styles/plotly';
@import '../../styles/legacy-messages';
@import '../../styles/vue-styles.scss';

View File

@ -0,0 +1,30 @@
/*****************************************************************************
* 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.
*****************************************************************************/
// Note: This darkmatter theme is in Beta and is not yet ready for prime time. It needs some more tweaking.
import { installTheme } from './installTheme.js';
export default function plugin() {
return function install(openmct) {
installTheme(openmct, 'darkmatter');
};
}

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