Merge branch 'master' into 6820

This commit is contained in:
David Tsay 2023-11-22 11:41:07 -08:00
commit a63f4175bc
322 changed files with 6332 additions and 4023 deletions

View File

@ -2,7 +2,7 @@ version: 2.1
executors:
pw-focal-development:
docker:
- image: mcr.microsoft.com/playwright:v1.36.2-focal
- image: mcr.microsoft.com/playwright:v1.39.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
@ -162,7 +162,7 @@ jobs:
steps:
- build_and_install:
node-version: <<parameters.node-version>>
- run: npx playwright@1.36.2 install #Necessary for bare ubuntu machine
- run: npx playwright@1.39.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

@ -483,7 +483,14 @@
"websockets",
"swgs",
"memlab",
"devmode"
"devmode",
"blockquote",
"blockquotes",
"Blockquote",
"Blockquotes",
"oger",
"lcovonly",
"gcov"
],
"dictionaries": ["npm", "softwareTerms", "node", "html", "css", "bash", "en_US"],
"ignorePaths": [
@ -496,4 +503,4 @@
"html-test-results",
"test-results"
]
}
}

View File

@ -15,7 +15,8 @@ module.exports = {
'plugin:compat/recommended',
'plugin:vue/vue3-recommended',
'plugin:you-dont-need-lodash-underscore/compatible',
'plugin:prettier/recommended'
'plugin:prettier/recommended',
'plugin:no-unsanitized/DOM'
],
parser: 'vue-eslint-parser',
parserOptions: {
@ -145,29 +146,26 @@ module.exports = {
'no-implicit-coercion': 'error',
//https://eslint.org/docs/rules/no-unneeded-ternary
'no-unneeded-ternary': 'error',
"unicorn/filename-case": [
"error",
'unicorn/filename-case': [
'error',
{
"cases": {
"pascalCase": true
cases: {
pascalCase: true
},
"ignore": [
"^.*\\.js$"
]
ignore: ['^.*\\.js$']
}
],
'vue/first-attribute-linebreak': 'error',
'vue/multiline-html-element-content-newline': 'off',
'vue/singleline-html-element-content-newline': 'off',
'vue/multi-word-component-names': 'off', // TODO enable, align with conventions
'vue/no-mutating-props': 'off'
'vue/no-mutating-props': 'off' // TODO: Remove this rule and fix resulting errors
},
overrides: [
{
files: LEGACY_FILES,
rules: {
'no-unused-vars': [
'warn',
'error',
{
vars: 'all',
args: 'none',

View File

@ -5,6 +5,7 @@ updates:
schedule:
interval: 'weekly'
open-pull-requests-limit: 10
rebase-strategy: 'disabled'
labels:
- 'pr:daveit'
- 'pr:e2e'
@ -28,10 +29,13 @@ updates:
update-types: ['version-update:semver-patch']
- dependency-name: '@types/lodash'
update-types: ['version-update:semver-patch']
- dependency-name: 'marked'
update-types: ['version-update:semver-patch']
- package-ecosystem: 'github-actions'
directory: '/'
schedule:
interval: 'daily'
rebase-strategy: 'disabled'
labels:
- 'pr:daveit'
- 'type:maintenance'

23
.github/release.yml vendored Normal file
View File

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

View File

@ -27,7 +27,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL

View File

@ -15,8 +15,8 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 'lts/hydrogen'
@ -27,7 +27,7 @@ jobs:
key: ${{ runner.os }}-node-${{ hashFiles('**/package.json') }}
restore-keys: |
${{ runner.os }}-node-
- run: npm install --cache ~/.npm --no-audit --progress=false
- name: Login to DockerHub
@ -36,8 +36,8 @@ jobs:
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- run: npx playwright@1.36.2 install
- run: npx playwright@1.39.0 install
- name: Start CouchDB Docker Container and Init with Setup Scripts
run: |

View File

@ -20,11 +20,11 @@ jobs:
- ubuntu-latest
- windows-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 'lts/hydrogen'
- name: Cache NPM dependencies
uses: actions/cache@v3
with:
@ -32,8 +32,8 @@ jobs:
key: ${{ runner.os }}-node-${{ hashFiles('**/package.json') }}
restore-keys: |
${{ runner.os }}-node-
- run: npx playwright@1.36.2 install
- run: npx playwright@1.39.0 install
- run: npx playwright install chrome-beta
- run: npm install --cache ~/.npm --no-audit --progress=false
- run: npm run test:e2e:full -- --max-failures=40
@ -65,4 +65,4 @@ jobs:
});
} catch (error) {
core.warning(`Failed to remove ' + labelToRemove + ' label: ${error.message}`);
}
}

View File

@ -11,8 +11,8 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/hydrogen
- run: npm install
@ -26,8 +26,8 @@ jobs:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/hydrogen
registry-url: https://registry.npmjs.org/

View File

@ -29,10 +29,10 @@ jobs:
name: Node ${{ matrix.node_version }} - ${{ matrix.architecture }} on ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node_version }}
architecture: ${{ matrix.architecture }}

View File

@ -77,8 +77,7 @@ const config = {
MCT: path.join(projectRootDir, 'src/MCT'),
testUtils: path.join(projectRootDir, 'src/utils/testUtils.js'),
objectUtils: path.join(projectRootDir, 'src/api/objects/object-utils.js'),
utils: path.join(projectRootDir, 'src/utils'),
vue: path.join(projectRootDir, 'node_modules/@vue/compat/dist/vue.esm-bundler.js'),
utils: path.join(projectRootDir, 'src/utils')
}
},
plugins: [
@ -86,7 +85,9 @@ const config = {
__OPENMCT_VERSION__: `'${packageDefinition.version}'`,
__OPENMCT_BUILD_DATE__: `'${new Date()}'`,
__OPENMCT_REVISION__: `'${gitRevision}'`,
__OPENMCT_BUILD_BRANCH__: `'${gitBranch}'`
__OPENMCT_BUILD_BRANCH__: `'${gitBranch}'`,
__VUE_OPTIONS_API__: true, // enable/disable Options API support, default: true
__VUE_PROD_DEVTOOLS__: false // enable/disable devtools support in production, default: false
}),
new VueLoaderPlugin(),
new CopyWebpackPlugin({
@ -115,7 +116,7 @@ const config = {
new webpack.BannerPlugin({
test: /.*Theme\.css$/,
raw: true,
banner: '@charset "UTF-8";',
banner: '@charset "UTF-8";'
})
],
module: {
@ -142,10 +143,7 @@ const config = {
options: {
compilerOptions: {
hoistStatic: false,
whitespace: 'preserve',
compatConfig: {
MODE: 2
}
whitespace: 'preserve'
}
}
},

298
API.md
View File

@ -44,8 +44,10 @@
- [Clocks](#clocks)
- [Defining and registering clocks](#defining-and-registering-clocks)
- [Getting and setting active clock](#getting-and-setting-active-clock)
- [Stopping an active clock](#stopping-an-active-clock)
- [⚠️ \[DEPRECATED\] Stopping an active clock](#-deprecated-stopping-an-active-clock)
- [Clock Offsets](#clock-offsets)
- [Time Modes](#time-modes)
- [Time Mode Helper Methods](#time-mode-helper-methods)
- [Time Events](#time-events)
- [List of Time Events](#list-of-time-events)
- [The Time Conductor](#the-time-conductor)
@ -92,6 +94,9 @@ well as assets such as html, css, and images necessary for the UI.
## Starting an Open MCT application
> [!WARNING]
> Open MCT provides a development server via `webpack-dev-server` (`npm start`). **This should be used for development purposes only and should never be deployed to a production environment**.
To start a minimally functional Open MCT application, it is necessary to
include the Open MCT distributable, enable some basic plugins, and bootstrap
the application. The tutorials walk through the process of getting Open MCT up
@ -588,35 +593,108 @@ MinMax queries are issued by plots, and may be issued by other types as well. T
#### Telemetry Formats
Telemetry format objects define how to interpret and display telemetry data.
They have a simple structure:
They have a simple structure, provided here as a TypeScript interface:
- `key`: A `string` that uniquely identifies this formatter.
- `format`: A `function` that takes a raw telemetry value, and returns a
human-readable `string` representation of that value. It has one required
argument, and three optional arguments that provide context and can be used
for returning scaled representations of a value. An example of this is
representing time values in a scale such as the time conductor scale. There
are multiple ways of representing a point in time, and by providing a minimum
scale value, maximum scale value, and a count, it's possible to provide more
useful representations of time given the provided limitations.
- `value`: The raw telemetry value in its native type.
- `minValue`: An **optional** argument specifying the minimum displayed
value.
- `maxValue`: An **optional** argument specifying the maximum displayed
value.
- `count`: An **optional** argument specifying the number of displayed
values.
- `parse`: A `function` that takes a `string` representation of a telemetry
value, and returns the value in its native type. **Note** parse might receive an already-parsed value. This function should be idempotent.
- `validate`: A `function` that takes a `string` representation of a telemetry
value, and returns a `boolean` value indicating whether the provided string
can be parsed.
```ts
interface Formatter {
key: string; // A string that uniquely identifies this formatter.
format: (
value: any, // The raw telemetry value in its native type.
minValue?: number, // An optional argument specifying the minimum displayed value.
maxValue?: number, // An optional argument specifying the maximum displayed value.
count?: number // An optional argument specifying the number of displayed values.
) => string; // Returns a human-readable string representation of the provided value.
parse: (
value: string | any // A string representation of a telemetry value or an already-parsed value.
) => any; // Returns the value in its native type. This function should be idempotent.
validate: (value: string) => boolean; // Takes a string representation of a telemetry value and returns a boolean indicating whether the provided string can be parsed.
}
```
##### Built-in Formats
Open MCT on its own defines a handful of built-in formats:
###### **Number Format (default):**
Applied to data with `format: 'number'`
```js
valueMetadata = {
format: 'number'
// ...
};
```
```ts
interface NumberFormatter extends Formatter {
parse: (x: any) => number;
format: (x: number) => string;
validate: (value: any) => boolean;
}
```
###### **String Format**:
Applied to data with `format: 'string'`
```js
valueMetadata = {
format: 'string'
// ...
};
```
```ts
interface StringFormatter extends Formatter {
parse: (value: any) => string;
format: (value: string) => string;
validate: (value: any) => boolean;
}
```
###### **Enum Format**:
Applied to data with `format: 'enum'`
```js
valueMetadata = {
format: 'enum',
enumerations: [
{
value: 1,
string: 'APPLE'
},
{
value: 2,
string: 'PEAR',
},
{
value: 3,
string: 'ORANGE'
}]
// ...
};
```
Creates a two-way mapping between enum string and value to be used in the `parse` and `format` methods.
Ex:
- `formatter.parse('APPLE') === 1;`
- `formatter.format(1) === 'APPLE';`
```ts
interface EnumFormatter extends Formatter {
parse: (value: string) => string;
format: (value: number) => string;
validate: (value: any) => boolean;
}
```
##### Registering Formats
Formats implement the following interface (provided here as TypeScript for simplicity):
Formats are registered with the Telemetry API using the `addFormat` function. eg.
``` javascript
```javascript
openmct.telemetry.addFormat({
key: 'number-to-string',
format: function (number) {
@ -685,8 +763,9 @@ state of the application, and emits events to inform listeners when the state ch
Because the data displayed tends to be time domain data, Open MCT must always
have at least one time system installed and activated. When you download Open
MCT, it will be pre-configured to use the UTC time system, which is installed and activated, along with other default plugins, in `index.html`. Installing and activating a time system is simple, and is covered
[in the next section](#defining-and-registering-time-systems).
MCT, it will be pre-configured to use the UTC time system, which is installed and activated,
along with other default plugins, in `index.html`. Installing and activating a time system
is simple, and is covered [in the next section](#defining-and-registering-time-systems).
### Time Systems and Bounds
@ -737,28 +816,38 @@ numbers in UTC terrestrial time.
#### Getting and Setting the Active Time System
Once registered, a time system can be activated by calling `timeSystem` with
the timeSystem `key` or an instance of the time system. If you are not using a
[clock](#clocks), you must also specify valid [bounds](#time-bounds) for the
timeSystem.
Once registered, a time system can be activated by calling `setTimeSystem` with
the timeSystem `key` or an instance of the time system. You can also specify
valid [bounds](#time-bounds) for the timeSystem.
```javascript
openmct.time.timeSystem('utc', bounds);
openmct.time.setTimeSystem('utc', bounds);
```
The current time system can be retrieved as well by calling `getTimeSystem`.
```javascript
openmct.time.getTimeSystem();
```
A time system can be immediately activated after registration:
```javascript
openmct.time.addTimeSystem(utcTimeSystem);
openmct.time.timeSystem(utcTimeSystem, bounds);
openmct.time.setTimeSystem(utcTimeSystem, bounds);
```
Setting the active time system will trigger a [`'timeSystem'`](#time-events)
event. If you supplied bounds, a [`'bounds'`](#time-events) event will be triggered afterwards with your newly supplied bounds.
Setting the active time system will trigger a [`'timeSystemChanged'`](#time-events)
event. If you supplied bounds, a [`'boundsChanged'`](#time-events) event will be triggered afterwards with your newly supplied bounds.
> ⚠️ **Deprecated**
> - The method `timeSystem()` is deprecated. Please use `getTimeSystem()` and `setTimeSystem()` as a replacement.
#### Time Bounds
The TimeAPI provides a getter/setter for querying and setting time bounds. Time
The TimeAPI provides a getter and setter for querying and setting time bounds. Time
bounds are simply an object with a `start` and an end `end` attribute.
- `start`: A `number` representing a moment in time in the active [Time System](#defining-and-registering-time-systems).
@ -768,26 +857,34 @@ telemetry views.
This will be used as the end of the time period displayed by time-responsive
telemetry views.
If invoked with bounds, it will set the new time bounds system-wide. If invoked
without any parameters, it will return the current application-wide time bounds.
New bounds can be set system wide by calling `setBounds` with [bounds](#time-bounds).
``` javascript
const ONE_HOUR = 60 * 60 * 1000;
let now = Date.now();
openmct.time.bounds({start: now - ONE_HOUR, now);
openmct.time.setBounds({start: now - ONE_HOUR, now);
```
To respond to bounds change events, listen for the [`'bounds'`](#time-events)
Calling `getBounds` will return the current application-wide time bounds.
``` javascript
openmct.time.getBounds();
```
To respond to bounds change events, listen for the [`'boundsChanged'`](#time-events)
event.
> ⚠️ **Deprecated**
> - The method `bounds()` is deprecated and will be removed in a future release. Please use `getBounds()` and `setBounds()` as a replacement.
### Clocks
The Time API can be set to follow a clock source which will cause the bounds
to be updated automatically whenever the clock source "ticks". A clock is simply
an object that supports registration of listeners and periodically invokes its
listeners with a number. Open MCT supports registration of new clock sources that
tick on almost anything. A tick occurs when the clock invokes callback functions
registered by its listeners with a new time value.
The Time API requires a clock source which will cause the bounds to be updated
automatically whenever the clock source "ticks". A clock is simply an object that
supports registration of listeners and periodically invokes its listeners with a
number. Open MCT supports registration of new clock sources that tick on almost
anything. A tick occurs when the clock invokes callback functions registered by its
listeners with a new time value.
An example of a clock source is the [LocalClock](https://github.com/nasa/openmct/blob/master/src/plugins/utcTimeSystem/LocalClock.js)
which emits the current time in UTC every 100ms. Clocks can tick on anything. For
@ -855,23 +952,29 @@ An example clock implementation is provided in the form of the [LocalClock](http
#### Getting and setting active clock
Once registered a clock can be activated by calling the `clock` function on the
Once registered a clock can be activated by calling the `setClock` function on the
Time API passing in the key or instance of a registered clock. Only one clock
may be active at once, so activating a clock will deactivate any currently
active clock. [`clockOffsets`](#clock-offsets) must be specified when changing a clock.
active clock and start the new clock. [`clockOffsets`](#clock-offsets) must be specified when changing a clock.
Setting the clock triggers a [`'clock'`](#time-events) event, followed by a [`'clockOffsets'`](#time-events) event, and then a [`'bounds'`](#time-events) event as the offsets are applied to the clock's currentValue().
Setting the clock triggers a [`'clockChanged'`](#time-events) event, followed by a [`'clockOffsetsChanged'`](#time-events) event, and then a [`'boundsChanged'`](#time-events) event as the offsets are applied to the clock's currentValue().
```
openmct.time.clock(someClock, clockOffsets);
openmct.time.setClock(someClock, clockOffsets);
```
Upon being activated, the time API will listen for tick events on the clock by calling `clock.on`.
The currently active clock (if any) can be retrieved by calling the same
function without any arguments.
The currently active clock can be retrieved by calling `getClock`.
#### Stopping an active clock
```
openmct.time.getClock();
```
> ⚠️ **Deprecated**
> - The method `clock()` is deprecated and will be removed in a future release. Please use `getClock()` and `setClock()` as a replacement.
#### ⚠️ [DEPRECATED] Stopping an active clock
_As of July 2023, this method will be deprecated. Open MCT will always have a ticking clock._
@ -882,12 +985,14 @@ will stop the clock from ticking, and set the active clock to `undefined`.
openmct.time.stopClock();
```
> ⚠️ **Deprecated**
> - The method `stopClock()` is deprecated and will be removed in a future release.
#### Clock Offsets
When a clock is active, the time bounds of the application will be updated
automatically each time the clock "ticks". The bounds are calculated based on
the current value provided by the active clock (via its `tick` event, or its
`currentValue()` method).
When in Real-time [mode](#time-modes), the time bounds of the application will be updated automatically each time the
clock "ticks". The bounds are calculated based on the current value provided by
the active clock (via its `tick` event, or its `currentValue()` method).
Unlike bounds, which represent absolute time values, clock offsets represent
relative time spans. Offsets are defined as an object with two properties:
@ -898,21 +1003,77 @@ value provided by a clock's tick callback, or its `currentValue()` function.
- `end`: A `number` that must be >= 0 and which is used to calculate the end
bounds on each clock tick.
The `clockOffsets` function can be used to get or set clock offsets. For example,
The `setClockOffsets` function can be used to get or set clock offsets. For example,
to show the last fifteen minutes in a ms-based time system:
```javascript
var FIFTEEN_MINUTES = 15 * 60 * 1000;
openmct.time.clockOffsets({
openmct.time.setClockOffsets({
start: -FIFTEEN_MINUTES,
end: 0
})
```
The `getClockOffsets` method will return the currently set clock offsets.
```javascript
openmct.time.getClockOffsets()
```
**Note:** Setting the clock offsets will trigger an immediate bounds change, as
new bounds will be calculated based on the `currentValue()` of the active clock.
Clock offsets are only relevant when a clock source is active.
Clock offsets are only relevant when in Real-time [mode](#time-modes).
> ⚠️ **Deprecated**
> - The method `clockOffsets()` is deprecated and will be removed in a future release. Please use `getClockOffsets()` and `setClockOffsets()` as a replacement.
### Time Modes
There are two time modes in Open MCT, "Fixed" and "Real-time". In Real-time mode the
time bounds of the application will be updated automatically each time the clock "ticks".
The bounds are calculated based on the current value provided by the active clock. In
Fixed mode, the time bounds are set for a specified time range. When Open MCT is first
initialized, it will be in Real-time mode.
The `setMode` method can be used to set the current time mode. It accepts a mode argument,
`'realtime'` or `'fixed'` and it also accepts an optional [offsets](#clock-offsets)/[bounds](#time-bounds) argument dependent
on the current mode.
``` javascript
openmct.time.setMode('fixed');
openmct.time.setMode('fixed', bounds); // with optional bounds
```
or
``` javascript
openmct.time.setMode('realtime');
openmct.time.setMode('realtime', offsets); // with optional offsets
```
The `getMode` method will return the current time mode, either `'realtime'` or `'fixed'`.
``` javascript
openmct.time.getMode();
```
#### Time Mode Helper Methods
There are two methods available to determine the current time mode in Open MCT programmatically,
`isRealTime` and `isFixed`. Each one will return a boolean value based on the current mode.
``` javascript
if (openmct.time.isRealTime()) {
// do real-time stuff
}
```
``` javascript
if (openmct.time.isFixed()) {
// do fixed-time stuff
}
```
### Time Events
@ -921,7 +1082,7 @@ The Time API is a standard event emitter; you can register callbacks for events
For example:
``` javascript
openmct.time.on('bounds', function callback (newBounds, tick) {
openmct.time.on('boundsChanged', function callback (newBounds, tick) {
// Do something with new bounds
});
```
@ -930,7 +1091,7 @@ openmct.time.on('bounds', function callback (newBounds, tick) {
The events emitted by the Time API are:
- `bounds`: emitted whenever the bounds change. The callback will be invoked
- `boundsChanged`: emitted whenever the bounds change. The callback will be invoked
with two arguments:
- `bounds`: A [bounds](#getting-and-setting-bounds) bounds object
representing a new time period bound by the specified start and send times.
@ -945,15 +1106,24 @@ The events emitted by the Time API are:
If `tick` is false,then the bounds change was not due to an automatic tick,
and a query for historical data may be necessary, depending on your data
caching strategy, and how significantly the start bound has changed.
- `timeSystem`: emitted whenever the active time system changes. The callback will be invoked with a single argument:
- `timeSystemChanged`: emitted whenever the active time system changes. The callback will be invoked with a single argument:
- `timeSystem`: The newly active [time system](#defining-and-registering-time-systems).
- `clock`: emitted whenever the clock changes. The callback will be invoked
- `clockChanged`: emitted whenever the clock changes. The callback will be invoked
with a single argument:
- `clock`: The newly active [clock](#clocks), or `undefined` if an active
clock has been deactivated.
- `clockOffsets`: emitted whenever the active clock offsets change. The
- `clockOffsetsChanged`: emitted whenever the active clock offsets change. The
callback will be invoked with a single argument:
- `clockOffsets`: The new [clock offsets](#clock-offsets).
- `modeChanged`: emitted whenever the time [mode](#time-modes) changed. The callback will
be invoked with one argument:
- `mode`: A string representation of the current time mode, either `'realtime'` or `'fixed'`.
> ⚠️ **Deprecated Events** (These will be removed in a future release):
> - `bounds``boundsChanged`
> - `timeSystem``timeSystemChanged`
> - `clock``clockChanged`
> - `clockOffsets``clockOffsetsChanged`
### The Time Conductor

View File

@ -127,6 +127,8 @@ Each test suite generates a report in CircleCI. For a complete overview of testi
Our code coverage is generated during the runtime of our unit, e2e, and visual tests. The combination of those reports is published to [codecov.io](https://app.codecov.io/gh/nasa/openmct/)
For more on the specifics of our code coverage setup, [see](TESTING.md#code-coverage)
# Glossary
Certain terms are used throughout Open MCT with consistent meanings
@ -182,3 +184,17 @@ You might still be using legacy API if your source code
### What should I do if I am using legacy API?
Please refer to [the modern Open MCT API](https://nasa.github.io/openmct/documentation/). Post any questions to the [Discussions section](https://github.com/nasa/openmct/discussions) of the Open MCT GitHub repository.
## Related Repos
> [!NOTE]
> Although Open MCT functions as a standalone project, it is primarily an extensible framework intended to be used as a dependency with users' own plugins and packaging. Furthermore, Open MCT is intended to be used with an HTTP server such as Apache or Nginx. A great example of hosting Open MCT with Apache is `openmct-quickstart` and can be found in the table below.
| Repository | Description |
| --- | --- |
| [openmct-tutorial](https://github.com/nasa/openmct-tutorial) | A great place for beginners to learn how to use and extend Open MCT. |
| [openmct-quickstart](https://github.com/scottbell/openmct-quickstart) | A working example of Open MCT integrated with Apache HTTP server, YAMCS telemetry, and Couch DB for persistence.
| [Open MCT YAMCS Plugin](https://github.com/akhenry/openmct-yamcs) | Plugin for integrating YAMCS telemetry and command server with Open MCT. |
| [openmct-performance](https://github.com/unlikelyzero/openmct-performance) | Resources for performance testing Open MCT. |
| [openmct-as-a-dependency](https://github.com/unlikelyzero/openmct-as-a-dependency) | An advanced guide for users on how to build, develop, and test Open MCT when it's used as a dependency. |

View File

@ -37,14 +37,85 @@ Documentation located [here](./e2e/README.md)
## Code Coverage
* 100% statement coverage is achievable and desirable.
It's up to the individual developer as to whether they want to add line coverage in the form of a unit test or e2e test.
Codecov.io will combine each of the above commands with [Codecov.io Flags](https://docs.codecov.com/docs/flags). Effectively, this allows us to combine multiple reports which are run at various stages of our CI Pipeline or run as part of a parallel process.
Line Code Coverage is generated by our unit tests and e2e tests, then combined by ([Codecov.io Flags](https://docs.codecov.com/docs/flags)), and finally reported in GitHub PRs by Codecov.io's PR Bot. This workflow gives a comprehensive (if flawed) view of line coverage.
### Karma-istanbul
Line coverage is generated by our `karma-coverage-istanbul-reporter` package as defined in our `karma.conf.js` file:
```js
coverageIstanbulReporter: {
fixWebpackSourcePaths: true,
skipFilesWithNoCoverage: true,
dir: 'coverage/unit', //Sets coverage file to be consumed by codecov.io
reports: ['lcovonly']
},
```
Once the file is generated, it can be published to codecov with
```json
"cov:unit:publish": "codecov --disable=gcov -f ./coverage/unit/lcov.info -F unit",
```
### 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. 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`.
1. The rest of our coverage only appears when run against `@unstable` tests, persistent datastore (couchdb), non-ubuntu machines, and non-chrome browsers with the `npm run cov:e2e:full:publish` flag. Since this happens about once a day, we have leveraged codecov.io's carryforward flag to report on lines covered outside of each commit on an individual PR.
This e2e coverage is combined with our unit test report to give a comprehensive (if flawed) view of line coverage.
### Limitations in our code coverage reporting
Our code coverage implementation has some known limitations:
- [Variability](https://github.com/nasa/openmct/issues/5811)
- [Accuracy](https://github.com/nasa/openmct/issues/7015)
- [Vue instrumentation gaps](https://github.com/nasa/openmct/issues/4973)
Our code coverage implementation has two known limitations:
- [Variability and accuracy](https://github.com/nasa/openmct/issues/5811)
- [Vue instrumentation](https://github.com/nasa/openmct/issues/4973)
## Troubleshooting CI
The following is an evolving guide to troubleshoot CI and PR issues.
### Github Checks failing
There are a few reasons that your GitHub PR could be failing beyond simple failed tests.
* Required Checks. We're leveraging required checks in GitHub so that we can quickly and precisely control what becomes and informational failure vs a hard requirement. The only way to determine the difference between a required vs information check is check for the `(Required)` emblem next to the step details in GitHub Checks.
* Not all required checks are run per commit. You may need to manually trigger addition GitHub checks with a `pr:<label>` label added to your PR.
### Flaky tests
There are two ways to know if a test on your branch is historically flaky:
1. `deploysentinel`'s PR comment bot to give an accurate and historical view of e2e flakiness. Check your PR for a view of the test failures and flakes (with link to the failing test). Note: only a 7 day window of flake is available.
2. (CircleCI's test insights feature)[https://circleci.com/blog/introducing-test-insights-with-flaky-test-detection/] collects historical data about the individual test results for both unit and e2e tests. Note: only a 14 day window of flake is available.
### Local=Pass and CI=Fail
Although rare, it is possible that your test can pass locally but fail in CI.
#### Busting Cache
In certain circumstances, the CircleCI cache can become stale. In order to bust the cache, we've implemented a runtime boolean parameter in Circle CI creatively name BUST_CACHE. To execute:
1. Navigate to the branch in Circle CI believed to have stale cache.
1. Click on the 'Trigger Pipeline' button.
1. Add Parameter -> Parameter Type = boolean , Name = BUST_CACHE ,Value = true
1. Click 'Trigger Pipeline'
#### Run tests in the same container as CI
In extreme cases, tests can fail due to the constraints of running within a container. To execute tests in exactly the same way as run in CircleCI.
```sh
// Replace {X.X.X} with the current Playwright version
// from our package.json or circleCI configuration file
docker run --rm --network host --cpus="2" -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v{X.X.X}-focal /bin/bash
npm install
```
At this point, you're running inside the same container and with 2 cpu cores. You can specify the unit tests:
```sh
npm run test
```
or e2e tests:
```sh
npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep <the testcase name>
```

View File

@ -193,7 +193,7 @@ Current list of test tags:
|`@ipad` | Test case or test suite is compatible with Playwright's iPad support and Open MCT's read-only mobile view (i.e. no create button).|
|`@gds` | Denotes a GDS Test Case used in the VIPER Mission.|
|`@addInit` | Initializes the browser with an injected and artificial state. Useful for loading non-default plugins. Likely will not work outside of `npm start`.|
|`@localStorage` | Captures or generates session storage to manipulate browser state. Useful for excluding in tests which require a persistent backend (i.e. CouchDB).|
|`@localStorage` | Captures or generates session storage to manipulate browser state. Useful for excluding in tests which require a persistent backend (i.e. CouchDB). See [note](#utilizing-localstorage)|
|`@snapshot` | Uses Playwright's snapshot functionality to record a copy of the DOM for direct comparison. Must be run inside of the playwright container.|
|`@unstable` | A new test or test which is known to be flaky.|
|`@2p` | Indicates that multiple users are involved, or multiple tabs/pages are used. Useful for testing multi-user interactivity.|
@ -352,6 +352,28 @@ By adhering to this principle, we can create tests that are both robust and refl
1. Avoid repeated setup to test a single assertion. Write longer tests with multiple soft assertions.
This ensures that your changes will be picked up with large refactors.
##### Utilizing LocalStorage
1. In order to save test runtime in the case of tests that require a decent amount of initial setup (such as in the case of testing complex displays), you may use [Playwright's `storageState` feature](https://playwright.dev/docs/api/class-browsercontext#browser-context-storage-state) to generate and load localStorage states.
1. To generate a localStorage state to be used in a test:
- Add an e2e test to our generateLocalStorageData suite which sets the initial state (creating/configuring objects, etc.), saving it in the `test-data` folder:
```js
// Save localStorage for future test execution
await context.storageState({
path: path.join(__dirname, '../../../e2e/test-data/display_layout_with_child_layouts.json')
});
```
- Load the state from file at the beginning of the desired test suite (within the `test.describe()`). (NOTE: the storage state will be used for each test in the suite, so you may need to create a new suite):
```js
const LOCALSTORAGE_PATH = path.resolve(
__dirname,
'../../../../test-data/display_layout_with_child_layouts.json'
);
test.use({
storageState: path.resolve(__dirname, LOCALSTORAGE_PATH)
});
```
### How to write a great test
- Avoid using css locators to find elements to the page. Use modern web accessible locators like `getByRole`
@ -468,15 +490,7 @@ Our e2e code coverage is captured and combined with our unit test coverage. For
#### Generating e2e code coverage
Code coverage is collected during test execution using our custom [baseFixture](./baseFixtures.js). The raw coverage files are stored in a `.nyc_report` directory to be converted into a lcov file with the following [nyc](https://github.com/istanbuljs/nyc) command:
```npm run cov:e2e:report```
At this point, the nyc linecov report can be published to [codecov.io](https://about.codecov.io/) with the following command:
```npm run cov:e2e:stable:publish``` for the stable suite running in ubuntu.
or
```npm run cov:e2e:full:publish``` for the full suite running against all available platforms.
Please read more about our code coverage [here](../TESTING.md#code-coverage)
## Other
@ -526,10 +540,10 @@ A single e2e test in Open MCT is extended to run:
- How is Open MCT extending default Playwright functionality?
- What about Component Testing?
### Troubleshooting
### e2e Troubleshooting
Please follow the general guide troubleshooting in [the general troubleshooting doc](../TESTING.md#troubleshooting-ci)
- Why is my test failing on CI and not locally?
- How can I view the failing tests on CI?
- Tests won't start because 'Error: <http://localhost:8080/># is already used...'
This error will appear when running the tests locally. Sometimes, the webserver is left in an orphaned state and needs to be cleaned up. To clear up the orphaned webserver, execute the following from your Terminal:
```lsof -n -i4TCP:8080 | awk '{print$2}' | tail -1 | xargs kill -9```

View File

@ -81,7 +81,7 @@ async function createDomainObjectWithDefaults(
await page.goto(`${parentUrl}`);
//Click the Create button
await page.click('button:has-text("Create")');
await page.getByRole('button', { name: 'Create' }).click();
// Click the object specified by 'type'
await page.click(`li[role='menuitem']:text("${type}")`);
@ -108,7 +108,7 @@ async function createDomainObjectWithDefaults(
// Click OK button and wait for Navigate event
await Promise.all([
page.waitForLoadState(),
page.click('[aria-label="Save"]'),
await page.getByRole('button', { name: 'Save' }).click(),
// Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
@ -120,8 +120,8 @@ async function createDomainObjectWithDefaults(
if (await _isInEditMode(page, uuid)) {
// Save (exit edit mode)
await page.locator('button[title="Save"]').click();
await page.locator('li[title="Save and Finish Editing"]').click();
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
}
return {
@ -182,7 +182,7 @@ async function createPlanFromJSON(page, { name, json, parent = 'mine' }) {
await page.goto(`${parentUrl}`);
// Click the Create button
await page.click('button:has-text("Create")');
await page.getByRole('button', { name: 'Create' }).click();
// Click 'Plan' menu option
await page.click(`li:text("Plan")`);
@ -231,7 +231,7 @@ async function createExampleTelemetryObject(page, parent = 'mine') {
await page.goto(`${parentUrl}`);
await page.locator('button:has-text("Create")').click();
await page.getByRole('button', { name: 'Create' }).click();
await page.locator('li:has-text("Sine Wave Generator")').click();

View File

@ -34,7 +34,7 @@ async function enterTextEntry(page, text) {
await page.locator(NOTEBOOK_DROP_AREA).click();
// enter text
await page.locator('[aria-label="Notebook Entry"].is-selected div.c-ne__text').fill(text);
await page.getByLabel('Notebook Entry Input').last().fill(text);
await commitEntry(page);
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -55,6 +55,67 @@ test.describe('Generate Visual Test Data @localStorage @generatedata', () => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
});
test('Generate display layout with 2 child display layouts', async ({ page, context }) => {
// Create Display Layout
const parent = await createDomainObjectWithDefaults(page, {
type: 'Display Layout',
name: 'Parent Display Layout'
});
const child1 = await createDomainObjectWithDefaults(page, {
type: 'Display Layout',
name: 'Child Layout 1',
parent: parent.uuid
});
const child2 = await createDomainObjectWithDefaults(page, {
type: 'Display Layout',
name: 'Child Layout 2',
parent: parent.uuid
});
await page.goto(parent.url);
await page.getByLabel('Edit').click();
await page.getByLabel(`${child2.name} Layout Grid`).hover();
await page.getByLabel('Move Sub-object Frame').nth(1).click();
await page.getByLabel('X:').fill('30');
await page.getByLabel(`${child1.name} Layout Grid`).hover();
await page.getByLabel('Move Sub-object Frame').first().click();
await page.getByLabel('Y:').fill('30');
await page.getByLabel('Save').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
//Save localStorage for future test execution
await context.storageState({
path: path.join(__dirname, '../../../e2e/test-data/display_layout_with_child_layouts.json')
});
});
test('Generate flexible layout with 2 child display layouts', async ({ page, context }) => {
// Create Display Layout
const parent = await createDomainObjectWithDefaults(page, {
type: 'Flexible Layout',
name: 'Parent Flexible Layout'
});
await createDomainObjectWithDefaults(page, {
type: 'Display Layout',
name: 'Child Layout 1',
parent: parent.uuid
});
await createDomainObjectWithDefaults(page, {
type: 'Display Layout',
name: 'Child Layout 2',
parent: parent.uuid
});
await page.goto(parent.url);
//Save localStorage for future test execution
await context.storageState({
path: path.join(__dirname, '../../../e2e/test-data/flexible_layout_with_child_layouts.json')
});
});
// TODO: Visual test for the generated object here
// - Move to using appActions to create the overlay plot
// and embedded standard telemetry object

View File

@ -0,0 +1,59 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
Verify that the "Clear Data" menu action performs as expected for various object types.
*/
const { test, expect } = require('../../pluginFixtures.js');
const { createDomainObjectWithDefaults } = require('../../appActions.js');
const backgroundImageSelector = '.c-imagery__main-image__background-image';
test.describe('Clear Data Action', () => {
test.beforeEach(async ({ page }) => {
// Go to baseURL
await page.goto('./', { waitUntil: 'domcontentloaded' });
// Create a default 'Example Imagery' object
const exampleImagery = await createDomainObjectWithDefaults(page, { type: 'Example Imagery' });
// Verify that the created object is focused
await expect(page.locator('.l-browse-bar__object-name')).toContainText(exampleImagery.name);
await page.locator('.c-imagery__main-image__bg').hover({ trial: true });
await expect(page.locator(backgroundImageSelector)).toBeVisible();
});
test('works as expected with Example Imagery', async ({ page }) => {
await expect(await page.locator('.c-thumb__image').count()).toBeGreaterThan(0);
// Click the "Clear Data" menu action
await page.getByTitle('More options').click();
const clearDataMenuItem = page.getByRole('menuitem', {
name: 'Clear Data'
});
await expect(clearDataMenuItem).toBeEnabled();
await clearDataMenuItem.click();
// Verify that the background image is no longer visible
await expect(page.locator(backgroundImageSelector)).toBeHidden();
await expect(await page.locator('.c-thumb__image').count()).toBe(0);
});
});

View File

@ -149,7 +149,7 @@ test.describe('Move & link item tests', () => {
// Finish editing and save Telemetry Table
await page.locator('.c-button--menu.c-button--major.icon-save').click();
await page.locator('text=Save and Finish Editing').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Create New Folder Basic Domain Object
let folder = 'Test Folder';

View File

@ -165,8 +165,7 @@ test.describe('Notification Overlay', () => {
// Click on the "Save" button
await page.click('button[title="Save"]');
// Click on the "Save and Finish Editing" option
await page.click('li[title="Save and Finish Editing"]');
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Verify that Notification List is NOT open
expect(await page.locator('div[role="dialog"]').isVisible()).toBe(false);

View File

@ -19,7 +19,7 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/* global __dirname */
/*
This test suite is dedicated to tests which verify the basic operations surrounding conditionSets. Note: this
suite is sharing state between tests which is considered an anti-pattern. Implementing in this way to
@ -31,6 +31,7 @@ const {
createDomainObjectWithDefaults,
createExampleTelemetryObject
} = require('../../../../appActions');
const path = require('path');
let conditionSetUrl;
let getConditionSetIdentifierFromUrl;
@ -48,7 +49,9 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
await Promise.all([page.waitForNavigation(), page.click('button:has-text("OK")')]);
//Save localStorage for future test execution
await context.storageState({ path: './e2e/test-data/recycled_local_storage.json' });
await context.storageState({
path: path.resolve(__dirname, '../../../../test-data/recycled_local_storage.json')
});
//Set object identifier from url
conditionSetUrl = page.url();
@ -59,7 +62,9 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
});
//Load localStorage for subsequent tests
test.use({ storageState: './e2e/test-data/recycled_local_storage.json' });
test.use({
storageState: path.resolve(__dirname, '../../../../test-data/recycled_local_storage.json')
});
//Begin suite of tests again localStorage
test('Condition set object properties persist in main view and inspector @localStorage', async ({
@ -117,7 +122,7 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
.nth(1)
.click();
// Click Save and Finish Editing Option
await page.locator('text=Save and Finish Editing').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
//Verify Main section reflects updated Name Property
await expect

View File

@ -19,8 +19,9 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/* global __dirname */
const { test, expect } = require('../../../../pluginFixtures');
const path = require('path');
const {
createDomainObjectWithDefaults,
setStartOffset,
@ -29,6 +30,88 @@ const {
setIndependentTimeConductorBounds
} = require('../../../../appActions');
const LOCALSTORAGE_PATH = path.resolve(
__dirname,
'../../../../test-data/display_layout_with_child_layouts.json'
);
const TINY_IMAGE_BASE64 =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII';
test.describe('Display Layout Toolbar Actions @localStorage', () => {
const PARENT_DISPLAY_LAYOUT_NAME = 'Parent Display Layout';
const CHILD_DISPLAY_LAYOUT_NAME1 = 'Child Layout 1';
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
await setRealTimeMode(page);
await page
.locator('a')
.filter({ hasText: 'Parent Display Layout Display Layout' })
.first()
.click();
await page.getByLabel('Edit').click();
});
test.use({
storageState: path.resolve(__dirname, LOCALSTORAGE_PATH)
});
test('can add/remove Text element to a single layout', async ({ page }) => {
const layoutObject = 'Text';
await test.step(`Add and remove ${layoutObject} from the parent's layout`, async () => {
await addAndRemoveDrawingObjectAndAssert(page, layoutObject, PARENT_DISPLAY_LAYOUT_NAME);
});
await test.step(`Add and remove ${layoutObject} from the child's layout`, async () => {
await addAndRemoveDrawingObjectAndAssert(page, layoutObject, CHILD_DISPLAY_LAYOUT_NAME1);
});
});
test('can add/remove Image to a single layout', async ({ page }) => {
const layoutObject = 'Image';
await test.step("Add and remove image element from the parent's layout", async () => {
expect(await page.getByLabel(`Move ${layoutObject} Frame`).count()).toBe(0);
await addLayoutObject(page, PARENT_DISPLAY_LAYOUT_NAME, layoutObject);
expect(await page.getByLabel(`Move ${layoutObject} Frame`).count()).toBe(1);
await removeLayoutObject(page, layoutObject);
expect(await page.getByLabel(`Move ${layoutObject} Frame`).count()).toBe(0);
});
await test.step("Add and remove image from the child's layout", async () => {
await addLayoutObject(page, CHILD_DISPLAY_LAYOUT_NAME1, layoutObject);
expect(await page.getByLabel(`Move ${layoutObject} Frame`).count()).toBe(1);
await removeLayoutObject(page, layoutObject);
expect(await page.getByLabel(`Move ${layoutObject} Frame`).count()).toBe(0);
});
});
test(`can add/remove Box to a single layout`, async ({ page }) => {
const layoutObject = 'Box';
await test.step(`Add and remove ${layoutObject} from the parent's layout`, async () => {
await addAndRemoveDrawingObjectAndAssert(page, layoutObject, PARENT_DISPLAY_LAYOUT_NAME);
});
await test.step(`Add and remove ${layoutObject} from the child's layout`, async () => {
await addAndRemoveDrawingObjectAndAssert(page, layoutObject, CHILD_DISPLAY_LAYOUT_NAME1);
});
});
test(`can add/remove Line to a single layout`, async ({ page }) => {
const layoutObject = 'Line';
await test.step(`Add and remove ${layoutObject} from the parent's layout`, async () => {
await addAndRemoveDrawingObjectAndAssert(page, layoutObject, PARENT_DISPLAY_LAYOUT_NAME);
});
await test.step(`Add and remove ${layoutObject} from the child's layout`, async () => {
await addAndRemoveDrawingObjectAndAssert(page, layoutObject, CHILD_DISPLAY_LAYOUT_NAME1);
});
});
test(`can add/remove Ellipse to a single layout`, async ({ page }) => {
const layoutObject = 'Ellipse';
await test.step(`Add and remove ${layoutObject} from the parent's layout`, async () => {
await addAndRemoveDrawingObjectAndAssert(page, layoutObject, PARENT_DISPLAY_LAYOUT_NAME);
});
await test.step(`Add and remove ${layoutObject} from the child's layout`, async () => {
await addAndRemoveDrawingObjectAndAssert(page, layoutObject, CHILD_DISPLAY_LAYOUT_NAME1);
});
});
test.fixme('Can switch view types of a single SWG in a layout', async ({ page }) => {});
test.fixme('Can merge multiple plots in a layout', async ({ page }) => {});
test.fixme('Can adjust stack order of a single object in a layout', async ({ page }) => {});
test.fixme('Can duplicate a single object in a layout', async ({ page }) => {});
});
test.describe('Display Layout', () => {
/** @type {import('../../../../appActions').CreatedObjectInfo} */
let sineWaveObject;
@ -41,6 +124,7 @@ test.describe('Display Layout', () => {
type: 'Sine Wave Generator'
});
});
test('alpha-numeric widget telemetry value exactly matches latest telemetry value received in real time', async ({
page
}) => {
@ -64,7 +148,7 @@ test.describe('Display Layout', () => {
const layoutGridHolder = page.locator('.l-layout__grid-holder');
await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder);
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Subscribe to the Sine Wave Generator data
// On getting data, check if the value found in the Display Layout is the most recent value
@ -102,7 +186,7 @@ test.describe('Display Layout', () => {
const layoutGridHolder = page.locator('.l-layout__grid-holder');
await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder);
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Subscribe to the Sine Wave Generator data
const getTelemValuePromise = await subscribeToTelemetry(page, sineWaveObject.uuid);
@ -144,7 +228,7 @@ test.describe('Display Layout', () => {
const layoutGridHolder = page.locator('.l-layout__grid-holder');
await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder);
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
expect.soft(await page.locator('.l-layout .l-layout__frame').count()).toEqual(1);
@ -186,7 +270,7 @@ test.describe('Display Layout', () => {
const layoutGridHolder = page.locator('.l-layout__grid-holder');
await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder);
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
expect.soft(await page.locator('.l-layout .l-layout__frame').count()).toEqual(1);
@ -242,7 +326,7 @@ test.describe('Display Layout', () => {
await page.locator('div[title="Resize object width"] > input').fill('70');
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
const startDate = '2021-12-30 01:01:00.000Z';
const endDate = '2021-12-30 01:11:00.000Z';
@ -304,7 +388,7 @@ test.describe('Display Layout', () => {
await page.getByText('Overlay Plot').click();
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Time to inspect some network traffic
let networkRequests = [];
@ -339,6 +423,59 @@ test.describe('Display Layout', () => {
});
});
async function addAndRemoveDrawingObjectAndAssert(page, layoutObject, DISPLAY_LAYOUT_NAME) {
expect(await page.getByLabel(layoutObject, { exact: true }).count()).toBe(0);
await addLayoutObject(page, DISPLAY_LAYOUT_NAME, layoutObject);
expect(
await page
.getByLabel(layoutObject, {
exact: true
})
.count()
).toBe(1);
await removeLayoutObject(page, layoutObject);
expect(await page.getByLabel(layoutObject, { exact: true }).count()).toBe(0);
}
/**
* Remove the first matching layout object from the layout
* @param {import('@playwright/test').Page} page
* @param {'Box' | 'Ellipse' | 'Line' | 'Text' | 'Image'} layoutObject
*/
async function removeLayoutObject(page, layoutObject) {
await page
.getByLabel(`Move ${layoutObject} Frame`, { exact: true })
.or(page.getByLabel(layoutObject, { exact: true }))
.first()
// eslint-disable-next-line playwright/no-force-option
.click({ force: true });
await page.getByTitle('Delete the selected object').click();
await page.getByRole('button', { name: 'OK' }).click();
}
/**
* Add a layout object to the specified layout
* @param {import('@playwright/test').Page} page
* @param {string} layoutName
* @param {'Box' | 'Ellipse' | 'Line' | 'Text' | 'Image'} layoutObject
*/
async function addLayoutObject(page, layoutName, layoutObject) {
await page.getByLabel(`${layoutName} Layout`, { exact: true }).click();
await page.getByText('Add Drawing Object').click();
await page
.getByRole('menuitem', {
name: layoutObject
})
.click();
if (layoutObject === 'Text') {
await page.getByRole('textbox', { name: 'Text' }).fill('Hello, Universe!');
await page.getByText('OK').click();
} else if (layoutObject === 'Image') {
await page.getByLabel('Image URL').fill(TINY_IMAGE_BASE64);
await page.getByText('OK').click();
}
}
/**
* Util for subscribing to a telemetry object by object identifier
* Limitations: Currently only works to return telemetry once to the node scope

View File

@ -40,7 +40,7 @@ test.describe('The Fault Management Plugin using example faults', () => {
}) => {
await utils.selectFaultItem(page, 1);
await page.getByRole('tab', { name: 'Fault Management Configuration' }).click();
await page.getByRole('tab', { name: 'Config' }).click();
const selectedFaultName = await page
.locator('.c-fault-mgmt__list.is-selected .c-fault-mgmt__list-faultname')
.textContent();
@ -65,7 +65,7 @@ test.describe('The Fault Management Plugin using example faults', () => {
);
expect.soft(await selectedRows.count()).toEqual(2);
await page.getByRole('tab', { name: 'Fault Management Configuration' }).click();
await page.getByRole('tab', { name: 'Config' }).click();
const firstSelectedFaultName = await selectedRows.nth(0).textContent();
const secondSelectedFaultName = await selectedRows.nth(1).textContent();
const firstNameInInspectorCount = await page

View File

@ -19,12 +19,19 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/* global __dirname */
const { test, expect } = require('../../../../pluginFixtures');
const {
createDomainObjectWithDefaults,
setIndependentTimeConductorBounds
} = require('../../../../appActions');
const path = require('path');
const LOCALSTORAGE_PATH = path.resolve(
__dirname,
'../../../../test-data/flexible_layout_with_child_layouts.json'
);
test.describe('Flexible Layout', () => {
let sineWaveObject;
@ -81,7 +88,7 @@ test.describe('Flexible Layout', () => {
await expect(dragWrapper).toHaveAttribute('draggable', 'true');
// Save Flexible Layout
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Check that panes are not draggable while Flexible Layout is in Browse mode
dragWrapper = page.locator('.c-fl-container__frames-holder .c-fl-frame__drag-wrapper').first();
await expect(dragWrapper).toHaveAttribute('draggable', 'false');
@ -167,7 +174,7 @@ test.describe('Flexible Layout', () => {
// Add the Sine Wave Generator to the Flexible Layout and save changes
await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first());
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
expect.soft(await page.locator('.c-fl-container__frame').count()).toEqual(1);
@ -198,7 +205,7 @@ test.describe('Flexible Layout', () => {
// Add the Sine Wave Generator to the Flexible Layout and save changes
await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first());
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
expect.soft(await page.locator('.c-fl-container__frame').count()).toEqual(1);
@ -239,7 +246,7 @@ test.describe('Flexible Layout', () => {
await exampleImageryTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first());
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// flip on independent time conductor
await setIndependentTimeConductorBounds(
@ -257,3 +264,53 @@ test.describe('Flexible Layout', () => {
await expect(page.getByText('2021-12-30 01:11:00.000Z')).toBeHidden();
});
});
test.describe('Flexible Layout Toolbar Actions @localStorage', () => {
test.use({
storageState: path.resolve(__dirname, LOCALSTORAGE_PATH)
});
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
await page
.locator('a')
.filter({ hasText: 'Parent Flexible Layout Flexible Layout' })
.first()
.click();
await page.getByLabel('Edit').click();
});
test('Add/Remove Container', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7234'
});
expect(await page.getByRole('group', { name: 'Container' }).count()).toEqual(2);
await page.getByRole('group', { name: 'Container' }).nth(1).click();
await page.getByTitle('Add Container').click();
expect(await page.getByRole('group', { name: 'Container' }).count()).toEqual(3);
await page.getByTitle('Remove Container').click();
await expect(page.getByRole('dialog')).toHaveText(
'This action will permanently delete this container from this Flexible Layout. Do you want to continue?'
);
await page.getByRole('button', { name: 'OK' }).click();
expect(await page.getByRole('group', { name: 'Container' }).count()).toEqual(2);
});
test('Remove Frame', async ({ page }) => {
expect(await page.getByRole('group', { name: 'Frame' }).count()).toEqual(2);
await page.getByRole('group', { name: 'Child Layout 1' }).click();
await page.getByTitle('Remove Frame').click();
await expect(page.getByRole('dialog')).toHaveText(
'This action will remove this frame from this Flexible Layout. Do you want to continue?'
);
await page.getByRole('button', { name: 'OK' }).click();
expect(await page.getByRole('group', { name: 'Frame' }).count()).toEqual(1);
});
test('Columns/Rows Layout Toggle', async ({ page }) => {
await page.getByRole('group', { name: 'Container' }).nth(1).click();
expect(await page.locator('.c-fl--rows').count()).toEqual(0);
await page.getByTitle('Columns layout').click();
expect(await page.locator('.c-fl--rows').count()).toEqual(1);
await page.getByTitle('Rows layout').click();
expect(await page.locator('.c-fl--rows').count()).toEqual(0);
});
});

View File

@ -25,7 +25,10 @@
*/
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
const {
createDomainObjectWithDefaults,
createExampleTelemetryObject
} = require('../../../../appActions');
const uuid = require('uuid').v4;
test.describe('Gauge', () => {
@ -53,7 +56,7 @@ test.describe('Gauge', () => {
await editButtonLocator.click();
await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeVisible();
await saveButtonLocator.click();
await page.locator('li[title="Save and Finish Editing"]').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Create another sine wave generator within the gauge
const swg2 = await createDomainObjectWithDefaults(page, {
@ -133,4 +136,50 @@ test.describe('Gauge', () => {
// TODO: Verify changes in the UI
});
test('Gauge does not display NaN when data not available', async ({ page }) => {
// Create a Gauge
const gauge = await createDomainObjectWithDefaults(page, {
type: 'Gauge'
});
// Create a Sine Wave Generator in the Gauge with a loading delay
const swgWith5sDelay = await createExampleTelemetryObject(page, gauge.uuid);
await page.goto(swgWith5sDelay.url);
await page.getByTitle('More options').click();
await page.getByRole('menuitem', { name: /Edit Properties.../ }).click();
//Edit Example Telemetry Object to include 5s loading Delay
await page.locator('[aria-label="Loading Delay \\(ms\\)"]').fill('5000');
await page.getByRole('button', { name: 'Save' }).click();
// Wait until the URL is updated
await page.waitForURL(`**/${gauge.uuid}/*`);
// Nav to the Gauge
await page.goto(gauge.url);
const gaugeNoDataText = await page.locator('.js-dial-current-value tspan').textContent();
expect(gaugeNoDataText).toBe('--');
});
test('Gauge enforces composition policy', async ({ page }) => {
// Create a Gauge
await createDomainObjectWithDefaults(page, {
type: 'Gauge',
name: 'Unnamed Gauge'
});
// Try to create a Folder into the Gauge. Should be disallowed.
await page.getByRole('button', { name: /Create/ }).click();
await page.getByRole('menuitem', { name: /Folder/ }).click();
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
await page.getByLabel('Cancel').click();
// Try to create a Display Layout into the Gauge. Should be disallowed.
await page.getByRole('button', { name: /Create/ }).click();
await page.getByRole('menuitem', { name: /Display Layout/ }).click();
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
});
});

View File

@ -57,6 +57,10 @@ test.describe('Example Imagery Object', () => {
await mouseZoomOnImageAndAssert(page, -2);
});
test('Compass HUD should be hidden by default', async ({ page }) => {
await expect(page.locator('.c-hud')).toBeHidden();
});
test('Can adjust image brightness/contrast by dragging the sliders', async ({
page,
browserName
@ -198,23 +202,26 @@ test.describe('Example Imagery Object', () => {
expect(afterDownPanBoundingBox.y).toBeLessThan(afterUpPanBoundingBox.y);
});
test('Can use alt+shift+drag to create a tag', async ({ page }) => {
test('Can use alt+shift+drag to create a tag and ensure toolbars disappear', async ({ page }) => {
const canvas = page.locator('canvas');
await canvas.hover({ trial: true });
const canvasBoundingBox = await canvas.boundingBox();
const canvasCenterX = canvasBoundingBox.x + canvasBoundingBox.width / 2;
const canvasCenterY = canvasBoundingBox.y + canvasBoundingBox.height / 2;
await Promise.all(tagHotkey.map((x) => page.keyboard.down(x)));
await page.mouse.down();
// steps not working for me here
await page.mouse.move(canvasCenterX - 20, canvasCenterY - 20);
await page.mouse.move(canvasCenterX - 100, canvasCenterY - 100);
// toolbar should hide when we're creating annotations with a drag
await expect(page.locator('[role="toolbar"][aria-label="Image controls"]')).toBeHidden();
await page.mouse.up();
// toolbar should reappear when we're done creating annotations
await expect(page.locator('[role="toolbar"][aria-label="Image controls"]')).toBeVisible();
await Promise.all(tagHotkey.map((x) => page.keyboard.up(x)));
//Wait for canvas to stabilize.
// Wait for canvas to stabilize.
await canvas.hover({ trial: true });
// add some tags
@ -226,6 +233,20 @@ test.describe('Example Imagery Object', () => {
await page.getByRole('button', { name: /Add Tag/ }).click();
await page.getByPlaceholder('Type to select tag').click();
await page.getByText('Science').click();
// click on a separate part of the canvas to ensure no tags appear
await page.mouse.click(canvasCenterX + 10, canvasCenterY + 10);
await expect(page.getByText('Driving')).toBeHidden();
await expect(page.getByText('Science')).toBeHidden();
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7083'
});
// click on annotation again and expect tags to appear
await page.mouse.click(canvasCenterX - 50, canvasCenterY - 50);
await expect(page.getByText('Driving')).toBeVisible();
await expect(page.getByText('Science')).toBeVisible();
});
test('Can use + - buttons to zoom on the image @unstable', async ({ page }) => {

View File

@ -25,21 +25,24 @@ const {
createDomainObjectWithDefaults,
setStartOffset,
setFixedTimeMode,
setRealTimeMode
setRealTimeMode,
openObjectTreeContextMenu
} = require('../../../../appActions');
test.describe('Testing LAD table configuration', () => {
let ladTable;
let sineWaveObject;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
// Create LAD table
const ladTable = await createDomainObjectWithDefaults(page, {
ladTable = await createDomainObjectWithDefaults(page, {
type: 'LAD Table',
name: 'Test LAD Table'
});
// Create Sine Wave Generator
await createDomainObjectWithDefaults(page, {
sineWaveObject = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'Test Sine Wave Generator',
parent: ladTable.uuid
@ -50,24 +53,28 @@ test.describe('Testing LAD table configuration', () => {
test('in edit mode, LAD Tables provide ability to hide columns', async ({ page }) => {
// Edit LAD table
await page.locator('[title="Edit"]').click();
// // 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 LAD table and save changes
// await page.dragAndDrop('role=treeitem[name=/Test Sine Wave Generator/]', '.c-lad-table-wrapper');
// select configuration tab in inspector
await page.getByRole('tab', { name: 'LAD Table Configuration' }).click();
// make sure headers are visible initially
await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'Units' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'Type' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'WATCH' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'WARNING' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'DISTRESS' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'CRITICAL' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'SEVERE' })).toBeVisible();
// hide timestamp column
await page.getByLabel('Timestamp').uncheck();
await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeHidden();
await expect(page.getByRole('cell', { name: 'Units' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'Type' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'WATCH' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'WARNING' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'DISTRESS' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'CRITICAL' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'SEVERE' })).toBeVisible();
// hide units & type column
await page.getByLabel('Units').uncheck();
@ -75,14 +82,35 @@ test.describe('Testing LAD table configuration', () => {
await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeHidden();
await expect(page.getByRole('cell', { name: 'Units' })).toBeHidden();
await expect(page.getByRole('cell', { name: 'Type' })).toBeHidden();
await expect(page.getByRole('cell', { name: 'WATCH' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'WARNING' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'DISTRESS' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'CRITICAL' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'SEVERE' })).toBeVisible();
// hide WATCH column
await page.getByLabel('WATCH').uncheck();
await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeHidden();
await expect(page.getByRole('cell', { name: 'Units' })).toBeHidden();
await expect(page.getByRole('cell', { name: 'Type' })).toBeHidden();
await expect(page.getByRole('cell', { name: 'WATCH' })).toBeHidden();
await expect(page.getByRole('cell', { name: 'WARNING' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'DISTRESS' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'CRITICAL' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'SEVERE' })).toBeVisible();
// save and reload and verify they columns are still hidden
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await page.reload();
await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeHidden();
await expect(page.getByRole('cell', { name: 'Units' })).toBeHidden();
await expect(page.getByRole('cell', { name: 'Type' })).toBeHidden();
await expect(page.getByRole('cell', { name: 'WATCH' })).toBeHidden();
await expect(page.getByRole('cell', { name: 'WARNING' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'DISTRESS' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'CRITICAL' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'SEVERE' })).toBeVisible();
// Edit LAD table
await page.locator('[title="Edit"]').click();
@ -93,33 +121,99 @@ test.describe('Testing LAD table configuration', () => {
await expect(page.getByRole('cell', { name: 'Units' })).toBeHidden();
await expect(page.getByRole('cell', { name: 'Type' })).toBeHidden();
await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'WATCH' })).toBeHidden();
await expect(page.getByRole('cell', { name: 'WARNING' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'DISTRESS' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'CRITICAL' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'SEVERE' })).toBeVisible();
// save and reload and make sure only timestamp is still visible
// save and reload and make sure timestamp is still visible
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await page.reload();
await expect(page.getByRole('cell', { name: 'Units' })).toBeHidden();
await expect(page.getByRole('cell', { name: 'Type' })).toBeHidden();
await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'WATCH' })).toBeHidden();
await expect(page.getByRole('cell', { name: 'WARNING' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'DISTRESS' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'CRITICAL' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'SEVERE' })).toBeVisible();
// Edit LAD table
await page.locator('[title="Edit"]').click();
await page.getByRole('tab', { name: 'LAD Table Configuration' }).click();
// show units and type columns
// show units, type, and WATCH columns
await page.getByLabel('Units').check();
await page.getByLabel('Type').check();
await page.getByLabel('WATCH').check();
await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'Units' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'Type' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'WATCH' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'WARNING' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'DISTRESS' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'CRITICAL' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'SEVERE' })).toBeVisible();
// save and reload and make sure all columns are still visible
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await page.reload();
await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'Units' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'Type' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'WATCH' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'WARNING' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'DISTRESS' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'CRITICAL' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'SEVERE' })).toBeVisible();
});
test('When adding something without Units, do not show Units column', async ({ page }) => {
// Create Sine Wave Generator
await createDomainObjectWithDefaults(page, {
type: 'Event Message Generator',
parent: ladTable.uuid
});
await page.goto(ladTable.url);
// Edit LAD table
await page.getByLabel('Edit').click();
await page.getByRole('tab', { name: 'LAD Table Configuration' }).click();
// make sure Sine Wave headers are visible initially too
await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'Units' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'Type' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'WATCH' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'WARNING' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'DISTRESS' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'CRITICAL' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'SEVERE' })).toBeVisible();
// save and reload and verify they columns are still hidden
await page.getByLabel('Save').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Remove Sin Wave Generator
openObjectTreeContextMenu(page, sineWaveObject.url);
await page.getByRole('menuitem', { name: /Remove/ }).click();
await page.getByRole('button', { name: 'OK' }).click();
// Ensure Units & Limit columns are gone
// as Event Generator don't have them
await page.goto(ladTable.url);
await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'Type' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'Units' })).toBeHidden();
await expect(page.getByRole('cell', { name: 'WATCH' })).toBeHidden();
await expect(page.getByRole('cell', { name: 'WARNING' })).toBeHidden();
await expect(page.getByRole('cell', { name: 'DISTRESS' })).toBeHidden();
await expect(page.getByRole('cell', { name: 'CRITICAL' })).toBeHidden();
await expect(page.getByRole('cell', { name: 'SEVERE' })).toBeHidden();
});
test("LAD Tables don't allow selection of rows but does show context click menus", async ({
@ -171,7 +265,7 @@ test.describe('Testing LAD table @unstable', () => {
// Add the Sine Wave Generator to the LAD table and save changes
await page.dragAndDrop('text=Test Sine Wave Generator', '.c-lad-table-wrapper');
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Subscribe to the Sine Wave Generator data
// On getting data, check if the value found in the LAD table is the most recent value
@ -199,7 +293,7 @@ test.describe('Testing LAD table @unstable', () => {
// Add the Sine Wave Generator to the LAD table and save changes
await page.dragAndDrop('text=Test Sine Wave Generator', '.c-lad-table-wrapper');
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Subscribe to the Sine Wave Generator data
const getTelemValuePromise = await subscribeToTelemetry(page, sineWaveObject.uuid);

View File

@ -279,8 +279,8 @@ test.describe('Notebook entry tests', () => {
// Click .c-notebook__drag-area
await page.locator('.c-notebook__drag-area').click();
await expect(page.locator('[aria-label="Notebook Entry Input"]')).toBeVisible();
await expect(page.locator('[aria-label="Notebook Entry"]')).toHaveClass(/is-selected/);
await expect(page.getByLabel('Notebook Entry Input')).toBeVisible();
await expect(page.getByLabel('Notebook Entry', { exact: true })).toHaveClass(/is-selected/);
});
test('When an object is dropped into a notebook, a new entry is created and it should be focused @unstable', async ({
page
@ -369,6 +369,8 @@ test.describe('Notebook entry tests', () => {
const validLink = page.locator(`a[href="${TEST_LINK}"]`);
expect(await validLink.count()).toBe(1);
// Start waiting for popup before clicking. Note no await.
const popupPromise = page.waitForEvent('popup');
@ -378,8 +380,6 @@ test.describe('Notebook entry tests', () => {
// Wait for the popup to load.
await popup.waitForLoadState();
expect.soft(popup.url()).toContain('www.google.com');
expect(await validLink.count()).toBe(1);
});
test('when an invalid link is entered into a notebook entry, it does not become clickable when viewing', async ({
page
@ -447,6 +447,8 @@ test.describe('Notebook entry tests', () => {
const validLink = page.locator(`a[href="${TEST_LINK}"]`);
expect(await validLink.count()).toBe(1);
// Start waiting for popup before clicking. Note no await.
const popupPromise = page.waitForEvent('popup');
@ -456,8 +458,6 @@ test.describe('Notebook entry tests', () => {
// Wait for the popup to load.
await popup.waitForLoadState();
expect.soft(popup.url()).toContain('www.google.com');
expect(await validLink.count()).toBe(1);
});
test('when a nefarious link is entered into a notebook entry, it is sanitized when viewing', async ({
page
@ -482,4 +482,55 @@ test.describe('Notebook entry tests', () => {
expect.soft(await sanitizedLink.count()).toBe(1);
expect(await unsanitizedLink.count()).toBe(0);
});
test('Can add markdown to a notebook entry', async ({ page }) => {
await page.goto(notebookObject.url);
// Headers
const headerMarkdown = `# Big Header\n## Large Header\n### Medium Header\n#### Small Header`;
await nbUtils.enterTextEntry(page, headerMarkdown);
await expect(page.getByRole('heading', { name: 'Big Header' })).toBeVisible();
// Text markup
const markupText =
'**This is bold.** _This is italic_. `This is code`. ~This is strikethrough~';
await nbUtils.enterTextEntry(page, markupText);
await expect(page.locator('strong:has-text("This is bold.")')).toBeVisible();
// Tables
const tablesText = '|Col 1|Col 2|Col3|\n|-|-|-|\n |Value 1|Value 2|Value 3|\n';
await nbUtils.enterTextEntry(page, tablesText);
await expect(page.getByRole('cell', { name: 'Value 2' })).toBeVisible();
// Links
const linksText =
'Raw links https://www.google.com and Markdown links like [Google](https://www.google.com) work';
await nbUtils.enterTextEntry(page, linksText);
await expect(page.getByRole('link', { name: 'https://www.google.com' })).toBeVisible();
await expect(page.getByRole('link', { name: 'Google', exact: true })).toBeVisible();
// Lists
const listsText = '- List item 1\n - Item 1A \n- List Item 2\n 1. Order 1\n 1. Order 2\n';
await nbUtils.enterTextEntry(page, listsText);
const childItem = page.locator('li:has-text("List Item 2") ol li:has-text("Order 2")');
await expect(childItem).toBeVisible();
// Code Blocks
const codeblockTest = '```javascript\nconst foo = "bar";\nconst bar = "foo";\n```';
await nbUtils.enterTextEntry(page, codeblockTest);
const codeBlock = page.locator('code.language-javascript:has-text("const foo = \\"bar\\";")');
await expect(codeBlock).toBeVisible();
// Blockquotes
const blockquoteTest =
'This is a quote by Mark Twain:\n> "The man with a new idea is a crank\n>until the idea succeeds."';
await nbUtils.enterTextEntry(page, blockquoteTest);
const firstLineOfBlockquoteText = page.locator(
'blockquote:has-text("The man with a new idea is a crank")'
);
await expect(firstLineOfBlockquoteText).toBeVisible();
const secondLineOfBlockquoteText = page.locator(
'blockquote:has-text("until the idea succeeds")'
);
await expect(secondLineOfBlockquoteText).toBeVisible();
});
});

View File

@ -188,14 +188,11 @@ test.describe('Snapshot image tests', () => {
}, fileData);
await page.dispatchEvent('.c-notebook__drag-area', 'drop', { dataTransfer: dropTransfer });
await page.locator('.c-ne__save-button > button').click();
// be sure that entry was created
await expect(page.getByText('favicon-96x96.png')).toBeVisible();
// click on image (need to click twice to focus)
await page.getByRole('img', { name: 'favicon-96x96.png thumbnail' }).click();
await page.getByRole('img', { name: 'favicon-96x96.png thumbnail' }).click();
// expect large image to be displayed
await expect(page.getByRole('dialog').getByText('favicon-96x96.png')).toBeVisible();
@ -217,3 +214,59 @@ test.describe('Snapshot image tests', () => {
expect(await page.getByRole('img', { name: 'favicon-96x96.png thumbnail' }).count()).toBe(1);
});
});
test.describe('Snapshot image failure tests', () => {
test.use({ failOnConsoleError: false });
test.beforeEach(async ({ page }) => {
//Navigate to baseURL
await page.goto('./', { waitUntil: 'domcontentloaded' });
// Create Notebook
await createDomainObjectWithDefaults(page, {
type: NOTEBOOK_NAME
});
});
test('Get an error notification when dropping unknown file onto notebook entry', async ({
page
}) => {
// fill Uint8Array array with some garbage data
const garbageData = new Uint8Array(100);
const fileData = Array.from(garbageData);
const dropTransfer = await page.evaluateHandle((data) => {
const dataTransfer = new DataTransfer();
const file = new File([new Uint8Array(data)], 'someGarbage.foo', { type: 'unknown/garbage' });
dataTransfer.items.add(file);
return dataTransfer;
}, fileData);
await page.dispatchEvent('.c-notebook__drag-area', 'drop', { dataTransfer: dropTransfer });
// should have gotten a notification from OpenMCT that we couldn't add it
await expect(page.getByText('Unknown object(s) dropped and cannot embed')).toBeVisible();
});
test('Get an error notification when dropping big files onto notebook entry', async ({
page
}) => {
const garbageSize = 15 * 1024 * 1024; // 15 megabytes
await page.addScriptTag({
// make the garbage client side
content: `window.bigGarbageData = new Uint8Array(${garbageSize})`
});
const bigDropTransfer = await page.evaluateHandle(() => {
const dataTransfer = new DataTransfer();
const file = new File([window.bigGarbageData], 'bigBoy.png', { type: 'image/png' });
dataTransfer.items.add(file);
return dataTransfer;
});
await page.dispatchEvent('.c-notebook__drag-area', 'drop', { dataTransfer: bigDropTransfer });
// should have gotten a notification from OpenMCT that we couldn't add it as it's too big
await expect(page.getByText('unable to embed')).toBeVisible();
});
});

View File

@ -147,16 +147,13 @@ test.describe('Tagging in Notebooks @addInit', () => {
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5823'
});
await createNotebookEntryAndTags(page);
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
const entryLocator = `[aria-label="Notebook Entry Input"] >> nth = 1`;
await page.locator(entryLocator).click();
await page.locator(entryLocator).fill(`An entry without tags`);
await page.locator('[aria-label="Notebook Entry Input"] >> nth=1').press('Enter');
await page.getByLabel('Notebook Entry Input').fill(`An entry without tags`);
await page.locator('.c-ne__save-button > button').click();
await page.hover('[aria-label="Notebook Entry Input"] >> nth=1');
await page.hover('[aria-label="Notebook Entry Display"] >> nth=1');
await page.locator('button[title="Delete this entry"]').last().click();
await expect(
page.locator('text=This action will permanently delete this entry. Do you wish to continue?')

View File

@ -68,7 +68,7 @@ test.describe('Autoscale', () => {
// save
await page.click('button[title="Save"]');
await Promise.all([
page.locator('li[title = "Save and Finish Editing"]').click(),
page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);

View File

@ -214,7 +214,7 @@ async function saveOverlayPlot(page) {
.click();
await Promise.all([
page.locator('text=Save and Finish Editing').click(),
page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);

View File

@ -105,7 +105,7 @@ test.describe('Overlay Plot', () => {
// Save (exit edit mode)
await page.locator('button[title="Save"]').click();
await page.locator('li[title="Save and Finish Editing"]').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await assertLimitLinesExistAndAreVisible(page);
@ -127,7 +127,7 @@ test.describe('Overlay Plot', () => {
// Save (exit edit mode)
await page.locator('button[title="Save"]').click();
await page.locator('li[title="Save and Finish Editing"]').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await assertLimitLinesExistAndAreVisible(page);

View File

@ -57,7 +57,7 @@ test.describe('Scatter Plot', () => {
await page.getByRole('tab', { name: 'Elements' }).click();
await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeVisible();
await saveButton.click();
await page.locator('li[title="Save and Finish Editing"]').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Create another sine wave generator within the scatter plot
const swg2 = await createDomainObjectWithDefaults(page, {

View File

@ -135,7 +135,7 @@ test.describe('Stacked Plot', () => {
// Save (exit edit mode)
await page.locator('button[title="Save"]').click();
await page.locator('li[title="Save and Finish Editing"]').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// assert plot order persists after save - [swgB, swgC, swgA]
await expect(stackedPlotItem1).toHaveAttribute('aria-label', `Stacked Plot Item ${swgB.name}`);
@ -243,7 +243,7 @@ test.describe('Stacked Plot', () => {
// Save (exit edit mode)
await page.locator('button[title="Save"]').click();
await page.locator('li[title="Save and Finish Editing"]').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await assertAggregateLegendIsVisible(page);

View File

@ -0,0 +1,74 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
const { createDomainObjectWithDefaults } = require('../../../../appActions');
const { test, expect } = require('../../../../pluginFixtures');
test.describe('Tabs View', () => {
test('Renders tabbed elements', async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
const tabsView = await createDomainObjectWithDefaults(page, {
type: 'Tabs View'
});
const table = await createDomainObjectWithDefaults(page, {
type: 'Telemetry Table',
parent: tabsView.uuid
});
await createDomainObjectWithDefaults(page, {
type: 'Event Message Generator',
parent: table.uuid
});
const notebook = await createDomainObjectWithDefaults(page, {
type: 'Notebook',
parent: tabsView.uuid
});
const sineWaveGenerator = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: tabsView.uuid
});
page.goto(tabsView.url);
// select first tab
await page.getByLabel(`${table.name} tab`).click();
// ensure table header visible
await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible();
// select second tab
await page.getByLabel(`${notebook.name} tab`).click();
// ensure notebook visible
await expect(page.locator('.c-notebook__drag-area')).toBeVisible();
// select third tab
await page.getByLabel(`${sineWaveGenerator.name} tab`).click();
// expect sine wave generator visible
expect(await page.locator('.c-plot').isVisible()).toBe(true);
// now try to select the first tab again
await page.getByLabel(`${table.name} tab`).click();
// ensure table header visible
await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible();
});
});

View File

@ -78,4 +78,85 @@ test.describe('Telemetry Table', () => {
const endBoundMilliseconds = Date.parse(endDate);
expect(latestMilliseconds).toBeLessThanOrEqual(endBoundMilliseconds);
});
test('Supports filtering telemetry by regular text search', async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
const table = await createDomainObjectWithDefaults(page, { type: 'Telemetry Table' });
await createDomainObjectWithDefaults(page, {
type: 'Event Message Generator',
parent: table.uuid
});
// focus the Telemetry Table
await page.goto(table.url);
await page.getByRole('searchbox', { name: 'message filter input' }).click();
await page.getByRole('searchbox', { name: 'message filter input' }).fill('Roger');
let cells = await page.getByRole('cell', { name: /Roger/ }).all();
// ensure we've got more than one cell
expect(cells.length).toBeGreaterThan(1);
// ensure the text content of each cell contains the search term
for (const cell of cells) {
const text = await cell.textContent();
expect(text).toContain('Roger');
}
await page.getByRole('searchbox', { name: 'message filter input' }).click();
await page.getByRole('searchbox', { name: 'message filter input' }).fill('Dodger');
cells = await page.getByRole('cell', { name: /Dodger/ }).all();
// ensure we've got more than one cell
expect(cells.length).toBe(0);
// ensure the text content of each cell contains the search term
for (const cell of cells) {
const text = await cell.textContent();
expect(text).not.toContain('Dodger');
}
// Click pause button
await page.click('button[title="Pause"]');
});
test('Supports filtering using Regex', async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
const table = await createDomainObjectWithDefaults(page, { type: 'Telemetry Table' });
await createDomainObjectWithDefaults(page, {
type: 'Event Message Generator',
parent: table.uuid
});
// focus the Telemetry Table
page.goto(table.url);
await page.getByRole('searchbox', { name: 'message filter input' }).hover();
await page.getByLabel('Message filter header').getByRole('button', { name: '/R/' }).click();
await page.getByRole('searchbox', { name: 'message filter input' }).click();
await page.getByRole('searchbox', { name: 'message filter input' }).fill('/[Rr]oger/');
let cells = await page.getByRole('cell', { name: /Roger/ }).all();
// ensure we've got more than one cell
expect(cells.length).toBeGreaterThan(1);
// ensure the text content of each cell contains the search term
for (const cell of cells) {
const text = await cell.textContent();
expect(text).toContain('Roger');
}
await page.getByRole('searchbox', { name: 'message filter input' }).click();
await page.getByRole('searchbox', { name: 'message filter input' }).fill('/[Dd]oger/');
cells = await page.getByRole('cell', { name: /Dodger/ }).all();
// ensure we've got more than one cell
expect(cells.length).toBe(0);
// ensure the text content of each cell contains the search term
for (const cell of cells) {
const text = await cell.textContent();
expect(text).not.toContain('Dodger');
}
// Click pause button
await page.click('button[title="Pause"]');
});
});

View File

@ -77,19 +77,19 @@ test.describe('Grand Search', () => {
// Click [aria-label="OpenMCT Search"] a >> nth=0
await page.locator('[aria-label="Search Result"] >> nth=0').click();
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeInViewport();
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeHidden();
// Fill [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('foo');
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).not.toBeInViewport();
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeHidden();
// Click text=Snapshot Save and Finish Editing Save and Continue Editing >> button >> nth=1
await page
.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button')
.nth(1)
.click();
// Click text=Save and Finish Editing
await page.locator('text=Save and Finish Editing').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Click [aria-label="OpenMCT Search"] [aria-label="Search Input"]
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click();
// Fill [aria-label="OpenMCT Search"] [aria-label="Search Input"]

View File

@ -89,20 +89,6 @@ test.describe('Verify tooltips', () => {
await expandEntireTree(page);
});
// LAD Tables - DONE
// Expanded collapsed plot legend - DONE
// Object Labels - DONE
// Display Layout headers - DONE
// Flexible Layout headers - DONE
// Tab View layout headers - DONE
// Search - DONE
// Gauge -
// Notebook Embed - DONE
// Telemetry Table -
// Timeline Objects
// Tree - DONE
// Recent Objects
test('display correct paths for LAD tables', async ({ page, openmctConfig }) => {
// Create LAD table
await createDomainObjectWithDefaults(page, {
@ -117,7 +103,7 @@ test.describe('Verify tooltips', () => {
await page.dragAndDrop(`text=${sineWaveObject2.name}`, '.c-lad-table-wrapper');
await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-lad-table-wrapper');
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await page.keyboard.down('Control');
@ -147,7 +133,7 @@ test.describe('Verify tooltips', () => {
await page.dragAndDrop(`text=${sineWaveObject2.name}`, '.gl-plot');
await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.gl-plot');
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await page.keyboard.down('Control');
@ -214,7 +200,7 @@ test.describe('Verify tooltips', () => {
await page.locator('[title="Edit"]').click();
await page.dragAndDrop(`text=${sineWaveObject1.name}`, '.gl-plot');
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Create Stacked Plot
await createDomainObjectWithDefaults(page, {
@ -225,7 +211,7 @@ test.describe('Verify tooltips', () => {
await page.locator('[title="Edit"]').click();
await page.dragAndDrop(`text=${sineWaveObject2.name}`, '.c-plot--stacked.holder');
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Create Display Layout
await createDomainObjectWithDefaults(page, {
@ -245,7 +231,7 @@ test.describe('Verify tooltips', () => {
targetPosition: { x: 500, y: 200 }
});
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await page.keyboard.down('Control');
@ -254,13 +240,13 @@ test.describe('Verify tooltips', () => {
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe('My Items / Test Overlay Plot');
// await page.keyboard.up('Control');
// await page.locator('.c-plot-legend__view-control >> nth=0').click();
// await page.keyboard.down('Control');
// await page.locator('.plot-wrapper-expanded-legend .plot-series-name').first().hover();
// tooltipText = await page.locator('.c-tooltip').textContent();
// tooltipText = tooltipText.replace('\n', '').trim();
// expect(tooltipText).toBe(sineWaveObject1.path);
await page.keyboard.up('Control');
await page.locator('.c-plot-legend__view-control >> nth=0').click();
await page.keyboard.down('Control');
await page.locator('.plot-wrapper-expanded-legend .plot-series-name').first().hover();
tooltipText = await page.locator('.c-tooltip').textContent();
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe(sineWaveObject1.path);
await page.getByText('Test Stacked Plot').nth(2).hover();
tooltipText = await page.locator('.c-tooltip').textContent();
@ -283,7 +269,7 @@ test.describe('Verify tooltips', () => {
await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-fl__container >> nth=1');
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await page.keyboard.down('Control');
await page.getByText('SWG 1').nth(2).hover();
@ -307,7 +293,7 @@ test.describe('Verify tooltips', () => {
await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-tabs-view__tabs-holder');
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await page.keyboard.down('Control');
await page.getByText('SWG 1').nth(2).hover();
@ -345,18 +331,18 @@ test.describe('Verify tooltips', () => {
expect(tooltipText).toBe(sineWaveObject3.path);
});
test('display path for source telemetry when hovering over gauge', ({ page }) => {
expect(true).toBe(true);
// await createDomainObjectWithDefaults(page, {
// type: 'Gauge',
// name: 'Test Gauge'
// });
// await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-gauge__wrapper');
// await page.keyboard.down('Control');
// await page.locator('.c-gauge__current-value-text-wrapper').hover();
// let tooltipText = await page.locator('.c-tooltip').textContent();
// tooltipText = tooltipText.replace('\n', '').trim();
// expect(tooltipText).toBe(sineWaveObject3.path);
test('display path for source telemetry when hovering over gauge', async ({ page }) => {
await createDomainObjectWithDefaults(page, {
type: 'Gauge',
name: 'Test Gauge'
});
await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-gauge__wrapper');
await page.keyboard.down('Control');
// eslint-disable-next-line playwright/no-force-option
await page.locator('.c-gauge.c-dial').hover({ position: { x: 0, y: 0 }, force: true });
let tooltipText = await page.locator('.c-tooltip').textContent();
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe(sineWaveObject3.path);
});
test('display tooltip path for notebook embeds', async ({ page }) => {
@ -373,26 +359,105 @@ test.describe('Verify tooltips', () => {
expect(tooltipText).toBe(sineWaveObject3.path);
});
// test('display tooltip path for telemetry table names', async ({ page }) => {
// await setEndOffset(page, { secs: '10' });
// await createDomainObjectWithDefaults(page, {
// type: 'Telemetry Table',
// name: 'Test Telemetry Table'
// });
test('display tooltip path for telemetry table names', async ({ page }) => {
// set endBound to 10 seconds after start bound
const url = await page.url();
const parsedUrl = new URL(url.replace('#', '!'));
const startBound = Number(parsedUrl.searchParams.get('tc.startBound'));
const tenSecondsInMilliseconds = 10 * 1000;
const endBound = startBound + tenSecondsInMilliseconds;
parsedUrl.searchParams.set('tc.endBound', endBound);
await page.goto(parsedUrl.href.replace('!', '#'));
// await page.dragAndDrop(`text=${sineWaveObject1.name}`, '.c-telemetry-table');
// await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-telemetry-table');
await createDomainObjectWithDefaults(page, {
type: 'Telemetry Table',
name: 'Test Telemetry Table'
});
// await page.locator('button[title="Save"]').click();
// await page.locator('text=Save and Finish Editing').click();
await page.dragAndDrop(`text=${sineWaveObject1.name}`, '.c-telemetry-table');
await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-telemetry-table');
// // .c-telemetry-table__body
await page.locator('button[title="Save"]').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await page.keyboard.down('Control');
// await page.keyboard.down('Control');
await page.locator('.noselect > [title="SWG 3"]').first().hover();
let tooltipText = await page.locator('.c-tooltip').textContent();
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe(sineWaveObject3.path);
// await page.locator('.noselect > [title="SWG 3"]').first().hover();
// let tooltipText = await page.locator('.c-tooltip').textContent();
// tooltipText = tooltipText.replace('\n', '').trim();
// expect(tooltipText).toBe(sineWaveObject3.path);
// });
await page.locator('.noselect > [title="SWG 1"]').first().hover();
tooltipText = await page.locator('.c-tooltip').textContent();
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe(sineWaveObject1.path);
});
test('display tooltip path for recently viewed items', async ({ page }) => {
// drag up Recently Viewed pane
await page
.locator('.l-pane.l-pane--vertical-handle-before', {
hasText: 'Recently Viewed'
})
.locator('.l-pane__handle')
.hover();
await page.mouse.down();
await page.mouse.move(0, 300);
await page.mouse.up();
await page.keyboard.down('Control');
await page.getByLabel('Recent Objects').getByText(sineWaveObject3.name).hover();
let tooltipText = await page.locator('.c-tooltip').textContent();
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe(sineWaveObject3.path);
await page.getByLabel('Recent Objects').getByText(sineWaveObject2.name).hover();
tooltipText = await page.locator('.c-tooltip').textContent();
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe(sineWaveObject2.path);
await page.getByLabel('Recent Objects').getByText(sineWaveObject1.name).hover();
tooltipText = await page.locator('.c-tooltip').textContent();
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe(sineWaveObject1.path);
});
test('display tooltip path for time strips', async ({ page }) => {
// Create Time Strip
await createDomainObjectWithDefaults(page, {
type: 'Time Strip',
name: 'Test Time Strip'
});
// Edit Overlay Plot
await page.locator('[title="Edit"]').click();
await page.dragAndDrop(
`text=${sineWaveObject1.name}`,
'.c-object-view.is-object-type-time-strip'
);
await page.dragAndDrop(
`text=${sineWaveObject2.name}`,
'.c-object-view.is-object-type-time-strip'
);
await page.dragAndDrop(
`text=${sineWaveObject3.name}`,
'.c-object-view.is-object-type-time-strip'
);
await page.locator('button[title="Save"]').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await page.keyboard.down('Control');
await page.getByText(sineWaveObject1.name).nth(2).hover();
let tooltipText = await page.locator('.c-tooltip').textContent();
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe(sineWaveObject1.path);
await page.getByText(sineWaveObject2.name).nth(2).hover();
tooltipText = await page.locator('.c-tooltip').textContent();
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe(sineWaveObject2.path);
await page.getByText(sineWaveObject3.name).nth(2).hover();
tooltipText = await page.locator('.c-tooltip').textContent();
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe(sineWaveObject3.path);
});
});

View File

@ -131,8 +131,8 @@ test.describe('Performance tests', () => {
await page.evaluate(() => window.performance.mark('new-notebook-entry-created'));
// Enter Notebook Entry text
await page.locator('div.c-ne__text').last().fill('New Entry');
await page.keyboard.press('Enter');
await page.getByLabel('Notebook Entry Input').last().fill('New Entry');
await page.locator('.c-ne__save-button').click();
await page.evaluate(() => window.performance.mark('new-notebook-entry-filled'));
//Individual Notebook Entry Search

View File

@ -0,0 +1,100 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
const { createDomainObjectWithDefaults, waitForPlotsToRender } = require('../../appActions');
const { test, expect } = require('../../pluginFixtures');
test.describe('Tabs View', () => {
test('Renders tabbed elements nicely', async ({ page }) => {
// Code to hook into the requestAnimationFrame function and log each call
let animationCalls = [];
await page.exposeFunction('logCall', (callCount) => {
animationCalls.push(callCount);
});
await page.addInitScript(() => {
const oldRequestAnimationFrame = window.requestAnimationFrame;
let callCount = 0;
window.requestAnimationFrame = function (callback) {
// eslint-disable-next-line no-undef
logCall(callCount++);
return oldRequestAnimationFrame(callback);
};
});
await page.goto('./', { waitUntil: 'domcontentloaded' });
const tabsView = await createDomainObjectWithDefaults(page, {
type: 'Tabs View'
});
const table = await createDomainObjectWithDefaults(page, {
type: 'Telemetry Table',
parent: tabsView.uuid
});
await createDomainObjectWithDefaults(page, {
type: 'Event Message Generator',
parent: table.uuid
});
const notebook = await createDomainObjectWithDefaults(page, {
type: 'Notebook',
parent: tabsView.uuid
});
const sineWaveGenerator = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: tabsView.uuid
});
page.goto(tabsView.url);
// select first tab
await page.getByLabel(`${table.name} tab`).click();
// ensure table header visible
await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible();
// select second tab
await page.getByLabel(`${notebook.name} tab`).click();
// expect notebook visible
await expect(page.locator('.c-notebook__drag-area')).toBeVisible();
// select third tab
await page.getByLabel(`${sineWaveGenerator.name} tab`).click();
// ensure sine wave generator visible
expect(await page.locator('.c-plot').isVisible()).toBe(true);
// now select notebook and clear animation calls
await page.getByLabel(`${notebook.name} tab`).click();
animationCalls = [];
// expect notebook visible
await expect(page.locator('.c-notebook__drag-area')).toBeVisible();
const notebookAnimationCalls = animationCalls.length;
// select sine wave generator and clear animation calls
animationCalls = [];
await page.getByLabel(`${sineWaveGenerator.name} tab`).click();
// ensure sine wave generator visible
await waitForPlotsToRender(page);
// we should be calling animation frames
const sineWaveAnimationCalls = animationCalls.length;
expect(sineWaveAnimationCalls).toBeGreaterThanOrEqual(notebookAnimationCalls);
});
});

View File

@ -0,0 +1,156 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/**
* Defines playwright locators that can be used in tests.
* @typedef {Object} LayoutLocators
* @property {Object<string, import('@playwright/test').Locator>} LayoutLocator
*/
const { test } = require('../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../appActions');
const VISUAL_URL = require('../../constants').VISUAL_URL;
const percySnapshot = require('@percy/playwright');
const snapshotScope = '.l-shell__pane-main .l-pane__contents';
test.describe('Visual - Display Layout', () => {
test('Resize Marquee surrounds selection', async ({ page, theme }) => {
const baseline = await setupBaseline(page);
const { child1LayoutLocator, child1LayoutObjectLocator } = baseline;
await percySnapshot(page, `Resize nested layout selected (theme: '${theme}')`, {
scope: snapshotScope
});
await child1LayoutLocator.click();
await percySnapshot(page, `Resize new nested layout selected (theme: '${theme}')`, {
scope: snapshotScope
});
await child1LayoutObjectLocator.click();
await percySnapshot(page, `Resize Object in nested layout selected (theme: '${theme}')`, {
scope: snapshotScope
});
});
test('Parent layout of selection displays grid', async ({ page, theme }) => {
const baseline = await setupBaseline(page);
const { parentLayoutLocator, child1LayoutObjectLocator } = baseline;
await percySnapshot(page, `Parent nested layout selected (theme: '${theme}')`, {
scope: snapshotScope
});
await parentLayoutLocator.click();
await percySnapshot(page, `Parent outer layout selected (theme: '${theme}')`, {
scope: snapshotScope
});
await child1LayoutObjectLocator.click();
await percySnapshot(page, `Parent Object in nested layout selected (theme: '${theme}')`, {
scope: snapshotScope
});
});
});
/**
* Sets up a complex layout with nested layouts and provides the playwright locators
* @param {import('@playwright/test').Page} page
* @returns {LayoutLocators} locators of baseline complex display to be used in tests
*/
async function setupBaseline(page) {
// Load Open MCT visual test baseline
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
// Open Tree
await page.getByRole('button', { name: 'Browse' }).click();
const treePane = page.getByRole('tree', {
name: 'Main Tree'
});
const objectViewLocator = page.locator('.c-object-view');
const parentLayoutLocator = objectViewLocator.first();
const child1LayoutLocator = parentLayoutLocator.locator(objectViewLocator).first();
const child1LayoutObjectLocator = child1LayoutLocator.locator('.l-layout__frame');
const parentLayout = await createDomainObjectWithDefaults(page, {
type: 'Display Layout',
name: 'Parent Layout'
});
const child1Layout = await createDomainObjectWithDefaults(page, {
type: 'Display Layout',
name: 'Child 1 Layout'
});
const child2Layout = await createDomainObjectWithDefaults(page, {
type: 'Display Layout',
name: 'Child 2 Layout'
});
const swg1 = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'SWG 1'
});
const swg2 = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'SWG 2'
});
const child1LayoutTreeItem = treePane.getByRole('treeitem', {
name: new RegExp(child1Layout.name)
});
const child2LayoutTreeItem = treePane.getByRole('treeitem', {
name: new RegExp(child2Layout.name)
});
const swg1TreeItem = treePane.getByRole('treeitem', {
name: new RegExp(swg1.name)
});
const swg2TreeItem = treePane.getByRole('treeitem', {
name: new RegExp(swg2.name)
});
// Expand folder containing created objects
await page.goto(parentLayout.url);
await page.getByTitle('Show selected item in tree').click();
// Add swg1 to child1Layout
await page.goto(child1Layout.url);
await page.getByRole('button', { name: 'Edit' }).click();
await swg1TreeItem.dragTo(parentLayoutLocator, { targetPosition: { x: 0, y: 0 } });
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Add swg1 to child1Layout
await page.goto(child2Layout.url);
await page.getByRole('button', { name: 'Edit' }).click();
await swg2TreeItem.dragTo(parentLayoutLocator, { targetPosition: { x: 0, y: 0 } });
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Add child1Layout and child2Layout to parentLayout
await page.goto(parentLayout.url);
await page.getByRole('button', { name: 'Edit' }).click();
await child1LayoutTreeItem.dragTo(parentLayoutLocator, { targetPosition: { x: 350, y: 0 } });
await child2LayoutTreeItem.dragTo(parentLayoutLocator, { targetPosition: { x: 0, y: 0 } });
return {
parentLayoutLocator,
child1LayoutLocator,
child1LayoutObjectLocator
};
}

View File

@ -29,7 +29,8 @@ define(['./WorkerInterface'], function (WorkerInterface) {
randomness: 0,
phase: 0,
loadDelay: 0,
infinityValues: false
infinityValues: false,
exceedFloat32: false
};
function GeneratorProvider(openmct, StalenessProvider) {
@ -53,7 +54,8 @@ define(['./WorkerInterface'], function (WorkerInterface) {
'randomness',
'phase',
'loadDelay',
'infinityValues'
'infinityValues',
'exceedFloat32'
];
request = request || {};

View File

@ -85,7 +85,8 @@
data.offset,
data.phase,
data.randomness,
data.infinityValues
data.infinityValues,
data.exceedFloat32
),
wavelengths: wavelengths(),
intensities: intensities(),
@ -96,7 +97,8 @@
data.offset,
data.phase,
data.randomness,
data.infinityValues
data.infinityValues,
data.exceedFloat32
)
}
});
@ -136,6 +138,7 @@
var randomness = request.randomness;
var loadDelay = Math.max(request.loadDelay, 0);
var infinityValues = request.infinityValues;
var exceedFloat32 = request.exceedFloat32;
var step = 1000 / dataRateInHz;
var nextStep = start - (start % step) + step;
@ -146,10 +149,28 @@
data.push({
utc: nextStep,
yesterday: nextStep - 60 * 60 * 24 * 1000,
sin: sin(nextStep, period, amplitude, offset, phase, randomness, infinityValues),
sin: sin(
nextStep,
period,
amplitude,
offset,
phase,
randomness,
infinityValues,
exceedFloat32
),
wavelengths: wavelengths(),
intensities: intensities(),
cos: cos(nextStep, period, amplitude, offset, phase, randomness, infinityValues)
cos: cos(
nextStep,
period,
amplitude,
offset,
phase,
randomness,
infinityValues,
exceedFloat32
)
});
}
@ -176,9 +197,26 @@
});
}
function cos(timestamp, period, amplitude, offset, phase, randomness, infinityValues) {
if (infinityValues && Math.random() > 0.5) {
function cos(
timestamp,
period,
amplitude,
offset,
phase,
randomness,
infinityValues,
exceedFloat32
) {
if (infinityValues && exceedFloat32) {
if (Math.random() > 0.5) {
return Number.POSITIVE_INFINITY;
} else if (Math.random() < 0.01) {
return getRandomFloat32OverflowValue();
}
} else if (infinityValues && Math.random() > 0.5) {
return Number.POSITIVE_INFINITY;
} else if (exceedFloat32 && Math.random() < 0.01) {
return getRandomFloat32OverflowValue();
}
return (
@ -188,9 +226,26 @@
);
}
function sin(timestamp, period, amplitude, offset, phase, randomness, infinityValues) {
if (infinityValues && Math.random() > 0.5) {
function sin(
timestamp,
period,
amplitude,
offset,
phase,
randomness,
infinityValues,
exceedFloat32
) {
if (infinityValues && exceedFloat32) {
if (Math.random() > 0.5) {
return Number.POSITIVE_INFINITY;
} else if (Math.random() < 0.01) {
return getRandomFloat32OverflowValue();
}
} else if (infinityValues && Math.random() > 0.5) {
return Number.POSITIVE_INFINITY;
} else if (exceedFloat32 && Math.random() < 0.01) {
return getRandomFloat32OverflowValue();
}
return (
@ -200,6 +255,13 @@
);
}
// Values exceeding float32 range (Positive: 3.4+38, Negative: -3.4+38)
function getRandomFloat32OverflowValue() {
const sign = Math.random() > 0.5 ? 1 : -1;
return sign * 3.4e39;
}
function wavelengths() {
let values = [];
while (values.length < 5) {

View File

@ -122,6 +122,13 @@ export default function (openmct) {
key: 'infinityValues',
property: ['telemetry', 'infinityValues']
},
{
name: 'Exceed Float32 Limits',
control: 'toggleSwitch',
cssClass: 'l-input',
key: 'exceedFloat32',
property: ['telemetry', 'exceedFloat32']
},
{
name: 'Provide Staleness Updates',
control: 'toggleSwitch',
@ -140,6 +147,7 @@ export default function (openmct) {
randomness: 0,
loadDelay: 0,
infinityValues: false,
exceedFloat32: false,
staleness: false
};
}

View File

@ -1,19 +0,0 @@
<template>
<div class="example">{{ msg }}</div>
</template>
<script>
export default {
data() {
return {
msg: 'Hello world!'
};
}
};
</script>
<style>
.example {
color: red;
}
</style>

View File

@ -1,36 +0,0 @@
import Vue from 'vue';
import HelloWorld from './HelloWorld.vue';
function SimpleVuePlugin() {
return function install(openmct) {
openmct.types.addType('hello-world', {
name: 'Hello World',
description: 'An introduction object',
creatable: true
});
openmct.objectViews.addProvider({
name: 'demo-provider',
key: 'hello-world',
cssClass: 'icon-packet',
canView: function (d) {
return d.type === 'hello-world';
},
view: function (domainObject) {
var vm;
return {
show: function (container) {
vm = new Vue(HelloWorld);
container.appendChild(vm.$mount().$el);
},
destroy: function (container) {
//vm.$destroy();
}
};
}
});
};
}
export default SimpleVuePlugin;

View File

@ -91,157 +91,157 @@
width: 100px;
}
</style>
<script defer>
const THIRTY_SECONDS = 30 * 1000;
const ONE_MINUTE = THIRTY_SECONDS * 2;
const FIVE_MINUTES = ONE_MINUTE * 5;
const FIFTEEN_MINUTES = FIVE_MINUTES * 3;
const THIRTY_MINUTES = FIFTEEN_MINUTES * 2;
const ONE_HOUR = THIRTY_MINUTES * 2;
const TWO_HOURS = ONE_HOUR * 2;
const ONE_DAY = ONE_HOUR * 24;
openmct.install(openmct.plugins.LocalStorage());
openmct.install(openmct.plugins.example.Generator());
openmct.install(openmct.plugins.example.EventGeneratorPlugin());
openmct.install(openmct.plugins.example.ExampleImagery());
openmct.install(openmct.plugins.example.ExampleTags());
openmct.install(openmct.plugins.Espresso());
openmct.install(openmct.plugins.MyItems());
openmct.install(
openmct.plugins.PlanLayout({
creatable: true
})
);
openmct.install(openmct.plugins.Timeline());
openmct.install(openmct.plugins.Hyperlink());
openmct.install(openmct.plugins.UTCTimeSystem());
openmct.install(
openmct.plugins.AutoflowView({
type: 'telemetry.panel'
})
);
openmct.install(
openmct.plugins.DisplayLayout({
showAsView: ['summary-widget', 'example.imagery']
})
);
openmct.install(
openmct.plugins.Conductor({
menuOptions: [
{
name: 'Fixed',
timeSystem: 'utc',
bounds: {
start: Date.now() - THIRTY_MINUTES,
end: Date.now()
},
// commonly used bounds can be stored in history
// bounds (start and end) can accept either a milliseconds number
// or a callback function returning a milliseconds number
// a function is useful for invoking Date.now() at exact moment of preset selection
presets: [
{
label: 'Last Day',
bounds: {
start: () => Date.now() - ONE_DAY,
end: () => Date.now()
}
},
{
label: 'Last 2 hours',
bounds: {
start: () => Date.now() - TWO_HOURS,
end: () => Date.now()
}
},
{
label: 'Last hour',
bounds: {
start: () => Date.now() - ONE_HOUR,
end: () => Date.now()
}
}
],
// maximum recent bounds to retain in conductor history
records: 10
// maximum duration between start and end bounds
// for utc-based time systems this is in milliseconds
// limit: ONE_DAY
},
{
name: 'Realtime',
timeSystem: 'utc',
clock: 'local',
clockOffsets: {
start: -THIRTY_MINUTES,
end: THIRTY_SECONDS
},
presets: [
{
label: '1 Hour',
bounds: {
start: -ONE_HOUR,
end: THIRTY_SECONDS
}
},
{
label: '30 Minutes',
bounds: {
start: -THIRTY_MINUTES,
end: THIRTY_SECONDS
}
},
{
label: '15 Minutes',
bounds: {
start: -FIFTEEN_MINUTES,
end: THIRTY_SECONDS
}
},
{
label: '5 Minutes',
bounds: {
start: -FIVE_MINUTES,
end: THIRTY_SECONDS
}
},
{
label: '1 Minute',
bounds: {
start: -ONE_MINUTE,
end: THIRTY_SECONDS
}
}
]
}
]
})
);
openmct.install(openmct.plugins.SummaryWidget());
openmct.install(openmct.plugins.Notebook());
openmct.install(openmct.plugins.LADTable());
openmct.install(openmct.plugins.Filters(['table', 'telemetry.plot.overlay']));
openmct.install(openmct.plugins.ObjectMigration());
openmct.install(
openmct.plugins.ClearData(
['table', 'telemetry.plot.overlay', 'telemetry.plot.stacked', 'example.imagery'],
{ indicator: true }
)
);
openmct.install(openmct.plugins.Clock({ enableClockIndicator: true }));
openmct.install(openmct.plugins.Timer());
openmct.install(openmct.plugins.Timelist());
openmct.install(openmct.plugins.BarChart());
openmct.install(openmct.plugins.ScatterPlot());
document.addEventListener('DOMContentLoaded', function () {
openmct.start();
});
</script>
</head>
<body>
<div id="app"></div>
</body>
<script>
const THIRTY_SECONDS = 30 * 1000;
const ONE_MINUTE = THIRTY_SECONDS * 2;
const FIVE_MINUTES = ONE_MINUTE * 5;
const FIFTEEN_MINUTES = FIVE_MINUTES * 3;
const THIRTY_MINUTES = FIFTEEN_MINUTES * 2;
const ONE_HOUR = THIRTY_MINUTES * 2;
const TWO_HOURS = ONE_HOUR * 2;
const ONE_DAY = ONE_HOUR * 24;
openmct.install(openmct.plugins.LocalStorage());
openmct.install(openmct.plugins.example.Generator());
openmct.install(openmct.plugins.example.EventGeneratorPlugin());
openmct.install(openmct.plugins.example.ExampleImagery());
openmct.install(openmct.plugins.example.ExampleTags());
openmct.install(openmct.plugins.Espresso());
openmct.install(openmct.plugins.MyItems());
openmct.install(
openmct.plugins.PlanLayout({
creatable: true
})
);
openmct.install(openmct.plugins.Timeline());
openmct.install(openmct.plugins.Hyperlink());
openmct.install(openmct.plugins.UTCTimeSystem());
openmct.install(
openmct.plugins.AutoflowView({
type: 'telemetry.panel'
})
);
openmct.install(
openmct.plugins.DisplayLayout({
showAsView: ['summary-widget', 'example.imagery']
})
);
openmct.install(
openmct.plugins.Conductor({
menuOptions: [
{
name: 'Fixed',
timeSystem: 'utc',
bounds: {
start: Date.now() - THIRTY_MINUTES,
end: Date.now()
},
// commonly used bounds can be stored in history
// bounds (start and end) can accept either a milliseconds number
// or a callback function returning a milliseconds number
// a function is useful for invoking Date.now() at exact moment of preset selection
presets: [
{
label: 'Last Day',
bounds: {
start: () => Date.now() - ONE_DAY,
end: () => Date.now()
}
},
{
label: 'Last 2 hours',
bounds: {
start: () => Date.now() - TWO_HOURS,
end: () => Date.now()
}
},
{
label: 'Last hour',
bounds: {
start: () => Date.now() - ONE_HOUR,
end: () => Date.now()
}
}
],
// maximum recent bounds to retain in conductor history
records: 10
// maximum duration between start and end bounds
// for utc-based time systems this is in milliseconds
// limit: ONE_DAY
},
{
name: 'Realtime',
timeSystem: 'utc',
clock: 'local',
clockOffsets: {
start: -THIRTY_MINUTES,
end: THIRTY_SECONDS
},
presets: [
{
label: '1 Hour',
bounds: {
start: -ONE_HOUR,
end: THIRTY_SECONDS
}
},
{
label: '30 Minutes',
bounds: {
start: -THIRTY_MINUTES,
end: THIRTY_SECONDS
}
},
{
label: '15 Minutes',
bounds: {
start: -FIFTEEN_MINUTES,
end: THIRTY_SECONDS
}
},
{
label: '5 Minutes',
bounds: {
start: -FIVE_MINUTES,
end: THIRTY_SECONDS
}
},
{
label: '1 Minute',
bounds: {
start: -ONE_MINUTE,
end: THIRTY_SECONDS
}
}
]
}
]
})
);
openmct.install(openmct.plugins.SummaryWidget());
openmct.install(openmct.plugins.Notebook());
openmct.install(openmct.plugins.LADTable());
openmct.install(openmct.plugins.Filters(['table', 'telemetry.plot.overlay']));
openmct.install(openmct.plugins.ObjectMigration());
openmct.install(
openmct.plugins.ClearData(
['table', 'telemetry.plot.overlay', 'telemetry.plot.stacked', 'example.imagery'],
{ indicator: true }
)
);
openmct.install(openmct.plugins.Clock({ enableClockIndicator: true }));
openmct.install(openmct.plugins.Timer());
openmct.install(openmct.plugins.Timelist());
openmct.install(openmct.plugins.BarChart());
openmct.install(openmct.plugins.ScatterPlot());
document.addEventListener('DOMContentLoaded', function () {
openmct.start();
});
</script>
</html>

View File

@ -1,45 +1,45 @@
{
"name": "openmct",
"version": "3.1.0-next",
"version": "3.2.0-next",
"description": "The Open MCT core platform",
"devDependencies": {
"@babel/eslint-parser": "7.22.5",
"@braintree/sanitize-url": "6.0.2",
"@braintree/sanitize-url": "6.0.4",
"@deploysentinel/playwright": "0.3.4",
"@percy/cli": "1.26.0",
"@percy/cli": "1.27.4",
"@percy/playwright": "1.0.4",
"@playwright/test": "1.36.2",
"@playwright/test": "1.39.0",
"@types/eventemitter3": "1.2.0",
"@types/jasmine": "4.3.4",
"@types/jasmine": "5.1.2",
"@types/lodash": "4.14.192",
"@vue/compat": "3.3.4",
"@vue/compiler-sfc": "3.3.4",
"@vue/compiler-sfc": "3.3.8",
"babel-loader": "9.1.0",
"babel-plugin-istanbul": "6.1.1",
"codecov": "3.8.3",
"comma-separated-values": "3.6.4",
"copy-webpack-plugin": "11.0.0",
"cspell": "7.1.1",
"cspell": "7.3.8",
"css-loader": "6.8.1",
"d3-axis": "3.0.0",
"d3-scale": "3.3.0",
"d3-selection": "3.0.0",
"eslint": "8.48.0",
"eslint": "8.54.0",
"eslint-config-prettier": "9.0.0",
"eslint-plugin-compat": "4.1.4",
"eslint-plugin-compat": "4.2.0",
"eslint-plugin-no-unsanitized": "4.0.2",
"eslint-plugin-playwright": "0.12.0",
"eslint-plugin-prettier": "4.2.1",
"eslint-plugin-simple-import-sort": "10.0.0",
"eslint-plugin-unicorn": "44.0.2",
"eslint-plugin-vue": "9.15.0",
"eslint-plugin-you-dont-need-lodash-underscore": "6.12.0",
"eslint-plugin-unicorn": "49.0.0",
"eslint-plugin-vue": "9.18.1",
"eslint-plugin-you-dont-need-lodash-underscore": "6.13.0",
"eventemitter3": "1.2.0",
"file-saver": "2.0.5",
"flatbush": "4.2.0",
"git-rev-sync": "3.0.2",
"html2canvas": "1.4.1",
"imports-loader": "4.0.1",
"jasmine-core": "5.0.0",
"jasmine-core": "5.1.1",
"karma": "6.4.2",
"karma-chrome-launcher": "3.2.0",
"karma-cli": "2.0.0",
@ -52,41 +52,43 @@
"karma-webpack": "5.0.0",
"location-bar": "3.0.1",
"lodash": "4.17.21",
"marked": "9.1.5",
"mini-css-extract-plugin": "2.7.6",
"moment": "2.29.4",
"moment-duration-format": "2.3.2",
"moment-timezone": "0.5.41",
"npm-run-all2": "6.0.6",
"npm-run-all2": "6.1.1",
"nyc": "15.1.0",
"painterro": "1.2.78",
"painterro": "1.2.87",
"plotly.js-basic-dist": "2.20.0",
"plotly.js-gl2d-dist": "2.20.0",
"prettier": "2.8.7",
"printj": "1.3.1",
"resolve-url-loader": "5.0.0",
"sanitize-html": "2.11.0",
"sass": "1.63.4",
"sass": "1.68.0",
"sass-loader": "13.3.2",
"sinon": "15.1.0",
"sinon": "17.0.0",
"style-loader": "3.3.3",
"tiny-emitter": "2.1.0",
"typescript": "5.2.2",
"uuid": "9.0.0",
"vue": "3.3.4",
"vue-eslint-parser": "9.3.1",
"uuid": "9.0.1",
"vue": "3.3.8",
"vue-eslint-parser": "9.3.2",
"vue-loader": "16.8.3",
"webpack": "5.88.0",
"webpack": "5.89.0",
"webpack-cli": "5.1.1",
"webpack-dev-server": "4.15.1",
"webpack-merge": "5.9.0"
"webpack-merge": "5.10.0"
},
"scripts": {
"clean": "rm -rf ./dist ./node_modules ./package-lock.json ./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",
"lint:js": "eslint example src e2e --ext .js openmct.js --max-warnings=0",
"lint:vue": "eslint example src --ext .vue",
"lint:spelling": "cspell \"**/*.{js,md,vue}\" --show-context --gitignore",
"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",

View File

@ -33,7 +33,7 @@ define([
'./ui/registries/ToolbarRegistry',
'./ui/router/ApplicationRouter',
'./ui/router/Browse',
'./ui/layout/Layout.vue',
'./ui/layout/AppLayout.vue',
'./ui/preview/plugin',
'./api/Branding',
'./plugins/licenses/plugin',

View File

@ -3,9 +3,9 @@ import mount from 'utils/mount';
import AutoCompleteField from './components/controls/AutoCompleteField.vue';
import CheckBoxField from './components/controls/CheckBoxField.vue';
import ClockDisplayFormatField from './components/controls/ClockDisplayFormatField.vue';
import Datetime from './components/controls/Datetime.vue';
import Datetime from './components/controls/DatetimeField.vue';
import FileInput from './components/controls/FileInput.vue';
import Locator from './components/controls/Locator.vue';
import Locator from './components/controls/LocatorField.vue';
import NumberField from './components/controls/NumberField.vue';
import SelectField from './components/controls/SelectField.vue';
import TextAreaField from './components/controls/TextAreaField.vue';
@ -87,7 +87,7 @@ export default class FormControl {
onChange
};
},
template: `<FormControlComponent :model="model" @onChange="onChange"></FormControlComponent>`
template: `<FormControlComponent :model="model" @on-change="onChange"></FormControlComponent>`
},
{
element,

View File

@ -171,7 +171,7 @@ export default class FormsAPI {
};
},
template:
'<FormProperties :model="formStructure" @onChange="onChange" @onCancel="onCancel" @onSave="onSave"></FormProperties>'
'<FormProperties :model="formStructure" @on-change="onChange" @on-cancel="onCancel" @on-save="onSave"></FormProperties>'
},
{
element,

View File

@ -44,7 +44,7 @@
:css-class="row.cssClass"
:first="index < 1"
:row="row"
@onChange="onChange"
@on-change="onChange"
/>
</div>
</form>
@ -94,6 +94,7 @@ export default {
}
}
},
emits: ['on-change', 'on-save', 'on-cancel'],
data() {
return {
invalidProperties: {},
@ -144,13 +145,13 @@ export default {
onChange(data) {
this.invalidProperties[data.model.key] = data.invalid;
this.$emit('onChange', data);
this.$emit('on-change', data);
},
onCancel() {
this.$emit('onCancel');
this.$emit('on-cancel');
},
onSave() {
this.$emit('onSave');
this.$emit('on-save');
}
}
};

View File

@ -21,7 +21,7 @@
-->
<template>
<div class="form-row c-form__row" :class="[{ first: first }, cssClass]" @onChange="onChange">
<div class="form-row c-form__row" :class="[{ first: first }, cssClass]" @on-change="onChange">
<label class="c-form-row__label" :title="row.description" :for="`form-${row.key}`">
{{ row.name }}
</label>
@ -51,6 +51,7 @@ export default {
required: true
}
},
emits: ['on-change'],
data() {
return {
formControl: this.openmct.forms.getFormControl(this.row.control),
@ -101,7 +102,7 @@ export default {
this.valid = this.validateRow(data);
data.invalid = !this.valid;
this.$emit('onChange', data);
this.$emit('on-change', data);
},
validateRow(data) {
let valid = true;

View File

@ -88,6 +88,7 @@ export default {
}
}
},
emits: ['on-change'],
data() {
return {
hideOptions: true,
@ -138,7 +139,7 @@ export default {
value: newValue
};
this.$emit('onChange', data);
this.$emit('on-change', data);
}
},
hideOptions(newValue) {

View File

@ -22,7 +22,7 @@
<template>
<div class="c-form-control--clock-display-format-fields">
<SelectField v-for="item in items" :key="item.key" :model="item" @onChange="onChange" />
<SelectField v-for="item in items" :key="item.key" :model="item" @on-change="onChange" />
</div>
</template>
@ -39,6 +39,7 @@ export default {
required: true
}
},
emits: ['on-change'],
data() {
return {
items: []
@ -55,7 +56,7 @@ export default {
},
methods: {
onChange(data) {
this.$emit('onChange', data);
this.$emit('on-change', data);
}
}
};

View File

@ -28,7 +28,7 @@
:first="index < 1"
:value="JSON.stringify(model.value[index])"
:item="item"
@onChange="onChange"
@on-change="onChange"
/>
</span>
</template>
@ -46,12 +46,13 @@ export default {
required: true
}
},
emits: ['on-change'],
mounted() {
this.model.items.forEach((item, index) => (item.key = `${this.model.key}.${index}`));
},
methods: {
onChange(data) {
this.$emit('onChange', data);
this.$emit('on-change', data);
}
}
};

View File

@ -22,7 +22,7 @@
<template>
<div :class="compositeCssClass">
<FormRow :css-class="item.cssClass" :first="first" :row="row" @onChange="onChange" />
<FormRow :css-class="item.cssClass" :first="first" :row="row" @on-change="onChange" />
<span class="composite-control-label">
{{ item.name }}
</span>
@ -50,6 +50,7 @@ export default {
}
}
},
emits: ['on-change'],
computed: {
compositeCssClass() {
return `l-composite-control l-${this.item.control}`;
@ -63,7 +64,7 @@ export default {
},
methods: {
onChange(data) {
this.$emit('onChange', data);
this.$emit('on-change', data);
}
}
};

View File

@ -85,6 +85,7 @@ export default {
required: true
}
},
emits: ['on-change'],
data() {
return {
format: DATE_FORMAT,
@ -139,7 +140,7 @@ export default {
value: new Date(timestamp).toISOString()
};
this.$emit('onChange', data);
this.$emit('on-change', data);
},
resetValues() {
this.setDatetime();

View File

@ -52,6 +52,7 @@ export default {
required: true
}
},
emits: ['on-change'],
data() {
return {
fileInfo: undefined
@ -101,7 +102,7 @@ export default {
model: self.model,
value: fileInfo
};
self.$emit('onChange', data);
self.$emit('on-change', data);
};
fileReader.onerror = function (error) {
@ -123,7 +124,7 @@ export default {
value: fileInfo
};
this.$emit('onChange', data);
this.$emit('on-change', data);
},
selectFile() {
this.$refs.fileInput.click();
@ -135,7 +136,7 @@ export default {
model: this.model,
value: undefined
};
this.$emit('onChange', data);
this.$emit('on-change', data);
}
}
};

View File

@ -42,6 +42,7 @@ export default {
required: true
}
},
emits: ['on-change'],
methods: {
handleItemSelection(item) {
const data = {
@ -49,7 +50,7 @@ export default {
value: item.objectPath
};
this.$emit('onChange', data);
this.$emit('on-change', data);
}
}
};

View File

@ -46,6 +46,7 @@ export default {
required: true
}
},
emits: ['on-change'],
data() {
return {
field: this.model.value
@ -61,7 +62,7 @@ export default {
value: this.field
};
this.$emit('onChange', data);
this.$emit('on-change', data);
}
}
};

View File

@ -44,6 +44,7 @@ export default {
required: true
}
},
emits: ['on-change'],
data() {
return {
selected: this.model.value
@ -56,7 +57,7 @@ export default {
value: this.selected
};
this.$emit('onChange', data);
this.$emit('on-change', data);
}
}
};

View File

@ -45,6 +45,7 @@ export default {
required: true
}
},
emits: ['on-change'],
data() {
return {
field: this.model.value
@ -60,7 +61,7 @@ export default {
value: this.field
};
this.$emit('onChange', data);
this.$emit('on-change', data);
}
}
};

View File

@ -45,6 +45,7 @@ export default {
required: true
}
},
emits: ['on-change'],
data() {
return {
field: this.model.value
@ -60,7 +61,7 @@ export default {
value: this.field
};
this.$emit('onChange', data);
this.$emit('on-change', data);
}
}
};

View File

@ -1,4 +1,5 @@
export default {
emits: ['on-change'],
data() {
return {
isChecked: false
@ -13,7 +14,7 @@ export default {
value: this.isChecked
};
this.$emit('onChange', data);
this.$emit('on-change', data);
}
}
};

View File

@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import Vue from 'vue';
import { nextTick } from 'vue';
import { createMouseEvent, createOpenMct, resetApplicationState } from '../../utils/testing';
import Menu from './menu';
@ -186,7 +186,7 @@ describe('The Menu API', () => {
superMenuItem.dispatchEvent(mouseOverEvent);
const itemDescription = document.querySelector('.l-item-description__description');
Vue.nextTick(() => {
nextTick(() => {
expect(menuElement).not.toBeNull();
expect(itemDescription.innerText).toEqual(actionsArray[0].description);

View File

@ -23,7 +23,7 @@ import EventEmitter from 'EventEmitter';
import mount from 'utils/mount';
import { h } from 'vue';
import MenuComponent from './components/Menu.vue';
import MenuComponent from './components/MenuComponent.vue';
import SuperMenuComponent from './components/SuperMenu.vue';
export const MENU_PLACEMENT = {
@ -71,11 +71,6 @@ class Menu extends EventEmitter {
},
provide: {
options: this.options
},
// TODO: Remove this exception upon full migration to Vue 3
// https://v3-migration.vuejs.org/breaking-changes/render-function-api.html#render-function-argument
compatConfig: {
RENDER_FUNCTION: false
}
});
@ -98,11 +93,6 @@ class Menu extends EventEmitter {
},
provide: {
options: this.options
},
// TODO: Remove this exception upon full migration to Vue 3
// https://v3-migration.vuejs.org/breaking-changes/render-function-api.html#render-function-argument
compatConfig: {
RENDER_FUNCTION: false
}
});

View File

@ -221,7 +221,7 @@ export default class ObjectAPI {
const provider = this.getProvider(identifier);
if (!provider) {
throw new Error(`No Provider Matched for keyString "${this.makeKeyString(identifier)}}"`);
throw new Error(`No Provider Matched for keyString "${this.makeKeyString(identifier)}"`);
}
if (!provider.get) {

View File

@ -57,6 +57,7 @@
<script>
export default {
inject: ['dismiss', 'element', 'buttons', 'dismissable'],
emits: ['destroy'],
data: function () {
return {
focusIndex: -1

View File

@ -34,7 +34,7 @@ export default class LADTableView {
this._destroy = null;
}
show(element) {
show(element, isEditing, { renderWhenVisible }) {
let ladTableConfiguration = new LADTableConfiguration(this.domainObject, this.openmct);
const { vNode, destroy } = mount(
@ -46,7 +46,8 @@ export default class LADTableView {
provide: {
openmct: this.openmct,
currentView: this,
ladTableConfiguration
ladTableConfiguration,
renderWhenVisible
},
data: () => {
return {

View File

@ -22,6 +22,7 @@
<template>
<tr
ref="tableRow"
class="js-lad-table__body__row c-table__selectable-row"
@click="clickedRow"
@contextmenu.prevent="showContextMenu"
@ -40,6 +41,9 @@
{{ unit }}
</td>
<td v-if="showType" class="js-type-data">{{ typeLabel }}</td>
<td v-for="limit in formattedLimitValues" :key="limit.key" class="js-limit-data">
{{ limit.value }}
</td>
</tr>
</template>
@ -54,7 +58,7 @@ import tooltipHelpers from '../../../api/tooltips/tooltipMixins';
export default {
mixins: [tooltipHelpers],
inject: ['openmct', 'currentView'],
inject: ['openmct', 'currentView', 'renderWhenVisible'],
props: {
domainObject: {
type: Object,
@ -77,13 +81,28 @@ export default {
configuration: {
type: Object,
required: true
},
limitDefinition: {
type: Object,
default() {
return {};
}
},
limitColumnNames: {
// for ordering
type: Array,
default() {
return [];
}
}
},
emits: ['row-context-click'],
data() {
return {
datum: undefined,
timestamp: undefined,
timestampKey: undefined,
valueKey: null,
composition: [],
unit: ''
};
@ -96,6 +115,26 @@ export default {
return this.formats[this.valueKey].format(this.datum);
},
formattedLimitValues() {
if (!this.valueKey) {
return [];
}
return this.limitColumnNames.map((column) => {
if (this.limitDefinition?.[column.key]) {
const highValue = this.limitDefinition[column.key].high[this.valueKey];
const lowValue = this.limitDefinition[column.key].low[this.valueKey];
return {
key: column.key,
value: `${lowValue}${highValue}`
};
} else {
return {
key: column.key,
value: BLANK_VALUE
};
}
});
},
typeLabel() {
if (this.isAggregate) {
return 'Aggregate';
@ -202,8 +241,7 @@ export default {
methods: {
updateView() {
if (!this.updatingView) {
this.updatingView = true;
requestAnimationFrame(() => {
this.updatingView = this.renderWhenVisible(() => {
this.timestamp = this.getParsedTimestamp(this.latestDatum);
this.datum = this.latestDatum;
this.updatingView = false;
@ -232,7 +270,7 @@ export default {
this.timestampKey = timeSystem.key;
},
updateViewContext() {
this.$emit('rowContextClick', {
this.$emit('row-context-click', {
viewHistoricalData: true,
viewDatumAction: true,
getDatum: () => {

View File

@ -30,6 +30,9 @@
<th>Value</th>
<th v-if="hasUnits">Units</th>
<th v-if="showType">Type</th>
<th v-for="limitColumn in limitColumnNames" :key="limitColumn.key">
{{ limitColumn.label }}
</th>
</tr>
</thead>
<tbody>
@ -37,11 +40,13 @@
v-for="ladRow in items"
:key="ladRow.key"
:domain-object="ladRow.domainObject"
:limit-definition="ladRow.limitDefinition"
:limit-column-names="limitColumnNames"
:path-to-table="objectPath"
:has-units="hasUnits"
:is-stale="staleObjects.includes(ladRow.key)"
:configuration="configuration"
@rowContextClick="updateViewContext"
@row-context-click="updateViewContext"
/>
</tbody>
</table>
@ -49,9 +54,9 @@
</template>
<script>
import Vue, { toRaw } from 'vue';
import { nextTick, toRaw } from 'vue';
import StalenessUtils from '@/utils/staleness';
import stalenessMixin from '@/ui/mixins/staleness-mixin';
import LadRow from './LadRow.vue';
@ -59,6 +64,7 @@ export default {
components: {
LadRow
},
mixins: [stalenessMixin],
inject: ['openmct', 'currentView', 'ladTableConfiguration'],
props: {
domainObject: {
@ -74,7 +80,6 @@ export default {
return {
items: [],
viewContext: {},
staleObjects: [],
configuration: this.ladTableConfiguration.getConfiguration()
};
},
@ -89,6 +94,23 @@ export default {
return itemsWithUnits.length !== 0 && !this.configuration?.hiddenColumns?.units;
},
limitColumnNames() {
const limitDefinitions = [];
this.items.forEach((item) => {
if (item.limitDefinition) {
const limits = Object.keys(item.limitDefinition);
limits.forEach((limit) => {
const limitAlreadyAdded = limitDefinitions.some((limitDef) => limitDef.key === limit);
const limitHidden = this.configuration?.hiddenColumns?.[limit];
if (!limitAlreadyAdded && !limitHidden) {
limitDefinitions.push({ label: `Limit ${limit}`, key: limit });
}
});
}
});
return limitDefinitions;
},
showTimestamp() {
return !this.configuration?.hiddenColumns?.timestamp;
},
@ -96,11 +118,7 @@ export default {
return !this.configuration?.hiddenColumns?.type;
},
staleClass() {
if (this.staleObjects.length !== 0) {
return 'is-stale';
}
return '';
return this.isStale ? 'is-stale' : '';
},
applyLayoutClass() {
if (this.configuration.isFixedLayout) {
@ -133,12 +151,16 @@ export default {
this.composition.on('remove', this.removeItem);
this.composition.on('reorder', this.reorder);
this.composition.load();
this.stalenessSubscription = {};
await Vue.nextTick();
await nextTick();
this.viewActionsCollection = this.openmct.actions.getActionsCollection(
this.objectPath,
this.currentView
);
this.setupClockChangedEvent((domainObject) => {
this.triggerUnsubscribeFromStaleness(domainObject);
this.subscribeToStaleness(domainObject);
});
this.initializeViewActions();
},
unmounted() {
@ -147,48 +169,25 @@ export default {
this.composition.off('add', this.addItem);
this.composition.off('remove', this.removeItem);
this.composition.off('reorder', this.reorder);
Object.values(this.stalenessSubscription).forEach((stalenessSubscription) => {
stalenessSubscription.unsubscribe();
stalenessSubscription.stalenessUtils.destroy();
});
},
methods: {
addItem(domainObject) {
async addItem(domainObject) {
let item = {};
item.domainObject = domainObject;
item.key = this.openmct.objects.makeKeyString(domainObject.identifier);
item.limitDefinition = await this.openmct.telemetry.limitDefinition(domainObject).limits();
this.items.push(item);
this.stalenessSubscription[item.key] = {};
this.stalenessSubscription[item.key].stalenessUtils = new StalenessUtils(
this.openmct,
domainObject
);
this.openmct.telemetry.isStale(domainObject).then((stalenessResponse) => {
if (stalenessResponse !== undefined) {
this.handleStaleness(item.key, stalenessResponse);
}
});
const stalenessSubscription = this.openmct.telemetry.subscribeToStaleness(
domainObject,
(stalenessResponse) => {
this.handleStaleness(item.key, stalenessResponse);
}
);
this.stalenessSubscription[item.key].unsubscribe = stalenessSubscription;
this.subscribeToStaleness(domainObject);
},
removeItem(identifier) {
const SKIP_CHECK = true;
const keystring = this.openmct.objects.makeKeyString(identifier);
const index = this.items.findIndex((item) => keystring === item.key);
const index = this.items.findIndex((item) => keystring === item.key);
this.items.splice(index, 1);
this.stalenessSubscription[keystring].unsubscribe();
this.handleStaleness(keystring, { isStale: false }, SKIP_CHECK);
const domainObject = this.openmct.objects.get(keystring);
this.triggerUnsubscribeFromStaleness(domainObject);
},
reorder(reorderPlan) {
const oldItems = this.items.slice();
@ -204,23 +203,6 @@ export default {
handleConfigurationChange(configuration) {
this.configuration = configuration;
},
handleStaleness(id, stalenessResponse, skipCheck = false) {
if (
skipCheck ||
this.stalenessSubscription[id].stalenessUtils.shouldUpdateStaleness(stalenessResponse)
) {
const index = this.staleObjects.indexOf(id);
if (stalenessResponse.isStale) {
if (index === -1) {
this.staleObjects.push(id);
}
} else {
if (index !== -1) {
this.staleObjects.splice(index, 1);
}
}
}
},
updateViewContext(rowContext) {
this.viewContext.row = rowContext;
},

View File

@ -58,23 +58,37 @@ export default {
const ladTableConfiguration = new LADTableConfiguration(domainObject, this.openmct);
return {
headers: {
timestamp: 'Timestamp',
units: 'Units',
type: 'Type'
},
ladTableConfiguration,
isEditing: this.openmct.editor.isEditing(),
configuration: ladTableConfiguration.getConfiguration(),
items: [],
ladTableObjects: [],
ladTelemetryObjects: {}
ladTableObjects: []
};
},
computed: {
headers() {
const fullHeaders = {
timestamp: 'Timestamp',
type: 'Type'
};
// check hasUnits and limitColumnName and add then to fullHeaders
this.items.forEach((item) => {
if (item.hasUnits) {
fullHeaders.units = 'Units';
}
if (item.limitDefinition) {
const limits = Object.keys(item.limitDefinition);
limits.forEach((limit) => {
fullHeaders[limit] = limit;
});
}
});
return fullHeaders;
}
},
mounted() {
this.openmct.editor.on('isEditing', this.toggleEdit);
this.composition = this.openmct.composition.get(this.ladTableConfiguration.domainObject);
this.shouldShowUnitsCheckbox();
if (this.ladTableConfiguration.domainObject.type === 'LadTable') {
this.composition.on('add', this.addItem);
@ -104,22 +118,25 @@ export default {
}
},
methods: {
addItem(domainObject) {
async addItem(domainObject) {
const item = {};
item.domainObject = domainObject;
item.key = this.openmct.objects.makeKeyString(domainObject.identifier);
item.limitDefinition = await this.openmct.telemetry.limitDefinition(domainObject).limits();
const metadata = this.openmct.telemetry.getMetadata(domainObject);
const valueMetadatas = metadata ? metadata.valueMetadatas : [];
const metadataWithUnits = valueMetadatas.filter((metadatum) => metadatum.unit);
item.hasUnits = metadataWithUnits.length > 0;
this.items.push(item);
this.shouldShowUnitsCheckbox();
},
removeItem(identifier) {
const keystring = this.openmct.objects.makeKeyString(identifier);
const index = this.items.findIndex((item) => keystring === item.key);
this.items.splice(index, 1);
this.shouldShowUnitsCheckbox();
},
addLadTable(domainObject) {
let ladTable = {};
@ -130,20 +147,12 @@ export default {
this.ladTableObjects.push(ladTable);
const composition = this.openmct.composition.get(ladTable.domainObject);
const addCallback = this.addTelemetryObject(ladTable);
const removeCallback = this.removeTelemetryObject(ladTable);
composition.on('add', addCallback);
composition.on('remove', removeCallback);
composition.load();
this.compositions.push({
composition,
addCallback,
removeCallback
composition
});
this.shouldShowUnitsCheckbox();
},
removeLadTable(identifier) {
const index = this.ladTableObjects.findIndex(
@ -153,36 +162,6 @@ export default {
delete this.ladTelemetryObjects[ladTable.key];
this.ladTableObjects.splice(index, 1);
this.shouldShowUnitsCheckbox();
},
addTelemetryObject(ladTable) {
return (domainObject) => {
const telemetryObject = {};
telemetryObject.key = this.openmct.objects.makeKeyString(domainObject.identifier);
telemetryObject.domainObject = domainObject;
const telemetryObjects = this.ladTelemetryObjects[ladTable.key];
telemetryObjects.push(telemetryObject);
this.ladTelemetryObjects[ladTable.key] = telemetryObjects;
this.shouldShowUnitsCheckbox();
};
},
removeTelemetryObject(ladTable) {
return (identifier) => {
const keystring = this.openmct.objects.makeKeyString(identifier);
const telemetryObjects = this.ladTelemetryObjects[ladTable.key];
const index = telemetryObjects.findIndex(
(telemetryObject) => keystring === telemetryObject.key
);
telemetryObjects.splice(index, 1);
this.ladTelemetryObjects[ladTable.key] = telemetryObjects;
this.shouldShowUnitsCheckbox();
};
},
combineKeys(ladKey, telemetryObjectKey) {
return `${ladKey}-${telemetryObjectKey}`;
@ -195,44 +174,6 @@ export default {
},
toggleEdit(isEditing) {
this.isEditing = isEditing;
},
shouldShowUnitsCheckbox() {
let showUnitsCheckbox = false;
if (this.ladTableConfiguration?.domainObject) {
if (this.ladTableConfiguration.domainObject.type === 'LadTable') {
const itemsWithUnits = this.items.filter((item) => {
return this.metadataHasUnits(item.domainObject);
});
showUnitsCheckbox = itemsWithUnits.length !== 0;
} else {
const ladTables = Object.values(this.ladTelemetryObjects);
for (const ladTable of ladTables) {
for (const telemetryObject of ladTable) {
if (this.metadataHasUnits(telemetryObject.domainObject)) {
showUnitsCheckbox = true;
}
}
}
}
}
if (showUnitsCheckbox && this.headers.units === undefined) {
this.headers.units = 'Units';
}
if (!showUnitsCheckbox && this.headers?.units) {
delete this.headers.units;
}
},
metadataHasUnits(domainObject) {
const metadata = this.openmct.telemetry.getMetadata(domainObject);
const valueMetadatas = metadata ? metadata.valueMetadatas : [];
const metadataWithUnits = valueMetadatas.filter((metadatum) => metadatum.unit);
return metadataWithUnits.length > 0;
}
}
};

View File

@ -45,9 +45,9 @@
:domain-object="ladRow.domainObject"
:path-to-table="ladTable.objectPath"
:has-units="hasUnits"
:is-stale="staleObjects.includes(combineKeys(ladTable.key, ladRow.key))"
:is-stale="staleObjects.includes(ladRow.key)"
:configuration="configuration"
@rowContextClick="updateViewContext"
@row-context-click="updateViewContext"
/>
</template>
</tbody>
@ -56,7 +56,9 @@
</template>
<script>
import StalenessUtils from '@/utils/staleness';
import { toRaw } from 'vue';
import stalenessMixin from '@/ui/mixins/staleness-mixin';
import LadRow from './LadRow.vue';
@ -64,6 +66,7 @@ export default {
components: {
LadRow
},
mixins: [stalenessMixin],
inject: ['openmct', 'objectPath', 'currentView', 'ladTableConfiguration'],
props: {
domainObject: {
@ -76,8 +79,8 @@ export default {
ladTableObjects: [],
ladTelemetryObjects: {},
viewContext: {},
staleObjects: [],
configuration: this.ladTableConfiguration.getConfiguration()
configuration: this.ladTableConfiguration.getConfiguration(),
subscribedObjects: {}
};
},
computed: {
@ -108,11 +111,7 @@ export default {
return !this.configuration?.hiddenColumns?.type;
},
staleClass() {
if (this.staleObjects.length !== 0) {
return 'is-stale';
}
return '';
return this.isStale ? 'is-stale' : '';
}
},
created() {
@ -125,8 +124,10 @@ export default {
this.composition.on('remove', this.removeLadTable);
this.composition.on('reorder', this.reorderLadTables);
this.composition.load();
this.stalenessSubscription = {};
this.setupClockChangedEvent((domainObject) => {
this.triggerUnsubscribeFromStaleness(domainObject);
this.subscribeToStaleness(domainObject);
});
},
unmounted() {
this.ladTableConfiguration.off('change', this.handleConfigurationChange);
@ -137,11 +138,6 @@ export default {
c.composition.off('add', c.addCallback);
c.composition.off('remove', c.removeCallback);
});
Object.values(this.stalenessSubscription).forEach((stalenessSubscription) => {
stalenessSubscription.unsubscribe();
stalenessSubscription.stalenessUtils.destroy();
});
},
methods: {
addLadTable(domainObject) {
@ -176,9 +172,18 @@ export default {
);
let ladTable = this.ladTableObjects[index];
this.ladTelemetryObjects[ladTable.key].forEach((telemetryObject) => {
let combinedKey = this.combineKeys(ladTable.key, telemetryObject.key);
this.unwatchStaleness(combinedKey);
ladTable?.domainObject?.composition.forEach((telemetryObject) => {
const telemetryKey = this.openmct.objects.makeKeyString(telemetryObject);
if (!this.subscribedObjects?.[telemetryKey]) {
return;
}
let subscribedObject = toRaw(this.subscribedObjects[telemetryKey]);
if (subscribedObject?.count > 1) {
subscribedObject.count -= 1;
} else if (subscribedObject?.count === 1) {
this.triggerUnsubscribeFromStaleness(subscribedObject.domainObject);
delete this.subscribedObjects[telemetryKey];
}
});
delete this.ladTelemetryObjects[ladTable.key];
@ -187,11 +192,7 @@ export default {
reorderLadTables(reorderPlan) {
let oldComposition = this.ladTableObjects.slice();
reorderPlan.forEach((reorderEvent) => {
this.$set(
this.ladTableObjects,
reorderEvent.newIndex,
oldComposition[reorderEvent.oldIndex]
);
this.ladTableObjects[reorderEvent.newIndex] = oldComposition[reorderEvent.oldIndex];
});
},
addTelemetryObject(ladTable) {
@ -199,77 +200,35 @@ export default {
let telemetryObject = {};
telemetryObject.key = this.openmct.objects.makeKeyString(domainObject.identifier);
telemetryObject.domainObject = domainObject;
const combinedKey = this.combineKeys(ladTable.key, telemetryObject.key);
const telemetryObjects = this.ladTelemetryObjects[ladTable.key];
telemetryObjects.push(telemetryObject);
this.ladTelemetryObjects[ladTable.key] = telemetryObjects;
this.stalenessSubscription[combinedKey] = {};
this.stalenessSubscription[combinedKey].stalenessUtils = new StalenessUtils(
this.openmct,
domainObject
);
this.openmct.telemetry.isStale(domainObject).then((stalenessResponse) => {
if (stalenessResponse !== undefined) {
this.handleStaleness(combinedKey, stalenessResponse);
}
});
const stalenessSubscription = this.openmct.telemetry.subscribeToStaleness(
domainObject,
(stalenessResponse) => {
this.handleStaleness(combinedKey, stalenessResponse);
}
);
this.stalenessSubscription[combinedKey].unsubscribe = stalenessSubscription;
if (!this.subscribedObjects[telemetryObject?.key]) {
this.subscribeToStaleness(domainObject);
this.subscribedObjects[telemetryObject?.key] = { count: 1, domainObject };
} else if (this.subscribedObjects?.[telemetryObject?.key]?.count) {
this.subscribedObjects[telemetryObject?.key].count += 1;
}
};
},
removeTelemetryObject(ladTable) {
return (identifier) => {
const keystring = this.openmct.objects.makeKeyString(identifier);
const telemetryObjects = this.ladTelemetryObjects[ladTable.key];
const combinedKey = this.combineKeys(ladTable.key, keystring);
let index = telemetryObjects.findIndex(
(telemetryObject) => keystring === telemetryObject.key
);
this.unwatchStaleness(combinedKey);
telemetryObjects.splice(index, 1);
this.ladTelemetryObjects[ladTable.key] = telemetryObjects;
};
},
unwatchStaleness(combinedKey) {
const SKIP_CHECK = true;
this.stalenessSubscription[combinedKey].unsubscribe();
this.stalenessSubscription[combinedKey].stalenessUtils.destroy();
this.handleStaleness(combinedKey, { isStale: false }, SKIP_CHECK);
delete this.stalenessSubscription[combinedKey];
},
handleConfigurationChange(configuration) {
this.configuration = configuration;
},
handleStaleness(combinedKey, stalenessResponse, skipCheck = false) {
if (
skipCheck ||
this.stalenessSubscription[combinedKey].stalenessUtils.shouldUpdateStaleness(
stalenessResponse
)
) {
const index = this.staleObjects.indexOf(combinedKey);
const foundStaleObject = index > -1;
if (stalenessResponse.isStale && !foundStaleObject) {
this.staleObjects.push(combinedKey);
} else if (!stalenessResponse.isStale && foundStaleObject) {
this.staleObjects.splice(index, 1);
}
}
},
updateViewContext(rowContext) {
this.viewContext.row = rowContext;
},

View File

@ -24,10 +24,11 @@ import {
getLatestTelemetry,
getMockObjects,
getMockTelemetry,
renderWhenVisible,
resetApplicationState,
spyOnBuiltins
} from 'utils/testing';
import Vue from 'vue';
import { nextTick } from 'vue';
import LadPlugin from './plugin.js';
@ -225,7 +226,7 @@ describe('The LAD Table', () => {
(viewProvider) => viewProvider.key === ladTableKey
);
ladTableView = ladTableViewProvider.view(mockObj.ladTable, [mockObj.ladTable]);
ladTableView.show(child, true);
ladTableView.show(child, true, { renderWhenVisible });
await Promise.all([
telemetryRequestPromise,
@ -233,7 +234,7 @@ describe('The LAD Table', () => {
anotherTelemetryObjectPromise,
aggregateTelemetryObjectResolve
]);
await Vue.nextTick();
await nextTick();
});
it('should show one row per object in the composition', () => {
@ -244,7 +245,7 @@ describe('The LAD Table', () => {
it('should show the most recent datum from the telemetry producing object', async () => {
const latestDatum = getLatestTelemetry(mockTelemetry, { timeFormat });
const expectedDate = utcTimeFormat(latestDatum[timeFormat]);
await Vue.nextTick();
await nextTick();
const latestDate = parent.querySelector(TABLE_BODY_FIRST_ROW_SECOND_DATA).innerText;
expect(latestDate).toBe(expectedDate);
const dataType = parent
@ -254,7 +255,7 @@ describe('The LAD Table', () => {
});
it('should show aggregate telemetry type with blank data', async () => {
await Vue.nextTick();
await nextTick();
const latestData = parent
.querySelectorAll(TABLE_BODY_ROWS)[1]
.querySelectorAll('td')[2].innerText;
@ -282,7 +283,7 @@ describe('The LAD Table', () => {
const mostRecentTelemetry = getLatestTelemetry(mockTelemetry, { timeFormat });
const rangeValue = mostRecentTelemetry[range];
const domainValue = utcTimeFormat(mostRecentTelemetry[domain]);
await Vue.nextTick();
await nextTick();
const actualDomainValue = parent.querySelector(TABLE_BODY_FIRST_ROW_SECOND_DATA).innerText;
const actualRangeValue = parent.querySelector(TABLE_BODY_FIRST_ROW_THIRD_DATA).innerText;
expect(actualRangeValue).toBe(rangeValue);
@ -424,7 +425,7 @@ describe('The LAD Table Set', () => {
ladTableSetView = ladTableSetViewProvider.view(mockObj.ladTableSet, [mockObj.ladTableSet]);
ladTableSetView.show(child);
return Vue.nextTick();
return nextTick();
});
it('should show one row per lad table object in the composition', () => {

View File

@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { createOpenMct, resetApplicationState, spyOnBuiltins } from 'utils/testing';
import Vue from 'vue';
import { nextTick } from 'vue';
import AutoflowTabularConstants from './AutoflowTabularConstants';
import AutoflowTabularPlugin from './AutoflowTabularPlugin';
@ -175,7 +175,7 @@ xdescribe('AutoflowTabularPlugin', () => {
view = provider.view(testObject, [testObject]);
view.show(testContainer);
return Vue.nextTick();
return nextTick();
});
afterEach(() => {

View File

@ -67,6 +67,7 @@ export default {
}
}
},
emits: ['subscribe', 'unsubscribe'],
data() {
return {
isZoomed: false,

View File

@ -6,7 +6,7 @@ import BarGraphOptions from './BarGraphOptions.vue';
export default function BarGraphInspectorViewProvider(openmct) {
return {
key: BAR_GRAPH_INSPECTOR_KEY,
name: 'Bar Graph Configuration',
name: 'Config',
canView: function (selection) {
if (selection.length === 0 || selection[0].length === 0) {
return false;

View File

@ -43,7 +43,7 @@
edit-title="Manually set the color for this bar graph series."
view-title="The color for this bar graph series."
short-label="Color"
@colorSet="setColor"
@color-set="setColor"
/>
</li>
</ul>

View File

@ -23,9 +23,9 @@
// import BarGraph from './BarGraphPlot.vue';
import EventEmitter from 'EventEmitter';
import { createOpenMct, resetApplicationState } from 'utils/testing';
import Vue from 'vue';
import { nextTick } from 'vue';
import { BAR_GRAPH_KEY, BAR_GRAPH_VIEW } from './BarGraphConstants';
import { BAR_GRAPH_INSPECTOR_KEY, BAR_GRAPH_KEY, BAR_GRAPH_VIEW } from './BarGraphConstants';
import BarGraphPlugin from './plugin';
describe('the plugin', function () {
@ -153,7 +153,7 @@ describe('the plugin', function () {
spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
await Vue.nextTick();
await nextTick();
});
it('provides a bar graph view', () => {
@ -254,7 +254,7 @@ describe('the plugin', function () {
spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
await Vue.nextTick();
await nextTick();
});
it('Renders spectral plots', async () => {
@ -305,8 +305,8 @@ describe('the plugin', function () {
barGraphView.show(child, true);
mockComposition.emit('add', dotFullTelemetryObject);
await Vue.nextTick();
await Vue.nextTick();
await nextTick();
await nextTick();
const plotElement = element.querySelector('.cartesianlayer .scatterlayer .trace .lines');
expect(plotElement).not.toBeNull();
@ -578,12 +578,10 @@ describe('the plugin', function () {
child.append(viewContainer);
const applicableViews = openmct.inspectorViews.get(selection);
plotInspectorView = applicableViews.filter(
(view) => view.name === 'Bar Graph Configuration'
)[0];
plotInspectorView = applicableViews.filter((view) => view.key === BAR_GRAPH_INSPECTOR_KEY)[0];
plotInspectorView.show(viewContainer);
await Vue.nextTick();
await nextTick();
optionsElement = element.querySelector('.c-bar-graph-options');
});

View File

@ -84,6 +84,7 @@ export default {
required: true
}
},
emits: ['on-change'],
data() {
return {
rangeMax: this.model.value.rangeMax,
@ -119,7 +120,7 @@ export default {
this.model.validate(data);
}
this.$emit('onChange', data);
this.$emit('on-change', data);
}
}
};

View File

@ -71,6 +71,7 @@ export default {
}
}
},
emits: ['subscribe', 'unsubscribe'],
data() {
return {
isZoomed: false,

View File

@ -59,7 +59,7 @@
edit-title="Manually set the line and marker color for this plot."
view-title="The line and marker color for this plot."
short-label="Color"
@colorSet="setColor"
@color-set="setColor"
/>
</ul>
</div>

View File

@ -118,7 +118,7 @@ export default function () {
onChange
};
},
template: `<scatter-plot-form :model="model" @onChange="onChange"></scatter-plot-form>`
template: `<scatter-plot-form :model="model" @on-change="onChange"></scatter-plot-form>`
},
{
app: openmct.app,

View File

@ -22,7 +22,7 @@
import EventEmitter from 'EventEmitter';
import { createOpenMct, resetApplicationState } from 'utils/testing';
import Vue from 'vue';
import { nextTick } from 'vue';
import ScatterPlotPlugin from './plugin';
import { SCATTER_PLOT_KEY, SCATTER_PLOT_VIEW } from './scatterPlotConstants';
@ -178,7 +178,7 @@ describe('the plugin', function () {
spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
await Vue.nextTick();
await nextTick();
});
it('provides a scatter plot view', () => {
@ -406,7 +406,7 @@ describe('the plugin', function () {
plotInspectorView = applicableViews[0];
plotInspectorView.show(viewContainer);
await Vue.nextTick();
await nextTick();
optionsElement = element.querySelector('.c-scatter-plot-options');
});

View File

@ -21,7 +21,7 @@
*****************************************************************************/
import { createMouseEvent, createOpenMct, resetApplicationState } from 'utils/testing';
import Vue from 'vue';
import { nextTick } from 'vue';
import ClearDataPlugin from './plugin.js';
@ -213,7 +213,7 @@ describe('The Clear Data Plugin:', () => {
});
it('renders its major elements', async () => {
await Vue.nextTick();
await nextTick();
const indicatorClass = appHolder.querySelector('.c-indicator');
const iconClass = appHolder.querySelector('.icon-clear-data');
const indicatorLabel = appHolder.querySelector('.c-indicator__label');

View File

@ -22,7 +22,7 @@
import mount from 'utils/mount';
import Clock from './components/Clock.vue';
import Clock from './components/ClockComponent.vue';
export default function ClockViewProvider(openmct) {
return {

View File

@ -22,7 +22,7 @@
import EventEmitter from 'EventEmitter';
import { createOpenMct, resetApplicationState } from 'utils/testing';
import Vue from 'vue';
import { nextTick } from 'vue';
import clockPlugin from './plugin';
@ -106,7 +106,7 @@ describe('Clock plugin:', () => {
clockView = clockViewProvider.view(mutableClockObject);
clockView.show(child);
await Vue.nextTick();
await nextTick();
await new Promise((resolve) => requestAnimationFrame(resolve));
});
@ -232,7 +232,7 @@ describe('Clock plugin:', () => {
it('contains text', async () => {
await setupClock(true);
await Vue.nextTick();
await nextTick();
await new Promise((resolve) => requestAnimationFrame(resolve));
clockIndicator = openmct.indicators.indicatorObjects.find(

View File

@ -97,8 +97,10 @@ describe('The condition', function () {
mockTimeSystems = {
key: 'utc'
};
openmct.time = jasmine.createSpyObj('time', ['getAllTimeSystems']);
openmct.time = jasmine.createSpyObj('time', ['getAllTimeSystems', 'on', 'off']);
openmct.time.getAllTimeSystems.and.returnValue([mockTimeSystems]);
openmct.time.on.and.returnValue(() => {});
openmct.time.off.and.returnValue(() => {});
testConditionDefinition = {
id: '123-456',

View File

@ -66,12 +66,12 @@
:is-editing="isEditing"
:move-index="moveIndex"
:is-dragging="isDragging"
@updateCondition="updateCondition"
@removeCondition="removeCondition"
@cloneCondition="cloneCondition"
@setMoveIndex="setMoveIndex"
@dragComplete="dragComplete"
@dropCondition="dropCondition"
@update-condition="updateCondition"
@remove-condition="removeCondition"
@clone-condition="cloneCondition"
@set-move-index="setMoveIndex"
@drag-complete="dragComplete"
@drop-condition="dropCondition"
/>
</div>
</div>
@ -79,15 +79,16 @@
</template>
<script>
import StalenessUtils from '@/utils/staleness';
import stalenessMixin from '@/ui/mixins/staleness-mixin';
import ConditionManager from '../ConditionManager';
import Condition from './Condition.vue';
import Condition from './ConditionItem.vue';
export default {
components: {
Condition
},
mixins: [stalenessMixin],
inject: ['openmct', 'domainObject'],
props: {
isEditing: Boolean,
@ -102,6 +103,12 @@ export default {
}
}
},
emits: [
'condition-set-result-updated',
'no-telemetry-objects',
'telemetry-updated',
'telemetry-staleness'
],
data() {
return {
expanded: true,
@ -135,13 +142,6 @@ export default {
if (this.stopObservingForChanges) {
this.stopObservingForChanges();
}
if (this.stalenessSubscription) {
Object.values(this.stalenessSubscription).forEach((stalenessSubscription) => {
stalenessSubscription.unsubscribe();
stalenessSubscription.stalenessUtils.destroy();
});
}
},
mounted() {
this.composition = this.openmct.composition.get(this.domainObject);
@ -153,16 +153,29 @@ export default {
this.conditionManager = new ConditionManager(this.domainObject, this.openmct);
this.conditionManager.on('conditionSetResultUpdated', this.handleConditionSetResultUpdated);
this.conditionManager.on('noTelemetryObjects', this.emitNoTelemetryObjectEvent);
this.stalenessSubscription = {};
this.setupClockChangedEvent((domainObject) => {
this.triggerUnsubscribeFromStaleness(domainObject, () => {
this.emitStaleness({
keyString: domainObject.identifier,
stalenessResponse: { isStale: false }
});
});
this.subscribeToStaleness(domainObject, (stalenessResponse) => {
this.emitStaleness({
keyString: domainObject.identifier,
stalenessResponse: stalenessResponse
});
});
});
},
methods: {
handleConditionSetResultUpdated(data) {
this.currentConditionId = data.conditionId;
this.$emit('conditionSetResultUpdated', data);
this.$emit('condition-set-result-updated', data);
},
emitNoTelemetryObjectEvent(data) {
this.currentConditionId = '';
this.$emit('noTelemetryObjects');
this.$emit('no-telemetry-objects');
},
observeForChanges() {
this.stopObservingForChanges = this.openmct.objects.observe(
@ -219,30 +232,14 @@ export default {
const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
this.telemetryObjs.push(domainObject);
this.$emit('telemetryUpdated', this.telemetryObjs);
this.$emit('telemetry-updated', this.telemetryObjs);
if (!this.stalenessSubscription[keyString]) {
this.stalenessSubscription[keyString] = {};
}
this.stalenessSubscription[keyString].stalenessUtils = new StalenessUtils(
this.openmct,
domainObject
);
this.openmct.telemetry.isStale(domainObject).then((stalenessResponse) => {
if (stalenessResponse !== undefined) {
this.handleStaleness(keyString, stalenessResponse);
}
this.subscribeToStaleness(domainObject, (stalenessResponse) => {
this.emitStaleness({
keyString,
stalenessResponse: stalenessResponse
});
});
const stalenessSubscription = this.openmct.telemetry.subscribeToStaleness(
domainObject,
(stalenessResponse) => {
this.handleStaleness(keyString, stalenessResponse);
}
);
this.stalenessSubscription[keyString].unsubscribe = stalenessSubscription;
},
removeTelemetryObject(identifier) {
const keyString = this.openmct.objects.makeKeyString(identifier);
@ -252,34 +249,20 @@ export default {
return objId === keyString;
});
const domainObject = this.telemetryObjs[index];
this.triggerUnsubscribeFromStaleness(domainObject, () => {
this.emitStaleness({
keyString,
stalenessResponse: { isStale: false }
});
});
if (index > -1) {
this.telemetryObjs.splice(index, 1);
}
if (this.stalenessSubscription[keyString]) {
this.stalenessSubscription[keyString].unsubscribe();
this.stalenessSubscription[keyString].stalenessUtils.destroy();
this.emitStaleness({
keyString,
isStale: false
});
delete this.stalenessSubscription[keyString];
}
},
handleStaleness(keyString, stalenessResponse) {
if (
this.stalenessSubscription[keyString].stalenessUtils.shouldUpdateStaleness(
stalenessResponse
)
) {
this.emitStaleness({
keyString,
isStale: stalenessResponse.isStale
});
}
},
emitStaleness(stalenessObject) {
this.$emit('telemetryStaleness', stalenessObject);
this.$emit('telemetry-staleness', stalenessObject);
},
addCondition() {
this.conditionManager.addCondition();

View File

@ -189,7 +189,7 @@ import { v4 as uuid } from 'uuid';
import { TRIGGER, TRIGGER_LABEL } from '@/plugins/condition/utils/constants';
import ConditionDescription from './ConditionDescription.vue';
import Criterion from './Criterion.vue';
import Criterion from './CriterionItem.vue';
export default {
components: {
@ -229,6 +229,14 @@ export default {
default: 0
}
},
emits: [
'set-move-index',
'drag-complete',
'drop-condition',
'remove-condition',
'clone-condition',
'update-condition'
],
data() {
return {
currentCriteria: this.currentCriteria,
@ -313,11 +321,11 @@ export default {
event.dataTransfer.setData('dragging', event.target); // required for FF to initiate drag
event.dataTransfer.effectAllowed = 'copyMove';
event.dataTransfer.setDragImage(event.target.closest('.c-condition-h'), 0, 0);
this.$emit('setMoveIndex', this.conditionIndex);
this.$emit('set-move-index', this.conditionIndex);
},
dragEnd() {
this.dragStarted = false;
this.$emit('dragComplete');
this.$emit('drag-complete');
},
dropCondition(event, targetIndex) {
if (!this.isDragging) {
@ -331,7 +339,7 @@ export default {
if (this.isValidTarget(targetIndex)) {
this.dragElement = undefined;
this.draggingOver = false;
this.$emit('dropCondition', targetIndex);
this.$emit('drop-condition', targetIndex);
}
},
dragEnter(event, targetIndex) {
@ -359,10 +367,10 @@ export default {
},
destroy() {},
removeCondition() {
this.$emit('removeCondition', this.condition.id);
this.$emit('remove-condition', this.condition.id);
},
cloneCondition() {
this.$emit('cloneCondition', {
this.$emit('clone-condition', {
condition: this.condition,
index: this.conditionIndex
});
@ -380,7 +388,7 @@ export default {
this.persist();
},
persist() {
this.$emit('updateCondition', {
this.$emit('update-condition', {
condition: this.condition
});
},

View File

@ -39,22 +39,24 @@
:is-editing="isEditing"
:test-data="testData"
:telemetry="telemetryObjs"
@updateTestData="updateTestData"
@update-test-data="updateTestData"
/>
<ConditionCollection
class="c-cs__conditions"
:is-editing="isEditing"
:test-data="testData"
@conditionSetResultUpdated="updateCurrentOutput"
@noTelemetryObjects="updateCurrentOutput('---')"
@telemetryUpdated="updateTelemetry"
@telemetryStaleness="handleStaleness"
@condition-set-result-updated="updateCurrentOutput"
@no-telemetry-objects="updateCurrentOutput('---')"
@telemetry-updated="updateTelemetry"
@telemetry-staleness="handleStaleness"
/>
</div>
</div>
</template>
<script>
import stalenessMixin from '@/ui/mixins/staleness-mixin';
import ConditionCollection from './ConditionCollection.vue';
import TestData from './TestData.vue';
@ -63,6 +65,7 @@ export default {
TestData,
ConditionCollection
},
mixins: [stalenessMixin],
inject: ['openmct', 'domainObject'],
props: {
isEditing: Boolean
@ -71,15 +74,9 @@ export default {
return {
currentConditionOutput: '',
telemetryObjs: [],
testData: {},
staleObjects: []
testData: {}
};
},
computed: {
isStale() {
return this.staleObjects.length !== 0;
}
},
mounted() {
this.conditionSetIdentifier = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.testData = {
@ -100,17 +97,8 @@ export default {
updateTestData(testData) {
this.testData = testData;
},
handleStaleness({ keyString, isStale }) {
const index = this.staleObjects.indexOf(keyString);
if (isStale) {
if (index === -1) {
this.staleObjects.push(keyString);
}
} else {
if (index !== -1) {
this.staleObjects.splice(index, 1);
}
}
handleStaleness({ keyString, stalenessResponse }) {
this.addOrRemoveStaleObject(keyString, stalenessResponse);
}
}
};

View File

@ -136,6 +136,7 @@ export default {
required: true
}
},
emits: ['persist'],
data() {
return {
telemetryMetadataOptions: [],

View File

@ -127,6 +127,7 @@ export default {
}
}
},
emits: ['update-test-data'],
data() {
return {
expanded: true,
@ -225,7 +226,7 @@ export default {
this.updateTestData();
},
updateTestData() {
this.$emit('updateTestData', {
this.$emit('update-test-data', {
applied: this.isApplied,
conditionTestInputs: this.testInputs
});

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