Compare commits
10 Commits
fix-webgl-
...
one-weird-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
850aca546b | ||
|
|
22962452e3 | ||
|
|
cfdc6d7f20 | ||
|
|
bafe8c33a9 | ||
|
|
62dcf76798 | ||
|
|
04adb790a0 | ||
|
|
6f46b4d87e | ||
|
|
4fff6b035b | ||
|
|
d48e11bbf7 | ||
|
|
8cc4eca3e8 |
@@ -2,7 +2,7 @@ version: 2.1
|
||||
executors:
|
||||
pw-focal-development:
|
||||
docker:
|
||||
- image: mcr.microsoft.com/playwright:v1.39.0-focal
|
||||
- image: mcr.microsoft.com/playwright:v1.36.2-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
|
||||
@@ -94,6 +94,7 @@ jobs:
|
||||
- build_and_install:
|
||||
node-version: <<parameters.node-version>>
|
||||
- run: npm run lint
|
||||
- run: npm run lint:spelling
|
||||
- generate_and_store_version_and_filesystem_artifacts
|
||||
unit-test:
|
||||
parameters:
|
||||
@@ -162,7 +163,7 @@ jobs:
|
||||
steps:
|
||||
- build_and_install:
|
||||
node-version: <<parameters.node-version>>
|
||||
- run: npx playwright@1.39.0 install #Necessary for bare ubuntu machine
|
||||
- run: npx playwright@1.36.2 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
|
||||
@@ -197,9 +198,7 @@ jobs:
|
||||
steps:
|
||||
- build_and_install:
|
||||
node-version: <<parameters.node-version>>
|
||||
- run: npm run test:perf:memory
|
||||
- run: npm run test:perf:localhost
|
||||
- run: npm run test:perf:contract
|
||||
- run: npm run test:perf
|
||||
- store_test_results:
|
||||
path: test-results/results.xml
|
||||
- store_artifacts:
|
||||
@@ -237,8 +236,8 @@ workflows:
|
||||
overall-circleci-commit-status: #These jobs run on every commit
|
||||
jobs:
|
||||
- lint:
|
||||
name: node20-lint
|
||||
node-version: lts/iron
|
||||
name: node16-lint
|
||||
node-version: lts/gallium
|
||||
- unit-test:
|
||||
name: node18-chrome
|
||||
node-version: lts/hydrogen
|
||||
@@ -256,8 +255,8 @@ workflows:
|
||||
the-nightly: #These jobs do not run on PRs, but against master at night
|
||||
jobs:
|
||||
- unit-test:
|
||||
name: node20-chrome-nightly
|
||||
node-version: lts/iron
|
||||
name: node16-chrome-nightly
|
||||
node-version: lts/gallium
|
||||
- unit-test:
|
||||
name: node18-chrome
|
||||
node-version: lts/hydrogen
|
||||
|
||||
15
.cspell.json
15
.cspell.json
@@ -479,18 +479,7 @@
|
||||
"mediump",
|
||||
"sinonjs",
|
||||
"generatedata",
|
||||
"grandsearch",
|
||||
"websockets",
|
||||
"swgs",
|
||||
"memlab",
|
||||
"devmode",
|
||||
"blockquote",
|
||||
"blockquotes",
|
||||
"Blockquote",
|
||||
"Blockquotes",
|
||||
"oger",
|
||||
"lcovonly",
|
||||
"gcov"
|
||||
"grandsearch"
|
||||
],
|
||||
"dictionaries": ["npm", "softwareTerms", "node", "html", "css", "bash", "en_US"],
|
||||
"ignorePaths": [
|
||||
@@ -503,4 +492,4 @@
|
||||
"html-test-results",
|
||||
"test-results"
|
||||
]
|
||||
}
|
||||
}
|
||||
21
.eslintrc.js
21
.eslintrc.js
@@ -9,14 +9,13 @@ module.exports = {
|
||||
globals: {
|
||||
_: 'readonly'
|
||||
},
|
||||
plugins: ['prettier', 'unicorn', 'simple-import-sort'],
|
||||
plugins: ['prettier'],
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:compat/recommended',
|
||||
'plugin:vue/vue3-recommended',
|
||||
'plugin:you-dont-need-lodash-underscore/compatible',
|
||||
'plugin:prettier/recommended',
|
||||
'plugin:no-unsanitized/DOM'
|
||||
'plugin:prettier/recommended'
|
||||
],
|
||||
parser: 'vue-eslint-parser',
|
||||
parserOptions: {
|
||||
@@ -29,8 +28,6 @@ module.exports = {
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
'simple-import-sort/imports': 'warn',
|
||||
'simple-import-sort/exports': 'warn',
|
||||
'vue/no-deprecated-dollar-listeners-api': 'warn',
|
||||
'vue/no-deprecated-events-api': 'warn',
|
||||
'vue/no-v-for-template-key': 'off',
|
||||
@@ -146,26 +143,18 @@ module.exports = {
|
||||
'no-implicit-coercion': 'error',
|
||||
//https://eslint.org/docs/rules/no-unneeded-ternary
|
||||
'no-unneeded-ternary': 'error',
|
||||
'unicorn/filename-case': [
|
||||
'error',
|
||||
{
|
||||
cases: {
|
||||
pascalCase: true
|
||||
},
|
||||
ignore: ['^.*\\.js$']
|
||||
}
|
||||
],
|
||||
'vue/first-attribute-linebreak': 'error',
|
||||
'vue/multiline-html-element-content-newline': 'off',
|
||||
'vue/singleline-html-element-content-newline': 'off',
|
||||
'vue/no-mutating-props': 'off' // TODO: Remove this rule and fix resulting errors
|
||||
'vue/multi-word-component-names': 'off', // TODO enable, align with conventions
|
||||
'vue/no-mutating-props': 'off'
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: LEGACY_FILES,
|
||||
rules: {
|
||||
'no-unused-vars': [
|
||||
'error',
|
||||
'warn',
|
||||
{
|
||||
vars: 'all',
|
||||
args: 'none',
|
||||
|
||||
4
.github/dependabot.yml
vendored
4
.github/dependabot.yml
vendored
@@ -5,7 +5,6 @@ updates:
|
||||
schedule:
|
||||
interval: 'weekly'
|
||||
open-pull-requests-limit: 10
|
||||
rebase-strategy: 'disabled'
|
||||
labels:
|
||||
- 'pr:daveit'
|
||||
- 'pr:e2e'
|
||||
@@ -29,13 +28,10 @@ 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
23
.github/release.yml
vendored
@@ -1,23 +0,0 @@
|
||||
changelog:
|
||||
categories:
|
||||
- title: 🏕 Features
|
||||
labels:
|
||||
- type:feature
|
||||
- title: 🎉 Enhancements
|
||||
labels:
|
||||
- type:enhancement
|
||||
exclude:
|
||||
labels:
|
||||
- type:feature
|
||||
- title: 🔧 Maintenance
|
||||
labels:
|
||||
- type:maintenance
|
||||
- title: ⚡ Performance
|
||||
labels:
|
||||
- performance
|
||||
- title: 👒 Dependencies
|
||||
labels:
|
||||
- dependencies
|
||||
- title: 🐛 Bug Fixes
|
||||
labels:
|
||||
- '*'
|
||||
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
|
||||
12
.github/workflows/e2e-couchdb.yml
vendored
12
.github/workflows/e2e-couchdb.yml
vendored
@@ -15,8 +15,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 'lts/hydrogen'
|
||||
|
||||
@@ -27,17 +27,17 @@ 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
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v2
|
||||
continue-on-error: true
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- run: npx playwright@1.39.0 install
|
||||
|
||||
- run: npx playwright@1.36.2 install
|
||||
|
||||
- name: Start CouchDB Docker Container and Init with Setup Scripts
|
||||
run: |
|
||||
|
||||
12
.github/workflows/e2e-pr.yml
vendored
12
.github/workflows/e2e-pr.yml
vendored
@@ -20,11 +20,11 @@ jobs:
|
||||
- ubuntu-latest
|
||||
- windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
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.39.0 install
|
||||
|
||||
- run: npx playwright@1.36.2 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}`);
|
||||
}
|
||||
}
|
||||
8
.github/workflows/npm-prerelease.yml
vendored
8
.github/workflows/npm-prerelease.yml
vendored
@@ -11,8 +11,8 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: lts/hydrogen
|
||||
- run: npm install
|
||||
@@ -26,8 +26,8 @@ jobs:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: lts/hydrogen
|
||||
registry-url: https://registry.npmjs.org/
|
||||
|
||||
16
.github/workflows/pr-platform.yml
vendored
16
.github/workflows/pr-platform.yml
vendored
@@ -22,17 +22,17 @@ jobs:
|
||||
- macos-latest
|
||||
- windows-latest
|
||||
node_version:
|
||||
- lts/iron
|
||||
- lts/gallium
|
||||
- lts/hydrogen
|
||||
architecture:
|
||||
- x64
|
||||
|
||||
|
||||
name: Node ${{ matrix.node_version }} - ${{ matrix.architecture }} on ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node_version }}
|
||||
architecture: ${{ matrix.architecture }}
|
||||
@@ -46,11 +46,11 @@ jobs:
|
||||
${{ runner.os }}-${{ matrix.node_version }}-
|
||||
|
||||
- run: npm install --cache ~/.npm --no-audit --progress=false
|
||||
|
||||
|
||||
- run: npm test
|
||||
|
||||
|
||||
- run: npm run lint -- --quiet
|
||||
|
||||
|
||||
- name: Remove pr:platform label (if present)
|
||||
if: always()
|
||||
uses: actions/github-script@v6
|
||||
|
||||
@@ -33,16 +33,6 @@ const projectRootDir = path.resolve(__dirname, '..');
|
||||
/** @type {import('webpack').Configuration} */
|
||||
const config = {
|
||||
context: projectRootDir,
|
||||
devServer: {
|
||||
client: {
|
||||
progress: true,
|
||||
overlay: {
|
||||
// Disable overlay for runtime errors.
|
||||
// See: https://github.com/webpack/webpack-dev-server/issues/4771
|
||||
runtimeErrors: false
|
||||
}
|
||||
}
|
||||
},
|
||||
entry: {
|
||||
openmct: './openmct.js',
|
||||
generatorWorker: './example/generator/generatorWorker.js',
|
||||
@@ -71,12 +61,14 @@ const config = {
|
||||
bourbon: 'bourbon.scss',
|
||||
'plotly-basic': 'plotly.js-basic-dist',
|
||||
'plotly-gl2d': 'plotly.js-gl2d-dist',
|
||||
'd3-scale': path.join(projectRootDir, 'node_modules/d3-scale/dist/d3-scale.min.js'),
|
||||
printj: path.join(projectRootDir, 'node_modules/printj/dist/printj.min.js'),
|
||||
styles: path.join(projectRootDir, 'src/styles'),
|
||||
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')
|
||||
utils: path.join(projectRootDir, 'src/utils'),
|
||||
vue: path.join(projectRootDir, 'node_modules/@vue/compat/dist/vue.esm-bundler.js'),
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
@@ -84,9 +76,7 @@ const config = {
|
||||
__OPENMCT_VERSION__: `'${packageDefinition.version}'`,
|
||||
__OPENMCT_BUILD_DATE__: `'${new Date()}'`,
|
||||
__OPENMCT_REVISION__: `'${gitRevision}'`,
|
||||
__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
|
||||
__OPENMCT_BUILD_BRANCH__: `'${gitBranch}'`
|
||||
}),
|
||||
new VueLoaderPlugin(),
|
||||
new CopyWebpackPlugin({
|
||||
@@ -110,12 +100,6 @@ const config = {
|
||||
new MiniCssExtractPlugin({
|
||||
filename: '[name].css',
|
||||
chunkFilename: '[name].css'
|
||||
}),
|
||||
// Add a UTF-8 BOM to CSS output to avoid random mojibake
|
||||
new webpack.BannerPlugin({
|
||||
test: /.*Theme\.css$/,
|
||||
raw: true,
|
||||
banner: '@charset "UTF-8";'
|
||||
})
|
||||
],
|
||||
module: {
|
||||
@@ -141,8 +125,10 @@ const config = {
|
||||
loader: 'vue-loader',
|
||||
options: {
|
||||
compilerOptions: {
|
||||
hoistStatic: false,
|
||||
whitespace: 'preserve'
|
||||
whitespace: 'preserve',
|
||||
compatConfig: {
|
||||
MODE: 2
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -45,6 +45,14 @@ module.exports = merge(common, {
|
||||
directory: path.join(__dirname, '..', '/dist'),
|
||||
publicPath: '/dist',
|
||||
watch: false
|
||||
},
|
||||
client: {
|
||||
progress: true,
|
||||
overlay: {
|
||||
// Disable overlay for runtime errors.
|
||||
// See: https://github.com/webpack/webpack-dev-server/issues/4771
|
||||
runtimeErrors: false
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
298
API.md
298
API.md
@@ -44,10 +44,8 @@
|
||||
- [Clocks](#clocks)
|
||||
- [Defining and registering clocks](#defining-and-registering-clocks)
|
||||
- [Getting and setting active clock](#getting-and-setting-active-clock)
|
||||
- [⚠️ \[DEPRECATED\] Stopping an active clock](#️-deprecated-stopping-an-active-clock)
|
||||
- [Stopping an active clock](#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)
|
||||
@@ -94,9 +92,6 @@ 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
|
||||
@@ -593,108 +588,35 @@ 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, provided here as a TypeScript interface:
|
||||
They have a simple structure:
|
||||
|
||||
```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;
|
||||
}
|
||||
```
|
||||
- `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.
|
||||
|
||||
##### 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) {
|
||||
@@ -763,9 +685,8 @@ 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
|
||||
|
||||
@@ -816,38 +737,28 @@ numbers in UTC terrestrial time.
|
||||
|
||||
#### Getting and Setting the Active Time System
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
```javascript
|
||||
openmct.time.setTimeSystem('utc', bounds);
|
||||
```
|
||||
|
||||
The current time system can be retrieved as well by calling `getTimeSystem`.
|
||||
|
||||
```javascript
|
||||
openmct.time.getTimeSystem();
|
||||
openmct.time.timeSystem('utc', bounds);
|
||||
```
|
||||
|
||||
A time system can be immediately activated after registration:
|
||||
|
||||
```javascript
|
||||
openmct.time.addTimeSystem(utcTimeSystem);
|
||||
openmct.time.setTimeSystem(utcTimeSystem, bounds);
|
||||
openmct.time.timeSystem(utcTimeSystem, 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.
|
||||
|
||||
|
||||
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.
|
||||
|
||||
#### Time Bounds
|
||||
|
||||
The TimeAPI provides a getter and setter for querying and setting time bounds. Time
|
||||
The TimeAPI provides a getter/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).
|
||||
@@ -857,34 +768,26 @@ telemetry views.
|
||||
This will be used as the end of the time period displayed by time-responsive
|
||||
telemetry views.
|
||||
|
||||
New bounds can be set system wide by calling `setBounds` with [bounds](#time-bounds).
|
||||
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.
|
||||
|
||||
``` javascript
|
||||
const ONE_HOUR = 60 * 60 * 1000;
|
||||
let now = Date.now();
|
||||
openmct.time.setBounds({start: now - ONE_HOUR, now);
|
||||
openmct.time.bounds({start: now - ONE_HOUR, now);
|
||||
```
|
||||
|
||||
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)
|
||||
To respond to bounds change events, listen for the [`'bounds'`](#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 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.
|
||||
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.
|
||||
|
||||
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
|
||||
@@ -952,29 +855,23 @@ 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 `setClock` function on the
|
||||
Once registered a clock can be activated by calling the `clock` 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 and start the new clock. [`clockOffsets`](#clock-offsets) must be specified when changing a clock.
|
||||
active clock. [`clockOffsets`](#clock-offsets) must be specified when changing a clock.
|
||||
|
||||
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().
|
||||
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().
|
||||
|
||||
```
|
||||
openmct.time.setClock(someClock, clockOffsets);
|
||||
openmct.time.clock(someClock, clockOffsets);
|
||||
```
|
||||
|
||||
Upon being activated, the time API will listen for tick events on the clock by calling `clock.on`.
|
||||
|
||||
The currently active clock can be retrieved by calling `getClock`.
|
||||
The currently active clock (if any) can be retrieved by calling the same
|
||||
function without any arguments.
|
||||
|
||||
```
|
||||
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
|
||||
#### Stopping an active clock
|
||||
|
||||
_As of July 2023, this method will be deprecated. Open MCT will always have a ticking clock._
|
||||
|
||||
@@ -985,14 +882,12 @@ 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 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).
|
||||
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).
|
||||
|
||||
Unlike bounds, which represent absolute time values, clock offsets represent
|
||||
relative time spans. Offsets are defined as an object with two properties:
|
||||
@@ -1003,77 +898,21 @@ 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 `setClockOffsets` function can be used to get or set clock offsets. For example,
|
||||
The `clockOffsets` 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.setClockOffsets({
|
||||
openmct.time.clockOffsets({
|
||||
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 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
|
||||
}
|
||||
```
|
||||
Clock offsets are only relevant when a clock source is active.
|
||||
|
||||
### Time Events
|
||||
|
||||
@@ -1082,7 +921,7 @@ The Time API is a standard event emitter; you can register callbacks for events
|
||||
For example:
|
||||
|
||||
``` javascript
|
||||
openmct.time.on('boundsChanged', function callback (newBounds, tick) {
|
||||
openmct.time.on('bounds', function callback (newBounds, tick) {
|
||||
// Do something with new bounds
|
||||
});
|
||||
```
|
||||
@@ -1091,7 +930,7 @@ openmct.time.on('boundsChanged', function callback (newBounds, tick) {
|
||||
|
||||
The events emitted by the Time API are:
|
||||
|
||||
- `boundsChanged`: emitted whenever the bounds change. The callback will be invoked
|
||||
- `bounds`: 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.
|
||||
@@ -1106,24 +945,15 @@ 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.
|
||||
- `timeSystemChanged`: emitted whenever the active time system changes. The callback will be invoked with a single argument:
|
||||
- `timeSystem`: 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).
|
||||
- `clockChanged`: emitted whenever the clock changes. The callback will be invoked
|
||||
- `clock`: 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.
|
||||
- `clockOffsetsChanged`: emitted whenever the active clock offsets change. The
|
||||
- `clockOffsets`: 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
|
||||
|
||||
|
||||
56
README.md
56
README.md
@@ -2,9 +2,7 @@
|
||||
|
||||
Open MCT (Open Mission Control Technologies) is a next-generation mission control framework for visualization of data on desktop and mobile devices. It is developed at NASA's Ames Research Center, and is being used by NASA for data analysis of spacecraft missions, as well as planning and operation of experimental rover systems. As a generalizable and open source framework, Open MCT could be used as the basis for building applications for planning, operation, and analysis of any systems producing telemetry data.
|
||||
|
||||
> [!NOTE]
|
||||
> Please visit our [Official Site](https://nasa.github.io/openmct/) and [Getting Started Guide](https://nasa.github.io/openmct/getting-started/)
|
||||
|
||||
Please visit our [Official Site](https://nasa.github.io/openmct/) and [Getting Started Guide](https://nasa.github.io/openmct/getting-started/)
|
||||
|
||||
Once you've created something amazing with Open MCT, showcase your work in our GitHub Discussions [Show and Tell](https://github.com/nasa/openmct/discussions/categories/show-and-tell) section. We love seeing unique and wonderful implementations of Open MCT!
|
||||
|
||||
@@ -16,32 +14,20 @@ Once you've created something amazing with Open MCT, showcase your work in our G
|
||||
Building and running Open MCT in your local dev environment is very easy. Be sure you have [Git](https://git-scm.com/downloads) and [Node.js](https://nodejs.org/) installed, then follow the directions below. Need additional information? Check out the [Getting Started](https://nasa.github.io/openmct/getting-started/) page on our website.
|
||||
(These instructions assume you are installing as a non-root user; developers have [reported issues](https://github.com/nasa/openmct/issues/1151) running these steps with root privileges.)
|
||||
|
||||
1. Clone the source code:
|
||||
1. Clone the source code
|
||||
|
||||
```
|
||||
git clone https://github.com/nasa/openmct.git
|
||||
```
|
||||
`git clone https://github.com/nasa/openmct.git`
|
||||
|
||||
2. (Optional) Install the correct node version using [nvm](https://github.com/nvm-sh/nvm):
|
||||
2. (Optionally) Install the correct node version using [nvm](https://github.com/nvm-sh/nvm) (`nvm install`)
|
||||
3. Install development dependencies. Note: Check the package.json engine for our tested and supported node versions.
|
||||
|
||||
```
|
||||
nvm install
|
||||
```
|
||||
`npm install`
|
||||
|
||||
3. Install development dependencies (Note: Check the `package.json` engine for our tested and supported node versions):
|
||||
4. Run a local development server
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
`npm start`
|
||||
|
||||
4. Run a local development server:
|
||||
|
||||
```
|
||||
npm start
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Open MCT is now running, and can be accessed by pointing a web browser at [http://localhost:8080/](http://localhost:8080/)
|
||||
Open MCT is now running, and can be accessed by pointing a web browser at [http://localhost:8080/](http://localhost:8080/)
|
||||
|
||||
Open MCT is built using [`npm`](http://npmjs.com/) and [`webpack`](https://webpack.js.org/).
|
||||
|
||||
@@ -55,12 +41,8 @@ The clearest examples for developing Open MCT plugins are in the
|
||||
[tutorials](https://github.com/nasa/openmct-tutorial) provided in
|
||||
our documentation.
|
||||
|
||||
> [!NOTE]
|
||||
> We want Open MCT to be as easy to use, install, run, and develop for as
|
||||
> possible, and your feedback will help us get there!
|
||||
> Feedback can be provided via [GitHub issues](https://github.com/nasa/openmct/issues/new/choose),
|
||||
> [Starting a GitHub Discussion](https://github.com/nasa/openmct/discussions),
|
||||
> or by emailing us at [arc-dl-openmct@mail.nasa.gov](mailto:arc-dl-openmct@mail.nasa.gov).
|
||||
We want Open MCT to be as easy to use, install, run, and develop for as
|
||||
possible, and your feedback will help us get there! Feedback can be provided via [GitHub issues](https://github.com/nasa/openmct/issues/new/choose), [Starting a GitHub Discussion](https://github.com/nasa/openmct/discussions), or by emailing us at [arc-dl-openmct@mail.nasa.gov](mailto:arc-dl-openmct@mail.nasa.gov).
|
||||
|
||||
## Developing Applications With Open MCT
|
||||
|
||||
@@ -127,8 +109,6 @@ 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
|
||||
@@ -184,17 +164,3 @@ 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. |
|
||||
|
||||
|
||||
83
TESTING.md
83
TESTING.md
@@ -37,85 +37,14 @@ Documentation located [here](./e2e/README.md)
|
||||
|
||||
## Code Coverage
|
||||
|
||||
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.
|
||||
* 100% statement coverage is achievable and desirable.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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)
|
||||
|
||||
## 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>
|
||||
```
|
||||
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)
|
||||
|
||||
@@ -3,22 +3,15 @@ snapshot:
|
||||
widths: [1024]
|
||||
min-height: 1440 # px
|
||||
percyCSS: |
|
||||
/* Clock indicator... your days are numbered */
|
||||
.t-indicator-clock > .label {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
.c-input--datetime {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
/* Timer object text */
|
||||
.c-ne__time-and-creator {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
/* Time Conductor ticks */
|
||||
div.c-conductor-axis.c-conductor__ticks > svg {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
/* Embedded timestamp in notebooks */
|
||||
.c-ne__embed__time{
|
||||
opacity: 0 !important;
|
||||
}
|
||||
div.c-inspector__properties.c-inspect-properties > ul > li:nth-child(3) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@@ -3,22 +3,15 @@ snapshot:
|
||||
widths: [1024, 2000]
|
||||
min-height: 1440 # px
|
||||
percyCSS: |
|
||||
/* Clock indicator... your days are numbered */
|
||||
.t-indicator-clock > .label {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
.c-input--datetime {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
/* Timer object text */
|
||||
.c-ne__time-and-creator {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
/* Time Conductor ticks */
|
||||
div.c-conductor-axis.c-conductor__ticks > svg {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
/* Embedded timestamp in notebooks */
|
||||
.c-ne__embed__time{
|
||||
opacity: 0 !important;
|
||||
}
|
||||
div.c-inspector__properties.c-inspect-properties > ul > li:nth-child(3) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
176
e2e/README.md
176
e2e/README.md
@@ -82,20 +82,13 @@ To make this possible, we're leveraging a 3rd party service, [Percy](https://per
|
||||
|
||||
At present, we are using percy with two configuration files: `./e2e/.percy.nightly.yml` and `./e2e/.percy.ci.yml`. This is mainly to reduce the number of snapshots.
|
||||
|
||||
### Advanced: Snapshot Testing (Not Recommended)
|
||||
### (Advanced) Snapshot Testing
|
||||
|
||||
While snapshot testing offers a precise way to detect changes in your application without relying on third-party services like Percy.io, we've found that it doesn't offer any advantages over visual testing in our use-cases. Therefore, snapshot testing is **not recommended** for further implementation.
|
||||
Snapshot testing is very similar to visual testing but allows us to be more precise in detecting change without relying on a 3rd party service. Unfortuantely, this precision requires advanced test setup and teardown and so we're using this pattern as a last resort.
|
||||
|
||||
#### CI vs Manual Checks
|
||||
Snapshot tests can be reliably executed in Continuous Integration (CI) environments but lack the manual oversight provided by visual testing platforms like Percy.io. This means they may miss issues that a human reviewer could catch during manual checks.
|
||||
|
||||
#### Example
|
||||
A single visual test assertion in Percy.io can be executed across 10 different browser and resolution combinations without additional setup, providing comprehensive testing with minimal configuration. In contrast, a snapshot test is restricted to a single OS and browser resolution, requiring more effort to achieve the same level of coverage.
|
||||
|
||||
|
||||
#### Further Reading
|
||||
For those interested in the mechanics of snapshot testing with Playwright, you can refer to the [Playwright Snapshots Documentation](https://playwright.dev/docs/test-snapshots). However, keep in mind that we do not recommend using this approach.
|
||||
To give an example, if a _single_ visual test assertion for an Overlay plot is run through multiple DOM rendering engines at various viewports to see how the Plot looks. If that same test were run as a snapshot test, it could only be executed against a single browser, on a single platform (ubuntu docker container).
|
||||
|
||||
Read more about [Playwright Snapshots](https://playwright.dev/docs/test-snapshots)
|
||||
|
||||
#### Open MCT's implementation
|
||||
|
||||
@@ -134,11 +127,11 @@ npm run test:e2e:updatesnapshots
|
||||
|
||||
## Performance Testing
|
||||
|
||||
The open source performance tests function in three ways which match their naming and folder structure:
|
||||
The open source performance tests function mostly as a contract for the locator logic, functionality, and assumptions will work in our downstream, closed source test suites.
|
||||
|
||||
`./e2e/tests/performance` - The tests at the root of this folder path detect functional changes which are mostly apparent with large performance regressions like [this](https://github.com/nasa/openmct/issues/6879). These tests run against openmct webpack in `production-mode` with the `npm run test:perf:localhost` script.
|
||||
`./e2e/tests/performance/contract/` - These tests serve as [contracts](https://martinfowler.com/bliki/ContractTest.html) for the locator logic, functionality, and assumptions will work in our downstream, closed source test suites. These tests run against openmct webpack in `dev-mode` with the `npm run test:perf:contract` script.
|
||||
`./e2e/tests/performance/memory/` - These tests execute memory leak detection checks in various ways. This is expected to evolve as we move to the `memlab` project. These tests run against openmct webpack in `production-mode` with the `npm run test:perf:memory` script.
|
||||
They're found under `./e2e/tests/performance` and are to be executed with the following npm script:
|
||||
|
||||
`npm run test:perf`
|
||||
|
||||
These tests are expected to become blocking and gating with assertions as we extend the capabilities of Playwright.
|
||||
|
||||
@@ -158,11 +151,8 @@ Our file structure follows the type of type of testing being excercised at the e
|
||||
|`./tests/functional/example/` | Tests which specifically verify the example plugins (e.g.: Sine Wave Generator).|
|
||||
|`./tests/functional/plugins/` | Tests which loosely test each plugin. This folder is the most likely to change. Note: some `@snapshot` tests are still contained within this structure.|
|
||||
|`./tests/framework/` | Tests which verify that our testing framework's functionality and assumptions will continue to work based on further refactoring or Playwright version changes (e.g.: verifying custom fixtures and appActions).|
|
||||
|`./tests/performance/` | Performance tests which should be run on every commit.|
|
||||
|`./tests/performance/contract/` | A subset of performance tests which are designed to provide a contract between the open source tests which are run on every commit and the downstream tests which are run post merge and with other frameworks.|
|
||||
|`./tests/performance/memory` | A subset of performance tests which are designed to test for memory leaks.|
|
||||
|`./tests/performance/` | Performance tests.|
|
||||
|`./tests/visual/` | Visual tests.|
|
||||
|`./tests/visual/component/` | Visual tests which are only run against a single component.|
|
||||
|`./appActions.js` | Contains common methods which can be leveraged by test case authors to quickly move through the application when writing new tests.|
|
||||
|`./baseFixture.js` | Contains base fixtures which only extend default `@playwright/test` functionality. The expectation is that these fixtures will be removed as the native Playwright API improves|
|
||||
|
||||
@@ -179,7 +169,6 @@ Open MCT is leveraging the [config file](https://playwright.dev/docs/test-config
|
||||
|`./playwright-ci.config.js` | Used when running in CI or to debug CI issues locally|
|
||||
|`./playwright-local.config.js` | Used when running locally|
|
||||
|`./playwright-performance.config.js` | Used when running performance tests in CI or locally|
|
||||
|`./playwright-performance-devmode.config.js` | Used when running performance tests in CI or locally|
|
||||
|`./playwright-visual.config.js` | Used to run the visual tests in CI or locally|
|
||||
|
||||
#### Test Tags
|
||||
@@ -193,7 +182,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). See [note](#utilizing-localstorage)|
|
||||
|`@localStorage` | Captures or generates session storage to manipulate browser state. Useful for excluding in tests which require a persistent backend (i.e. CouchDB).|
|
||||
|`@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.|
|
||||
@@ -319,27 +308,14 @@ Skipping based on browser version (Rarely used): <https://github.com/microsoft/p
|
||||
|
||||
## Test Design, Best Practices, and Tips & Tricks
|
||||
|
||||
### Test Design
|
||||
### Test Design (TODO)
|
||||
|
||||
#### Test as the User
|
||||
- How to make tests robust to function in other contexts (VISTA, VIPER, etc.)
|
||||
- Leverage the use of `appActions.js` methods such as `createDomainObjectWithDefaults()`
|
||||
- How to make tests faster and more resilient
|
||||
- When possible, navigate directly by URL:
|
||||
|
||||
In general, strive to test only through the UI as a user would. As stated in the [Playwright Best Practices](https://playwright.dev/docs/best-practices#test-user-visible-behavior):
|
||||
|
||||
> "Automated tests should verify that the application code works for the end users, and avoid relying on implementation details such as things which users will not typically use, see, or even know about such as the name of a function, whether something is an array, or the CSS class of some element. The end user will see or interact with what is rendered on the page, so your test should typically only see/interact with the same rendered output."
|
||||
|
||||
By adhering to this principle, we can create tests that are both robust and reflective of actual user experiences.
|
||||
|
||||
#### How to make tests robust to function in other contexts (VISTA, COUCHDB, YAMCS, VIPER, etc.)
|
||||
1. Leverage the use of `appActions.js` methods such as `createDomainObjectWithDefaults()`. This ensures that your tests will create unique instances of objects for your test to interact with.
|
||||
1. Do not assert on the order or structure of objects available unless you created them yourself. These tests may be used against a persistent datastore like couchdb with many objects in the tree.
|
||||
1. Do not search for your created objects. Open MCT does not performance uniqueness checks so it's possible that your tests will break when run twice.
|
||||
1. Avoid creating locator aliases. This likely means that you're compensating for a bad locator. Improve the application instead.
|
||||
1. Leverage `await page.goto('./', { waitUntil: 'domcontentloaded' });` instead of `{ waitUntil: 'networkidle' }`. Tests run against deployments with websockets often have issues with the networkidle detection.
|
||||
|
||||
#### How to make tests faster and more resilient
|
||||
1. Avoid app interaction when possible. The best way of doing this is to navigate directly by URL:
|
||||
|
||||
```js
|
||||
```javascript
|
||||
// You can capture the CreatedObjectInfo returned from this appAction:
|
||||
const clock = await createDomainObjectWithDefaults(page, { type: 'Clock' });
|
||||
|
||||
@@ -347,36 +323,12 @@ By adhering to this principle, we can create tests that are both robust and refl
|
||||
await page.goto(clock.url);
|
||||
```
|
||||
|
||||
1. Leverage `await page.goto('./', { waitUntil: 'domcontentloaded' });`
|
||||
- Leverage `await page.goto('./', { waitUntil: 'domcontentloaded' });`
|
||||
- Initial navigation should _almost_ always use the `{ waitUntil: 'domcontentloaded' }` option.
|
||||
1. Avoid repeated setup to test a single assertion. Write longer tests with multiple soft assertions.
|
||||
This ensures that your changes will be picked up with large refactors.
|
||||
|
||||
##### 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)
|
||||
});
|
||||
```
|
||||
|
||||
- Avoid repeated setup to test to test a single assertion. Write longer tests with multiple soft assertions.
|
||||
|
||||
### How to write a great test
|
||||
|
||||
- Avoid using css locators to find elements to the page. Use modern web accessible locators like `getByRole`
|
||||
- Use our [App Actions](./appActions.js) for performing common actions whenever applicable.
|
||||
- Use `waitForPlotsToRender()` before asserting against anything that is dependent upon plot series data being loaded and drawn.
|
||||
- If you create an object outside of using the `createDomainObjectWithDefaults` App Action, make sure to fill in the 'Notes' section of your object with `page.testNotes`:
|
||||
@@ -389,39 +341,26 @@ By adhering to this principle, we can create tests that are both robust and refl
|
||||
await notesInput.fill(testNotes);
|
||||
```
|
||||
|
||||
#### How to Write a Great Visual Test
|
||||
#### How to write a great visual test
|
||||
|
||||
1. **Look for the Unknown Unknowns**: Avoid asserting on specific differences in the visual diff. Visual tests are most effective for identifying unknown unknowns.
|
||||
|
||||
2. **Get the App into Interesting States**: Prioritize getting Open MCT into unusual layouts or behaviors before capturing a visual snapshot. For instance, you could open a dropdown menu.
|
||||
|
||||
3. **Expect the Unexpected**: Use functional expect statements only to verify assumptions about the state between steps. A great visual test doesn't fail during the test itself, but rather when changes are reviewed in Percy.io.
|
||||
|
||||
4. **Control Variability**: Account for variations inherent in working with time-based telemetry and clocks.
|
||||
- Utilize `percyCSS` to ignore time-based elements. For more details, consult our [percyCSS file](./.percy.ci.yml).
|
||||
- Use Open MCT's fixed-time mode unless explicitly testing realtime clock
|
||||
- Employ the `createExampleTelemetryObject` appAction to source telemetry and specify a `name` to avoid autogenerated names.
|
||||
|
||||
5. **Hide the Tree and Inspector**: Generally, your test will not require comparisons involving the tree and inspector. These aspects are covered in component-specific tests (explained below). To exclude them from the comparison by default, navigate to the root of the main view with the tree and inspector hidden:
|
||||
- `await page.goto('./#/browse/mine?hideTree=true&hideInspector=true')`
|
||||
|
||||
6. **Component-Specific Tests**: If you wish to focus on a particular component, use the `/visual/component/` folder and limit the scope of the comparison to that component. For instance:
|
||||
- Generally speaking, you should avoid being "specific" in what you hope to find in the diff. Visual tests are best suited for finding unknown unknowns.
|
||||
- These should only use functional expect statements to verify assumptions about the state
|
||||
in a test and not for functional verification of correctness. Visual tests are not supposed
|
||||
to "fail" on assertions. Instead, they should be used to detect changes between builds or branches.
|
||||
- A great visual test controls for the variation inherent to working with time-based telemetry and clocks. We do our best to remove this variation by using `percyCSS` to ignore all possible time-based components. For more, please see our [percyCSS file](./.percy.ci.yml).
|
||||
- Additionally, you should try the following:
|
||||
- Use fixed-time mode of Open MCT
|
||||
- Use the `createExampleTelemetryObject` appAction to source telemetry
|
||||
- When using the `createDomainObjectWithDefaults` appAction, make sure to specify a `name` which is explicit to avoid the autogenerated name
|
||||
- Very likely, your test will not need to compare changes on the tree. Keep it out of the comparison with the following
|
||||
- `await page.goto('./#/browse/mine')` will go to the root of the main view with the tree collapsed.
|
||||
- If you only want to compare changes on a specific component, use the /visual/component/ folder and limit the scope of the comparison to the object like so:
|
||||
- ```
|
||||
await percySnapshot(page, `Tree Pane w/ single level expanded (theme: ${theme})`, {
|
||||
scope: treePane
|
||||
});
|
||||
```js
|
||||
await percySnapshot(page, `Tree Pane w/ single level expanded (theme: ${theme})`, {
|
||||
scope: treePane
|
||||
});
|
||||
```
|
||||
- Note: The `scope` variable can be any valid CSS selector.
|
||||
|
||||
7. **Write many `percySnapshot` commands in a single test**: In line with our approach to longer functional tests, we recommend that many test percySnapshots are taken in a single test. For instance:
|
||||
```js
|
||||
//<Some interesting state>
|
||||
await percySnapshot(page, `Before object expanded (theme: ${theme})`);
|
||||
//<Click on object>
|
||||
await percySnapshot(page, `object expanded (theme: ${theme})`);
|
||||
//Select from object
|
||||
await percySnapshot(page, `object selected (theme: ${theme})`)
|
||||
```
|
||||
- The `scope` variable can be any valid css selector
|
||||
|
||||
#### How to write a great network test
|
||||
|
||||
@@ -438,35 +377,12 @@ For now, our best practices exist as self-tested, living documentation in our [e
|
||||
|
||||
For best practices with regards to mocking network responses, see our [couchdb.e2e.spec.js](./tests/functional/couchdb.e2e.spec.js) file.
|
||||
|
||||
### Tips & Tricks
|
||||
### Tips & Tricks (TODO)
|
||||
|
||||
The following contains a list of tips and tricks which don't exactly fit into a FAQ or Best Practices doc.
|
||||
|
||||
- (Advanced) Overriding the Browser's Clock
|
||||
It is possible to override the browser's clock in order to control time-based elements. Since this can cause unwanted behavior (i.e. Tree not rendering), only use this sparingly. To do this, use the `overrideClock` fixture as such:
|
||||
|
||||
```js
|
||||
const { test, expect } = require('../../pluginFixtures.js');
|
||||
|
||||
test.describe('foo test suite', () => {
|
||||
|
||||
// All subsequent tests in this suite will override the clock
|
||||
test.use({
|
||||
clockOptions: {
|
||||
now: 1732413600000, // A timestamp given as milliseconds since the epoch
|
||||
shouldAdvanceTime: true // Should the clock tick?
|
||||
}
|
||||
});
|
||||
|
||||
test('bar test', async ({ page }) => {
|
||||
// ...
|
||||
});
|
||||
});
|
||||
```
|
||||
More info and options for `overrideClock` can be found in [baseFixtures.js](baseFixtures.js)
|
||||
|
||||
- Working with multiple pages
|
||||
There are instances where multiple browser pages will needed to verify multi-page or multi-tab application behavior. Make sure to use the `@2p` annotation as well as name each page appropriately: i.e. `page1` and `page2` or `tab1` and `tab2` depending on the intended use case. Generally pages should be used unless testing `sharedWorker` code, specifically.
|
||||
There are instances where multiple browser pages will need to be opened to verify multi-page or multi-tab application behavior.
|
||||
|
||||
### Reporting
|
||||
|
||||
@@ -490,7 +406,15 @@ Our e2e code coverage is captured and combined with our unit test coverage. For
|
||||
|
||||
#### Generating e2e code coverage
|
||||
|
||||
Please read more about our code coverage [here](../TESTING.md#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.
|
||||
|
||||
## Other
|
||||
|
||||
@@ -540,10 +464,10 @@ A single e2e test in Open MCT is extended to run:
|
||||
- How is Open MCT extending default Playwright functionality?
|
||||
- What about Component Testing?
|
||||
|
||||
### e2e Troubleshooting
|
||||
|
||||
Please follow the general guide troubleshooting in [the general troubleshooting doc](../TESTING.md#troubleshooting-ci)
|
||||
### Troubleshooting
|
||||
|
||||
- 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```
|
||||
|
||||
@@ -78,10 +78,10 @@ async function createDomainObjectWithDefaults(
|
||||
|
||||
// Navigate to the parent object. This is necessary to create the object
|
||||
// in the correct location, such as a folder, layout, or plot.
|
||||
await page.goto(`${parentUrl}`);
|
||||
await page.goto(`${parentUrl}?hideTree=true`);
|
||||
|
||||
//Click the Create button
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
// 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(),
|
||||
await page.getByRole('button', { name: 'Save' }).click(),
|
||||
page.click('[aria-label="Save"]'),
|
||||
// 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.getByRole('button', { name: 'Save' }).click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.locator('li[title="Save and Finish Editing"]').click();
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -179,10 +179,10 @@ async function createPlanFromJSON(page, { name, json, parent = 'mine' }) {
|
||||
|
||||
// Navigate to the parent object. This is necessary to create the object
|
||||
// in the correct location, such as a folder, layout, or plot.
|
||||
await page.goto(`${parentUrl}`);
|
||||
await page.goto(`${parentUrl}?hideTree=true`);
|
||||
|
||||
// Click the Create button
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
// Click 'Plan' menu option
|
||||
await page.click(`li:text("Plan")`);
|
||||
@@ -228,15 +228,17 @@ async function createPlanFromJSON(page, { name, json, parent = 'mine' }) {
|
||||
*/
|
||||
async function createExampleTelemetryObject(page, parent = 'mine') {
|
||||
const parentUrl = await getHashUrlToDomainObject(page, parent);
|
||||
// TODO: Make this field even more accessible
|
||||
const name = 'VIPER Rover Heading';
|
||||
const nameInputLocator = page.getByRole('dialog').locator('input[type="text"]');
|
||||
|
||||
await page.goto(`${parentUrl}`);
|
||||
await page.goto(`${parentUrl}?hideTree=true`);
|
||||
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
|
||||
await page.locator('li:has-text("Sine Wave Generator")').click();
|
||||
|
||||
const name = 'VIPER Rover Heading';
|
||||
await page.getByRole('dialog').locator('input[type="text"]').fill(name);
|
||||
await nameInputLocator.fill(name);
|
||||
|
||||
// Fill out the fields with default values
|
||||
await page.getByRole('spinbutton', { name: 'Period' }).fill('10');
|
||||
@@ -384,13 +386,11 @@ async function setTimeConductorMode(page, isFixedTimespan = true) {
|
||||
// Click 'mode' button
|
||||
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
|
||||
await page.getByRole('button', { name: 'Time Conductor Mode Menu' }).click();
|
||||
// Switch time conductor mode. Note, need to wait here for URL to update as the router is debounced.
|
||||
// Switch time conductor mode
|
||||
if (isFixedTimespan) {
|
||||
await page.getByRole('menuitem', { name: /Fixed Timespan/ }).click();
|
||||
await page.waitForURL(/tc\.mode=fixed/);
|
||||
} else {
|
||||
await page.getByRole('menuitem', { name: /Real-Time/ }).click();
|
||||
await page.waitForURL(/tc\.mode=local/);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -520,7 +520,6 @@ async function setIndependentTimeConductorBounds(page, startDate, endDate) {
|
||||
|
||||
/**
|
||||
* Set the bounds of the visible conductor in fixed time mode
|
||||
* @private
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} startDate
|
||||
* @param {string} endDate
|
||||
@@ -545,6 +544,18 @@ async function setTimeBounds(page, startDate, endDate) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects an inspector tab based on the provided tab name
|
||||
*
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {String} name the name of the tab
|
||||
*/
|
||||
async function selectInspectorTab(page, name) {
|
||||
const inspectorTabs = page.getByRole('tablist');
|
||||
const inspectorTab = inspectorTabs.getByTitle(name);
|
||||
await inspectorTab.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits and asserts that all plot series data on the page
|
||||
* is loaded and drawn.
|
||||
@@ -663,6 +674,7 @@ module.exports = {
|
||||
setEndOffset,
|
||||
setTimeConductorBounds,
|
||||
setIndependentTimeConductorBounds,
|
||||
selectInspectorTab,
|
||||
waitForPlotsToRender,
|
||||
renameObjectFromContextMenu
|
||||
};
|
||||
|
||||
@@ -81,7 +81,7 @@ exports.test = base.test.extend({
|
||||
* ```js
|
||||
* test.use({
|
||||
* clockOptions: {
|
||||
* now: MISSION_TIME,
|
||||
* now: 0,
|
||||
* shouldAdvanceTime: true
|
||||
* ```
|
||||
* If clockOptions are provided, will override the default clock with fake timers provided by SinonJS.
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable prettier/prettier */
|
||||
/**
|
||||
* Constants which may be used across all e2e tests.
|
||||
*/
|
||||
@@ -8,11 +7,3 @@
|
||||
* - Used for overriding the browser clock in tests.
|
||||
*/
|
||||
export const MISSION_TIME = 1732413600000; // Saturday, November 23, 2024 6:00:00 PM GMT-08:00 (Thanksgiving Dinner Time)
|
||||
|
||||
/**
|
||||
* URL Constants
|
||||
* - This is the URL that the browser will be directed to when running visual tests. This URL
|
||||
* - hides the tree and inspector to prevent visual noise
|
||||
* - sets the time bounds to a fixed range
|
||||
*/
|
||||
export const VISUAL_URL = './#/browse/mine?tc.mode=fixed&tc.startBound=1693592063607&tc.endBound=1693593893607&tc.timeSystem=utc&view=grid&hideInspector=true&hideTree=true';
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
const { createDomainObjectWithDefaults } = require('../appActions');
|
||||
const { selectInspectorTab, createDomainObjectWithDefaults } = require('../appActions');
|
||||
|
||||
const NOTEBOOK_DROP_AREA = '.c-notebook__drag-area';
|
||||
const CUSTOM_NAME = 'CUSTOM_NAME';
|
||||
@@ -34,7 +34,7 @@ async function enterTextEntry(page, text) {
|
||||
await page.locator(NOTEBOOK_DROP_AREA).click();
|
||||
|
||||
// enter text
|
||||
await page.getByLabel('Notebook Entry Input').last().fill(text);
|
||||
await page.locator('[aria-label="Notebook Entry"].is-selected div.c-ne__text').fill(text);
|
||||
await commitEntry(page);
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ async function createNotebookAndEntry(page, iterations = 1) {
|
||||
*/
|
||||
async function createNotebookEntryAndTags(page, iterations = 1) {
|
||||
const notebook = await createNotebookAndEntry(page, iterations);
|
||||
await page.getByRole('tab', { name: 'Annotations' }).click();
|
||||
await selectInspectorTab(page, 'Annotations');
|
||||
|
||||
for (let iteration = 0; iteration < iterations; iteration++) {
|
||||
// Hover and click "Add Tag" button
|
||||
|
||||
@@ -99,14 +99,3 @@ export async function setBoundsToSpanAllActivities(page, planJson, planObjectUrl
|
||||
`${planObjectUrl}?tc.mode=fixed&tc.startBound=${start}&tc.endBound=${end}&tc.timeSystem=utc&view=plan.view`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses the Open MCT API to set the status of a plan to 'draft'.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {import('../../appActions').CreatedObjectInfo} plan
|
||||
*/
|
||||
export async function setDraftStatusForPlan(page, plan) {
|
||||
await page.evaluate(async (planObject) => {
|
||||
await window.openmct.status.set(planObject.uuid, 'draft');
|
||||
}, plan);
|
||||
}
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
/* eslint-disable no-undef */
|
||||
// playwright.config.js
|
||||
// @ts-check
|
||||
|
||||
/** @type {import('@playwright/test').PlaywrightTestConfig} */
|
||||
const config = {
|
||||
retries: 0, //Only for debugging purposes for trace: 'on-first-retry'
|
||||
testDir: 'tests/performance/',
|
||||
testIgnore: '*.contract.perf.spec.js', //Run everything except contract tests which require marks in dev mode
|
||||
timeout: 60 * 1000,
|
||||
workers: 1, //Only run in serial with 1 worker
|
||||
webServer: {
|
||||
command: 'npm run start:prod', //Production mode
|
||||
url: 'http://localhost:8080/#',
|
||||
timeout: 200 * 1000,
|
||||
reuseExistingServer: false //Must be run with this option to prevent dev mode
|
||||
},
|
||||
use: {
|
||||
baseURL: 'http://localhost:8080/',
|
||||
headless: true,
|
||||
ignoreHTTPSErrors: false, //HTTP performance varies!
|
||||
screenshot: 'off',
|
||||
trace: 'on-first-retry',
|
||||
video: 'off'
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chrome-memory',
|
||||
testMatch: '*.memory.perf.spec.js', //Only run memory tests
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
launchOptions: {
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--disable-notifications',
|
||||
'--use-fake-ui-for-media-stream',
|
||||
'--use-fake-device-for-media-stream',
|
||||
'--js-flags=--no-move-object-start --expose-gc',
|
||||
'--enable-precise-memory-info',
|
||||
'--display=:100'
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'chrome',
|
||||
testIgnore: '*.memory.perf.spec.js', //Do not run memory tests without proper flags
|
||||
use: {
|
||||
browserName: 'chromium'
|
||||
}
|
||||
}
|
||||
],
|
||||
reporter: [
|
||||
['list'],
|
||||
['junit', { outputFile: '../test-results/results.xml' }],
|
||||
['json', { outputFile: '../test-results/results.json' }]
|
||||
]
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
@@ -2,32 +2,35 @@
|
||||
// playwright.config.js
|
||||
// @ts-check
|
||||
|
||||
const CI = process.env.CI === 'true';
|
||||
|
||||
/** @type {import('@playwright/test').PlaywrightTestConfig} */
|
||||
const config = {
|
||||
retries: 1, //Only for debugging purposes for trace: 'on-first-retry'
|
||||
testDir: 'tests/performance/',
|
||||
testMatch: '*.contract.perf.spec.js', //Run everything except contract tests which require marks in dev mode
|
||||
timeout: 60 * 1000,
|
||||
workers: 1, //Only run in serial with 1 worker
|
||||
webServer: {
|
||||
command: 'npm run start', //need development mode for performance.marks and others
|
||||
command: 'npm run start', //coverage not generated
|
||||
url: 'http://localhost:8080/#',
|
||||
timeout: 200 * 1000,
|
||||
reuseExistingServer: false
|
||||
reuseExistingServer: !CI
|
||||
},
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
baseURL: 'http://localhost:8080/',
|
||||
headless: true,
|
||||
ignoreHTTPSErrors: false, //HTTP performance varies!
|
||||
headless: CI, //Only if running locally
|
||||
ignoreHTTPSErrors: true,
|
||||
screenshot: 'off',
|
||||
trace: 'on-first-retry',
|
||||
video: 'off'
|
||||
video: 'off',
|
||||
launchOptions: {
|
||||
args: ['--js-flags=--expose-gc']
|
||||
}
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chrome',
|
||||
testIgnore: '*.memory.perf.spec.js', //Do not run memory tests without proper flags. Shouldn't get here
|
||||
use: {
|
||||
browserName: 'chromium'
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -6,17 +6,21 @@
|
||||
"localStorage": [
|
||||
{
|
||||
"name": "mct",
|
||||
"value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"20e7d5fe-9cf8-4099-8957-9453a8954c67\",\"namespace\":\"\"},{\"key\":\"2db521a9-996d-4d04-a171-93f4c5c220af\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413602540,\"created\":1732413600760,\"persisted\":1732413602540},\"20e7d5fe-9cf8-4099-8957-9453a8954c67\":{\"identifier\":{\"key\":\"20e7d5fe-9cf8-4099-8957-9453a8954c67\",\"namespace\":\"\"},\"name\":\"Overlay Plot with Telemetry Object\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"2db521a9-996d-4d04-a171-93f4c5c220af\",\"namespace\":\"\"}],\"configuration\":{\"series\":[]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate Overlay Plot with Telemetry Object\\nchrome\",\"modified\":1732413603960,\"location\":\"mine\",\"created\":1732413601820,\"persisted\":1732413603960},\"2db521a9-996d-4d04-a171-93f4c5c220af\":{\"name\":\"VIPER Rover Heading\",\"type\":\"generator\",\"identifier\":{\"key\":\"2db521a9-996d-4d04-a171-93f4c5c220af\",\"namespace\":\"\"},\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":0,\"infinityValues\":false,\"staleness\":false},\"modified\":1732413602540,\"location\":\"mine\",\"created\":1732413602540,\"persisted\":1732413602540}}"
|
||||
},
|
||||
{
|
||||
"name": "mct-recent-objects",
|
||||
"value": "[{\"objectPath\":[{\"identifier\":{\"key\":\"2db521a9-996d-4d04-a171-93f4c5c220af\",\"namespace\":\"\"},\"name\":\"VIPER Rover Heading\",\"type\":\"generator\",\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":0,\"infinityValues\":false,\"staleness\":false},\"modified\":1732413602540,\"location\":\"mine\",\"created\":1732413602540,\"persisted\":1732413602540},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"20e7d5fe-9cf8-4099-8957-9453a8954c67\",\"namespace\":\"\"},{\"key\":\"2db521a9-996d-4d04-a171-93f4c5c220af\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413602540,\"created\":1732413600760,\"persisted\":1732413602540},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/2db521a9-996d-4d04-a171-93f4c5c220af\",\"domainObject\":{\"identifier\":{\"key\":\"2db521a9-996d-4d04-a171-93f4c5c220af\",\"namespace\":\"\"},\"name\":\"VIPER Rover Heading\",\"type\":\"generator\",\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":0,\"infinityValues\":false,\"staleness\":false},\"modified\":1732413602540,\"location\":\"mine\",\"created\":1732413602540,\"persisted\":1732413602540}},{\"objectPath\":[{\"identifier\":{\"key\":\"20e7d5fe-9cf8-4099-8957-9453a8954c67\",\"namespace\":\"\"},\"name\":\"Overlay Plot with Telemetry Object\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"2db521a9-996d-4d04-a171-93f4c5c220af\",\"namespace\":\"\"}],\"configuration\":{\"series\":[{\"identifier\":{\"key\":\"2db521a9-996d-4d04-a171-93f4c5c220af\",\"namespace\":\"\"}}]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate Overlay Plot with Telemetry Object\\nchrome\",\"modified\":1732413603960,\"location\":\"mine\",\"created\":1732413601820,\"persisted\":1732413603960},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"20e7d5fe-9cf8-4099-8957-9453a8954c67\",\"namespace\":\"\"},{\"key\":\"2db521a9-996d-4d04-a171-93f4c5c220af\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413602540,\"created\":1732413600760,\"persisted\":1732413602540},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/20e7d5fe-9cf8-4099-8957-9453a8954c67\",\"domainObject\":{\"identifier\":{\"key\":\"20e7d5fe-9cf8-4099-8957-9453a8954c67\",\"namespace\":\"\"},\"name\":\"Overlay Plot with Telemetry Object\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"2db521a9-996d-4d04-a171-93f4c5c220af\",\"namespace\":\"\"}],\"configuration\":{\"series\":[{\"identifier\":{\"key\":\"2db521a9-996d-4d04-a171-93f4c5c220af\",\"namespace\":\"\"}}]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate Overlay Plot with Telemetry Object\\nchrome\",\"modified\":1732413603960,\"location\":\"mine\",\"created\":1732413601820,\"persisted\":1732413603960}},{\"objectPath\":[{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"20e7d5fe-9cf8-4099-8957-9453a8954c67\",\"namespace\":\"\"},{\"key\":\"2db521a9-996d-4d04-a171-93f4c5c220af\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413602540,\"created\":1732413600760,\"persisted\":1732413602540},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine\",\"domainObject\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"20e7d5fe-9cf8-4099-8957-9453a8954c67\",\"namespace\":\"\"},{\"key\":\"2db521a9-996d-4d04-a171-93f4c5c220af\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413602540,\"created\":1732413600760,\"persisted\":1732413602540}}]"
|
||||
"value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"8c863964-4640-4db1-8a98-0e546c3c271d\",\"namespace\":\"\"},{\"key\":\"f5c7e86c-a5b4-4c6c-9038-9cf03cd9a99e\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1678741852556,\"created\":1678741830859,\"persisted\":1678741852557},\"8c863964-4640-4db1-8a98-0e546c3c271d\":{\"identifier\":{\"key\":\"8c863964-4640-4db1-8a98-0e546c3c271d\",\"namespace\":\"\"},\"name\":\"Overlay Plot with Telemetry Object\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"f5c7e86c-a5b4-4c6c-9038-9cf03cd9a99e\",\"namespace\":\"\"}],\"configuration\":{\"series\":[{\"identifier\":{\"key\":\"f5c7e86c-a5b4-4c6c-9038-9cf03cd9a99e\",\"namespace\":\"\"}}]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate Overlay Plot with Telemetry Object\\nchrome\",\"modified\":1678741862011,\"location\":\"mine\",\"created\":1678741839461,\"persisted\":1678741862011},\"f5c7e86c-a5b4-4c6c-9038-9cf03cd9a99e\":{\"name\":\"VIPER Rover Heading\",\"type\":\"generator\",\"identifier\":{\"key\":\"f5c7e86c-a5b4-4c6c-9038-9cf03cd9a99e\",\"namespace\":\"\"},\"telemetry\":{\"period\":\"1\",\"amplitude\":\"1\",\"offset\":\"0\",\"dataRateInHz\":\"1\",\"phase\":\"0\",\"randomness\":\"0\",\"loadDelay\":\"0\",\"infinityValues\":false,\"staleness\":false},\"modified\":1678741852553,\"location\":\"mine\",\"created\":1678741852553,\"persisted\":1678741852553}}"
|
||||
},
|
||||
{
|
||||
"name": "mct-tree-expanded",
|
||||
"value": "[]"
|
||||
},
|
||||
{
|
||||
"name": "tcHistory",
|
||||
"value": "{\"utc\":[{\"start\":1678740030748,\"end\":1678741830748}]}"
|
||||
},
|
||||
{
|
||||
"name": "mct-recent-objects",
|
||||
"value": "[{\"objectPath\":[{\"identifier\":{\"key\":\"8c863964-4640-4db1-8a98-0e546c3c271d\",\"namespace\":\"\"},\"name\":\"Overlay Plot with Telemetry Object\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"f5c7e86c-a5b4-4c6c-9038-9cf03cd9a99e\",\"namespace\":\"\"}],\"configuration\":{\"series\":[]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate Overlay Plot with Telemetry Object\\nchrome\",\"modified\":1678741860389,\"location\":\"mine\",\"created\":1678741839461,\"persisted\":1678741860389},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"8c863964-4640-4db1-8a98-0e546c3c271d\",\"namespace\":\"\"},{\"key\":\"f5c7e86c-a5b4-4c6c-9038-9cf03cd9a99e\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1678741852556,\"created\":1678741830859,\"persisted\":1678741852557},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/8c863964-4640-4db1-8a98-0e546c3c271d\",\"domainObject\":{\"identifier\":{\"key\":\"8c863964-4640-4db1-8a98-0e546c3c271d\",\"namespace\":\"\"},\"name\":\"Overlay Plot with Telemetry Object\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"f5c7e86c-a5b4-4c6c-9038-9cf03cd9a99e\",\"namespace\":\"\"}],\"configuration\":{\"series\":[]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate Overlay Plot with Telemetry Object\\nchrome\",\"modified\":1678741860389,\"location\":\"mine\",\"created\":1678741839461,\"persisted\":1678741860389}},{\"objectPath\":[{\"identifier\":{\"key\":\"f5c7e86c-a5b4-4c6c-9038-9cf03cd9a99e\",\"namespace\":\"\"},\"name\":\"VIPER Rover Heading\",\"type\":\"generator\",\"telemetry\":{\"period\":\"1\",\"amplitude\":\"1\",\"offset\":\"0\",\"dataRateInHz\":\"1\",\"phase\":\"0\",\"randomness\":\"0\",\"loadDelay\":\"0\",\"infinityValues\":false,\"staleness\":false},\"modified\":1678741852553,\"location\":\"mine\",\"created\":1678741852553,\"persisted\":1678741852553},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"8c863964-4640-4db1-8a98-0e546c3c271d\",\"namespace\":\"\"},{\"key\":\"f5c7e86c-a5b4-4c6c-9038-9cf03cd9a99e\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1678741852556,\"created\":1678741830859,\"persisted\":1678741852557},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/f5c7e86c-a5b4-4c6c-9038-9cf03cd9a99e\",\"domainObject\":{\"identifier\":{\"key\":\"f5c7e86c-a5b4-4c6c-9038-9cf03cd9a99e\",\"namespace\":\"\"},\"name\":\"VIPER Rover Heading\",\"type\":\"generator\",\"telemetry\":{\"period\":\"1\",\"amplitude\":\"1\",\"offset\":\"0\",\"dataRateInHz\":\"1\",\"phase\":\"0\",\"randomness\":\"0\",\"loadDelay\":\"0\",\"infinityValues\":false,\"staleness\":false},\"modified\":1678741852553,\"location\":\"mine\",\"created\":1678741852553,\"persisted\":1678741852553}},{\"objectPath\":[{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"8c863964-4640-4db1-8a98-0e546c3c271d\",\"namespace\":\"\"},{\"key\":\"f5c7e86c-a5b4-4c6c-9038-9cf03cd9a99e\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1678741852556,\"created\":1678741830859,\"persisted\":1678741852557},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine\",\"domainObject\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"8c863964-4640-4db1-8a98-0e546c3c271d\",\"namespace\":\"\"},{\"key\":\"f5c7e86c-a5b4-4c6c-9038-9cf03cd9a99e\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1678741852556,\"created\":1678741830859,\"persisted\":1678741852557}}]"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,13 +6,21 @@
|
||||
"localStorage": [
|
||||
{
|
||||
"name": "mct",
|
||||
"value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"98161570-a735-4a50-9c75-11b346ad3789\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413601340,\"created\":1732413600580,\"persisted\":1732413601340},\"98161570-a735-4a50-9c75-11b346ad3789\":{\"identifier\":{\"key\":\"98161570-a735-4a50-9c75-11b346ad3789\",\"namespace\":\"\"},\"name\":\"Overlay Plot with 5s Delay\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"477e60bb-4cba-4603-b4c9-2281ccf7e054\",\"namespace\":\"\"}],\"configuration\":{\"series\":[{\"identifier\":{\"key\":\"477e60bb-4cba-4603-b4c9-2281ccf7e054\",\"namespace\":\"\"}}]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate Overlay Plot with 5s Delay\\nchrome\",\"modified\":1732413602660,\"location\":\"mine\",\"created\":1732413601340,\"persisted\":1732413602660},\"477e60bb-4cba-4603-b4c9-2281ccf7e054\":{\"identifier\":{\"key\":\"477e60bb-4cba-4603-b4c9-2281ccf7e054\",\"namespace\":\"\"},\"name\":\"VIPER Rover Heading\",\"type\":\"generator\",\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":5000,\"infinityValues\":false,\"staleness\":false},\"modified\":1732413602520,\"location\":\"98161570-a735-4a50-9c75-11b346ad3789\",\"created\":1732413602040,\"persisted\":1732413602520}}"
|
||||
"value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"db9fb115-7a72-4c45-81a4-1f6021156b4e\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1678741890986,\"created\":1678741885545,\"persisted\":1678741890987},\"db9fb115-7a72-4c45-81a4-1f6021156b4e\":{\"identifier\":{\"key\":\"db9fb115-7a72-4c45-81a4-1f6021156b4e\",\"namespace\":\"\"},\"name\":\"Overlay Plot with Telemetry Object\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"4c0b2c16-5f28-4906-abd3-befd16e5a77e\",\"namespace\":\"\"}],\"configuration\":{\"series\":[{\"identifier\":{\"key\":\"4c0b2c16-5f28-4906-abd3-befd16e5a77e\",\"namespace\":\"\"}}]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate Overlay Plot with 5s Delay\\nchrome\",\"modified\":1678741904378,\"location\":\"mine\",\"created\":1678741890983,\"persisted\":1678741904385},\"4c0b2c16-5f28-4906-abd3-befd16e5a77e\":{\"name\":\"Unnamed Sine Wave Generator\",\"type\":\"generator\",\"identifier\":{\"key\":\"4c0b2c16-5f28-4906-abd3-befd16e5a77e\",\"namespace\":\"\"},\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":\"5000\",\"infinityValues\":false,\"staleness\":false},\"modified\":1678741896800,\"location\":\"db9fb115-7a72-4c45-81a4-1f6021156b4e\",\"created\":1678741896800,\"persisted\":1678741896800}}"
|
||||
},
|
||||
{
|
||||
"name": "mct-tree-expanded",
|
||||
"value": "[]"
|
||||
},
|
||||
{
|
||||
"name": "tcHistory",
|
||||
"value": "{\"utc\":[{\"start\":1678740085436,\"end\":1678741885436}]}"
|
||||
},
|
||||
{
|
||||
"name": "mct-recent-objects",
|
||||
"value": "[{\"objectPath\":[{\"identifier\":{\"key\":\"db9fb115-7a72-4c45-81a4-1f6021156b4e\",\"namespace\":\"\"},\"name\":\"Overlay Plot with Telemetry Object\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"4c0b2c16-5f28-4906-abd3-befd16e5a77e\",\"namespace\":\"\"}],\"configuration\":{\"series\":[]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate Overlay Plot with 5s Delay\\nchrome\",\"modified\":1678741896803,\"location\":\"mine\",\"created\":1678741890983,\"persisted\":1678741896803},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"db9fb115-7a72-4c45-81a4-1f6021156b4e\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1678741890986,\"created\":1678741885545,\"persisted\":1678741890987},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/db9fb115-7a72-4c45-81a4-1f6021156b4e\",\"domainObject\":{\"identifier\":{\"key\":\"db9fb115-7a72-4c45-81a4-1f6021156b4e\",\"namespace\":\"\"},\"name\":\"Overlay Plot with Telemetry Object\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"4c0b2c16-5f28-4906-abd3-befd16e5a77e\",\"namespace\":\"\"}],\"configuration\":{\"series\":[]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate Overlay Plot with 5s Delay\\nchrome\",\"modified\":1678741896803,\"location\":\"mine\",\"created\":1678741890983,\"persisted\":1678741896803}},{\"objectPath\":[{\"identifier\":{\"key\":\"4c0b2c16-5f28-4906-abd3-befd16e5a77e\",\"namespace\":\"\"},\"name\":\"Unnamed Sine Wave Generator\",\"type\":\"generator\",\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":\"5000\",\"infinityValues\":false,\"staleness\":false},\"modified\":1678741896800,\"location\":\"db9fb115-7a72-4c45-81a4-1f6021156b4e\",\"created\":1678741896800,\"persisted\":1678741896800},{\"identifier\":{\"key\":\"db9fb115-7a72-4c45-81a4-1f6021156b4e\",\"namespace\":\"\"},\"name\":\"Overlay Plot with Telemetry Object\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"4c0b2c16-5f28-4906-abd3-befd16e5a77e\",\"namespace\":\"\"}],\"configuration\":{\"series\":[]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate Overlay Plot with 5s Delay\\nchrome\",\"modified\":1678741896803,\"location\":\"mine\",\"created\":1678741890983,\"persisted\":1678741896803},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"db9fb115-7a72-4c45-81a4-1f6021156b4e\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1678741890986,\"created\":1678741885545,\"persisted\":1678741890987},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/db9fb115-7a72-4c45-81a4-1f6021156b4e/4c0b2c16-5f28-4906-abd3-befd16e5a77e\",\"domainObject\":{\"identifier\":{\"key\":\"4c0b2c16-5f28-4906-abd3-befd16e5a77e\",\"namespace\":\"\"},\"name\":\"Unnamed Sine Wave Generator\",\"type\":\"generator\",\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":\"5000\",\"infinityValues\":false,\"staleness\":false},\"modified\":1678741896800,\"location\":\"db9fb115-7a72-4c45-81a4-1f6021156b4e\",\"created\":1678741896800,\"persisted\":1678741896800}},{\"objectPath\":[{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"db9fb115-7a72-4c45-81a4-1f6021156b4e\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1678741890986,\"created\":1678741885545,\"persisted\":1678741890987},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine\",\"domainObject\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"db9fb115-7a72-4c45-81a4-1f6021156b4e\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1678741890986,\"created\":1678741885545,\"persisted\":1678741890987}}]"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -35,7 +35,8 @@
|
||||
const { test, expect } = require('../../pluginFixtures.js');
|
||||
const {
|
||||
createDomainObjectWithDefaults,
|
||||
createExampleTelemetryObject
|
||||
createExampleTelemetryObject,
|
||||
selectInspectorTab
|
||||
} = require('../../appActions.js');
|
||||
const { MISSION_TIME } = require('../../constants.js');
|
||||
const path = require('path');
|
||||
@@ -55,67 +56,6 @@ 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
|
||||
@@ -151,7 +91,7 @@ test.describe('Generate Visual Test Data @localStorage @generatedata', () => {
|
||||
|
||||
// TODO: Flesh Out Assertions against created Objects
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText(overlayPlotName);
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
await selectInspectorTab(page, 'Config');
|
||||
await page
|
||||
.getByRole('list', { name: 'Plot Series Properties' })
|
||||
.locator('span')
|
||||
@@ -182,7 +122,7 @@ test.describe('Generate Visual Test Data @localStorage @generatedata', () => {
|
||||
).toBeVisible();
|
||||
|
||||
await page.goto(exampleTelemetry.url);
|
||||
await page.getByRole('tab', { name: 'Properties' }).click();
|
||||
await selectInspectorTab(page, 'Properties');
|
||||
|
||||
// TODO: assert Example Telemetry property values
|
||||
// await page.goto(exampleTelemetry.url);
|
||||
@@ -241,7 +181,7 @@ test.describe('Validate Overlay Plot with Telemetry Object @localStorage @genera
|
||||
await page.locator('a').filter({ hasText: overlayPlotName }).click();
|
||||
// TODO: Flesh Out Assertions against created Objects
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText(overlayPlotName);
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
await selectInspectorTab(page, 'Config');
|
||||
await page
|
||||
.getByRole('list', { name: 'Plot Series Properties' })
|
||||
.locator('span')
|
||||
@@ -287,7 +227,7 @@ test.describe('Validate Overlay Plot with 5s Delay Telemetry Object @localStorag
|
||||
await page.locator('a').filter({ hasText: plotName }).click();
|
||||
// TODO: Flesh Out Assertions against created Objects
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText(plotName);
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
await selectInspectorTab(page, 'Config');
|
||||
await page
|
||||
.getByRole('list', { name: 'Plot Series Properties' })
|
||||
.locator('span')
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2023, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -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.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
// Create New Folder Basic Domain Object
|
||||
let folder = 'Test Folder';
|
||||
|
||||
@@ -109,7 +109,8 @@ test.describe('Notification Overlay', () => {
|
||||
// Click on the "Save" button
|
||||
await page.click('button[title="Save"]');
|
||||
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
// Click on the "Save and Finish Editing" option
|
||||
await page.click('li[title="Save and Finish Editing"]');
|
||||
|
||||
// Verify that Notification List is NOT open
|
||||
expect(await page.locator('div[role="dialog"]').isVisible()).toBe(false);
|
||||
|
||||
@@ -20,7 +20,11 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
const { test, expect } = require('../../../pluginFixtures');
|
||||
const { createPlanFromJSON, createDomainObjectWithDefaults } = require('../../../appActions');
|
||||
const {
|
||||
createPlanFromJSON,
|
||||
createDomainObjectWithDefaults,
|
||||
selectInspectorTab
|
||||
} = require('../../../appActions');
|
||||
const testPlan1 = require('../../../test-data/examplePlans/ExamplePlan_Small1.json');
|
||||
const testPlan2 = require('../../../test-data/examplePlans/ExamplePlan_Small2.json');
|
||||
const {
|
||||
@@ -76,7 +80,7 @@ test.describe('Gantt Chart', () => {
|
||||
.locator('g')
|
||||
.filter({ hasText: new RegExp(activity.name) })
|
||||
.click();
|
||||
await page.getByRole('tab', { name: 'Activity' }).click();
|
||||
await selectInspectorTab(page, 'Activity');
|
||||
|
||||
const startDateTime = await page
|
||||
.locator(
|
||||
|
||||
@@ -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,7 +31,6 @@ const {
|
||||
createDomainObjectWithDefaults,
|
||||
createExampleTelemetryObject
|
||||
} = require('../../../../appActions');
|
||||
const path = require('path');
|
||||
|
||||
let conditionSetUrl;
|
||||
let getConditionSetIdentifierFromUrl;
|
||||
@@ -49,9 +48,7 @@ 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: path.resolve(__dirname, '../../../../test-data/recycled_local_storage.json')
|
||||
});
|
||||
await context.storageState({ path: './e2e/test-data/recycled_local_storage.json' });
|
||||
|
||||
//Set object identifier from url
|
||||
conditionSetUrl = page.url();
|
||||
@@ -62,9 +59,7 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
|
||||
});
|
||||
|
||||
//Load localStorage for subsequent tests
|
||||
test.use({
|
||||
storageState: path.resolve(__dirname, '../../../../test-data/recycled_local_storage.json')
|
||||
});
|
||||
test.use({ storageState: './e2e/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 ({
|
||||
@@ -122,7 +117,7 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
|
||||
.nth(1)
|
||||
.click();
|
||||
// Click Save and Finish Editing Option
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
//Verify Main section reflects updated Name Property
|
||||
await expect
|
||||
|
||||
@@ -19,9 +19,8 @@
|
||||
* 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,
|
||||
@@ -30,88 +29,6 @@ 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;
|
||||
@@ -124,7 +41,6 @@ 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
|
||||
}) => {
|
||||
@@ -148,7 +64,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.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
await page.locator('text=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
|
||||
@@ -186,7 +102,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.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
// Subscribe to the Sine Wave Generator data
|
||||
const getTelemValuePromise = await subscribeToTelemetry(page, sineWaveObject.uuid);
|
||||
@@ -228,7 +144,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.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
expect.soft(await page.locator('.l-layout .l-layout__frame').count()).toEqual(1);
|
||||
|
||||
@@ -270,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.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
expect.soft(await page.locator('.l-layout .l-layout__frame').count()).toEqual(1);
|
||||
|
||||
@@ -326,7 +242,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.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
const startDate = '2021-12-30 01:01:00.000Z';
|
||||
const endDate = '2021-12-30 01:11:00.000Z';
|
||||
@@ -347,10 +263,7 @@ test.describe('Display Layout', () => {
|
||||
await setFixedTimeMode(page);
|
||||
// Create another Sine Wave Generator
|
||||
const anotherSineWaveObject = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
customParameters: {
|
||||
'[aria-label="Data Rate (hz)"]': '0.01'
|
||||
}
|
||||
type: 'Sine Wave Generator'
|
||||
});
|
||||
// Create a Display Layout
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
@@ -388,13 +301,12 @@ test.describe('Display Layout', () => {
|
||||
await page.getByText('Overlay Plot').click();
|
||||
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
// Time to inspect some network traffic
|
||||
let networkRequests = [];
|
||||
page.on('request', (request) => {
|
||||
const searchRequest =
|
||||
request.url().endsWith('_find') || request.url().includes('by_keystring');
|
||||
const searchRequest = request.url().endsWith('_find');
|
||||
const fetchRequest = request.resourceType() === 'fetch';
|
||||
if (searchRequest && fetchRequest) {
|
||||
networkRequests.push(request);
|
||||
@@ -410,7 +322,6 @@ test.describe('Display Layout', () => {
|
||||
expect(networkRequests.length).toBe(1);
|
||||
|
||||
await setRealTimeMode(page);
|
||||
|
||||
networkRequests = [];
|
||||
|
||||
await page.reload();
|
||||
@@ -423,59 +334,6 @@ 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
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const utils = require('../../../../helper/faultUtils');
|
||||
const { selectInspectorTab } = require('../../../../appActions');
|
||||
|
||||
test.describe('The Fault Management Plugin using example faults', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
@@ -40,7 +41,7 @@ test.describe('The Fault Management Plugin using example faults', () => {
|
||||
}) => {
|
||||
await utils.selectFaultItem(page, 1);
|
||||
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
await selectInspectorTab(page, 'Fault Management Configuration');
|
||||
const selectedFaultName = await page
|
||||
.locator('.c-fault-mgmt__list.is-selected .c-fault-mgmt__list-faultname')
|
||||
.textContent();
|
||||
@@ -65,7 +66,7 @@ test.describe('The Fault Management Plugin using example faults', () => {
|
||||
);
|
||||
expect.soft(await selectedRows.count()).toEqual(2);
|
||||
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
await selectInspectorTab(page, 'Fault Management Configuration');
|
||||
const firstSelectedFaultName = await selectedRows.nth(0).textContent();
|
||||
const secondSelectedFaultName = await selectedRows.nth(1).textContent();
|
||||
const firstNameInInspectorCount = await page
|
||||
|
||||
@@ -19,19 +19,12 @@
|
||||
* 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;
|
||||
@@ -88,7 +81,7 @@ test.describe('Flexible Layout', () => {
|
||||
await expect(dragWrapper).toHaveAttribute('draggable', 'true');
|
||||
// Save Flexible Layout
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
await page.locator('text=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');
|
||||
@@ -174,7 +167,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.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
expect.soft(await page.locator('.c-fl-container__frame').count()).toEqual(1);
|
||||
|
||||
@@ -205,7 +198,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.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
expect.soft(await page.locator('.c-fl-container__frame').count()).toEqual(1);
|
||||
|
||||
@@ -246,7 +239,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.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
// flip on independent time conductor
|
||||
await setIndependentTimeConductorBounds(
|
||||
@@ -264,53 +257,3 @@ 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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,10 +25,7 @@
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const {
|
||||
createDomainObjectWithDefaults,
|
||||
createExampleTelemetryObject
|
||||
} = require('../../../../appActions');
|
||||
const { createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||
const uuid = require('uuid').v4;
|
||||
|
||||
test.describe('Gauge', () => {
|
||||
@@ -56,7 +53,7 @@ test.describe('Gauge', () => {
|
||||
await editButtonLocator.click();
|
||||
await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeVisible();
|
||||
await saveButtonLocator.click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
await page.locator('li[title="Save and Finish Editing"]').click();
|
||||
|
||||
// Create another sine wave generator within the gauge
|
||||
const swg2 = await createDomainObjectWithDefaults(page, {
|
||||
@@ -136,50 +133,4 @@ 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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -57,10 +57,6 @@ 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
|
||||
@@ -202,26 +198,23 @@ test.describe('Example Imagery Object', () => {
|
||||
expect(afterDownPanBoundingBox.y).toBeLessThan(afterUpPanBoundingBox.y);
|
||||
});
|
||||
|
||||
test('Can use alt+shift+drag to create a tag and ensure toolbars disappear', async ({ page }) => {
|
||||
test('Can use alt+shift+drag to create a tag', 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
|
||||
@@ -233,20 +226,6 @@ 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 }) => {
|
||||
@@ -254,6 +233,7 @@ test.describe('Example Imagery Object', () => {
|
||||
});
|
||||
|
||||
test('Can use the reset button to reset the image @unstable', async ({ page }, testInfo) => {
|
||||
test.slow(testInfo.project === 'chrome-beta', 'This test is slow in chrome-beta');
|
||||
// Get initial image dimensions
|
||||
const initialBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
|
||||
|
||||
@@ -26,23 +26,21 @@ const {
|
||||
setStartOffset,
|
||||
setFixedTimeMode,
|
||||
setRealTimeMode,
|
||||
openObjectTreeContextMenu
|
||||
selectInspectorTab
|
||||
} = require('../../../../appActions');
|
||||
|
||||
test.describe('Testing LAD table configuration', () => {
|
||||
let ladTable;
|
||||
let sineWaveObject;
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Create LAD table
|
||||
ladTable = await createDomainObjectWithDefaults(page, {
|
||||
const ladTable = await createDomainObjectWithDefaults(page, {
|
||||
type: 'LAD Table',
|
||||
name: 'Test LAD Table'
|
||||
});
|
||||
|
||||
// Create Sine Wave Generator
|
||||
sineWaveObject = await createDomainObjectWithDefaults(page, {
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
name: 'Test Sine Wave Generator',
|
||||
parent: ladTable.uuid
|
||||
@@ -53,28 +51,24 @@ 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();
|
||||
await page.getByRole('tab', { name: 'LAD Table Configuration' }).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 selectInspectorTab(page, 'LAD Table Configuration');
|
||||
|
||||
// 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();
|
||||
@@ -82,138 +76,51 @@ 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.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
await page.locator('text=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();
|
||||
await page.getByRole('tab', { name: 'LAD Table Configuration' }).click();
|
||||
await selectInspectorTab(page, 'LAD Table Configuration');
|
||||
|
||||
// show timestamp column
|
||||
await page.getByLabel('Timestamp').check();
|
||||
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 timestamp is still visible
|
||||
// save and reload and make sure only timestamp is still visible
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
await page.locator('text=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();
|
||||
await selectInspectorTab(page, 'LAD Table Configuration');
|
||||
|
||||
// show units, type, and WATCH columns
|
||||
// show units and type 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.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
await page.locator('text=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 ({
|
||||
@@ -265,7 +172,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.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
await page.locator('text=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
|
||||
@@ -293,7 +200,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.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
// Subscribe to the Sine Wave Generator data
|
||||
const getTelemValuePromise = await subscribeToTelemetry(page, sineWaveObject.uuid);
|
||||
|
||||
@@ -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.getByLabel('Notebook Entry Input')).toBeVisible();
|
||||
await expect(page.getByLabel('Notebook Entry', { exact: true })).toHaveClass(/is-selected/);
|
||||
await expect(page.locator('[aria-label="Notebook Entry Input"]')).toBeVisible();
|
||||
await expect(page.locator('[aria-label="Notebook Entry"]')).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,8 +369,6 @@ 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');
|
||||
|
||||
@@ -380,6 +378,8 @@ 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,8 +447,6 @@ 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');
|
||||
|
||||
@@ -458,6 +456,8 @@ 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,55 +482,4 @@ 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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,11 +24,9 @@
|
||||
This test suite is dedicated to tests which verify the basic operations surrounding Notebooks.
|
||||
*/
|
||||
|
||||
const fs = require('fs').promises;
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||
|
||||
const NOTEBOOK_NAME = 'Notebook';
|
||||
// const { expandTreePaneItemByName, createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||
// const nbUtils = require('../../../../helper/notebookUtils');
|
||||
|
||||
test.describe('Snapshot Menu tests', () => {
|
||||
test.fixme(
|
||||
@@ -163,110 +161,3 @@ test.describe('Snapshot Container tests', () => {
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test.describe('Snapshot image tests', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
//Navigate to baseURL
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Create Notebook
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: NOTEBOOK_NAME
|
||||
});
|
||||
});
|
||||
|
||||
test('Can drop an image onto a notebook and create a new entry', async ({ page }) => {
|
||||
const imageData = await fs.readFile('src/images/favicons/favicon-96x96.png');
|
||||
const imageArray = new Uint8Array(imageData);
|
||||
const fileData = Array.from(imageArray);
|
||||
|
||||
const dropTransfer = await page.evaluateHandle((data) => {
|
||||
const dataTransfer = new DataTransfer();
|
||||
const file = new File([new Uint8Array(data)], 'favicon-96x96.png', { type: 'image/png' });
|
||||
dataTransfer.items.add(file);
|
||||
return dataTransfer;
|
||||
}, 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();
|
||||
|
||||
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();
|
||||
|
||||
await page.getByLabel('Close').click();
|
||||
|
||||
// drop another image onto the entry
|
||||
await page.dispatchEvent('.c-snapshots', 'drop', { dataTransfer: dropTransfer });
|
||||
|
||||
// expect two embedded images now
|
||||
expect(await page.getByRole('img', { name: 'favicon-96x96.png thumbnail' }).count()).toBe(2);
|
||||
|
||||
await page.locator('.c-snapshot.c-ne__embed').first().getByTitle('More options').click();
|
||||
|
||||
await page.getByRole('menuitem', { name: /Remove This Embed/ }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Ok', exact: true }).click();
|
||||
|
||||
// expect one embedded image now as we deleted the other
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,7 +25,7 @@ This test suite is dedicated to tests which verify notebook tag functionality.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||
const { createDomainObjectWithDefaults, selectInspectorTab } = require('../../../../appActions');
|
||||
const {
|
||||
enterTextEntry,
|
||||
createNotebookAndEntry,
|
||||
@@ -40,7 +40,7 @@ test.describe('Tagging in Notebooks @addInit', () => {
|
||||
test('Can load tags', async ({ page }) => {
|
||||
await createNotebookAndEntry(page);
|
||||
|
||||
await page.getByRole('tab', { name: 'Annotations' }).click();
|
||||
await selectInspectorTab(page, 'Annotations');
|
||||
|
||||
await page.locator('button:has-text("Add Tag")').click();
|
||||
|
||||
@@ -65,7 +65,7 @@ test.describe('Tagging in Notebooks @addInit', () => {
|
||||
});
|
||||
test('Can add tags with blank entry', async ({ page }) => {
|
||||
await createDomainObjectWithDefaults(page, { type: 'Notebook' });
|
||||
await page.getByRole('tab', { name: 'Annotations' }).click();
|
||||
await selectInspectorTab(page, 'Annotations');
|
||||
|
||||
await enterTextEntry(page, '');
|
||||
await page.hover(`button:has-text("Add Tag")`);
|
||||
@@ -81,7 +81,7 @@ test.describe('Tagging in Notebooks @addInit', () => {
|
||||
test('Can cancel adding tags', async ({ page }) => {
|
||||
await createNotebookAndEntry(page);
|
||||
|
||||
await page.getByRole('tab', { name: 'Annotations' }).click();
|
||||
await selectInspectorTab(page, 'Annotations');
|
||||
|
||||
// Test canceling adding a tag after we click "Type to select tag"
|
||||
await page.locator('button:has-text("Add Tag")').click();
|
||||
@@ -147,13 +147,16 @@ 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();
|
||||
await page.getByLabel('Notebook Entry Input').fill(`An entry without tags`);
|
||||
await page.locator('.c-ne__save-button > button').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.hover('[aria-label="Notebook Entry Display"] >> nth=1');
|
||||
await page.hover('[aria-label="Notebook Entry Input"] >> 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?')
|
||||
@@ -207,7 +210,7 @@ test.describe('Tagging in Notebooks @addInit', () => {
|
||||
test('Can cancel adding a tag', async ({ page }) => {
|
||||
await createNotebookAndEntry(page);
|
||||
|
||||
await page.getByRole('tab', { name: 'Annotations' }).click();
|
||||
await selectInspectorTab(page, 'Annotations');
|
||||
|
||||
// Click on the "Add Tag" button
|
||||
await page.locator('button:has-text("Add Tag")').click();
|
||||
|
||||
@@ -48,8 +48,6 @@ test.describe('Operator Status', () => {
|
||||
});
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
await expect(page.getByText('Select Role')).toBeVisible();
|
||||
// Description should be empty https://github.com/nasa/openmct/issues/6978
|
||||
await expect(page.locator('.c-message__action-text')).toBeHidden();
|
||||
// set role
|
||||
await page.getByRole('button', { name: 'Select' }).click();
|
||||
// dismiss role confirmation popup
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
Testsuite for plot autoscale.
|
||||
*/
|
||||
|
||||
const { createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||
const { selectInspectorTab, createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
test.use({
|
||||
viewport: {
|
||||
@@ -60,7 +60,7 @@ test.describe('Autoscale', () => {
|
||||
// enter edit mode
|
||||
await page.click('button[title="Edit"]');
|
||||
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
await selectInspectorTab(page, 'Config');
|
||||
await turnOffAutoscale(page);
|
||||
|
||||
await setUserDefinedMinAndMax(page, '-2', '2');
|
||||
@@ -68,7 +68,7 @@ test.describe('Autoscale', () => {
|
||||
// save
|
||||
await page.click('button[title="Save"]');
|
||||
await Promise.all([
|
||||
page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(),
|
||||
page.locator('li[title = "Save and Finish Editing"]').click(),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
|
||||
@@ -26,7 +26,7 @@ necessarily be used for reference when writing new tests in this area.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { setTimeConductorBounds } = require('../../../../appActions');
|
||||
const { selectInspectorTab, setTimeConductorBounds } = require('../../../../appActions');
|
||||
|
||||
test.describe('Log plot tests', () => {
|
||||
test('Log Plot ticks are functionally correct in regular and log mode and after refresh', async ({
|
||||
@@ -34,13 +34,14 @@ test.describe('Log plot tests', () => {
|
||||
openmctConfig
|
||||
}) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
//Test is slow and should be split in the future
|
||||
|
||||
//Test.slow decorator is currently broken. Needs to be fixed in https://github.com/nasa/openmct/issues/5374
|
||||
test.slow();
|
||||
|
||||
await makeOverlayPlot(page, myItemsFolderName);
|
||||
await testRegularTicks(page);
|
||||
await enableEditMode(page);
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
await selectInspectorTab(page, 'Config');
|
||||
await enableLogMode(page);
|
||||
await testLogTicks(page);
|
||||
await disableLogMode(page);
|
||||
@@ -214,7 +215,7 @@ async function saveOverlayPlot(page) {
|
||||
.click();
|
||||
|
||||
await Promise.all([
|
||||
page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(),
|
||||
page.locator('text=Save and Finish Editing').click(),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
|
||||
@@ -29,6 +29,7 @@ const { test, expect } = require('../../../../pluginFixtures');
|
||||
const {
|
||||
createDomainObjectWithDefaults,
|
||||
getCanvasPixels,
|
||||
selectInspectorTab,
|
||||
waitForPlotsToRender
|
||||
} = require('../../../../appActions');
|
||||
|
||||
@@ -49,7 +50,7 @@ test.describe('Overlay Plot', () => {
|
||||
|
||||
await page.goto(overlayPlot.url);
|
||||
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
await selectInspectorTab(page, 'Config');
|
||||
|
||||
// navigate to plot series color palette
|
||||
await page.click('.l-browse-bar__actions__edit');
|
||||
@@ -90,7 +91,7 @@ test.describe('Overlay Plot', () => {
|
||||
await page.click('button[title="Edit"]');
|
||||
|
||||
// Expand the "Sine Wave Generator" plot series options and enable limit lines
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
await selectInspectorTab(page, 'Config');
|
||||
await page
|
||||
.getByRole('list', { name: 'Plot Series Properties' })
|
||||
.locator('span')
|
||||
@@ -105,7 +106,7 @@ test.describe('Overlay Plot', () => {
|
||||
|
||||
// Save (exit edit mode)
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
await page.locator('li[title="Save and Finish Editing"]').click();
|
||||
|
||||
await assertLimitLinesExistAndAreVisible(page);
|
||||
|
||||
@@ -116,7 +117,7 @@ test.describe('Overlay Plot', () => {
|
||||
// Enter edit mode
|
||||
await page.click('button[title="Edit"]');
|
||||
|
||||
await page.getByRole('tab', { name: 'Elements' }).click();
|
||||
await selectInspectorTab(page, 'Elements');
|
||||
|
||||
// Drag Sine Wave Generator series from Y Axis 1 into Y Axis 2
|
||||
await page
|
||||
@@ -127,7 +128,7 @@ test.describe('Overlay Plot', () => {
|
||||
|
||||
// Save (exit edit mode)
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
await page.locator('li[title="Save and Finish Editing"]').click();
|
||||
|
||||
await assertLimitLinesExistAndAreVisible(page);
|
||||
|
||||
@@ -167,7 +168,7 @@ test.describe('Overlay Plot', () => {
|
||||
await page.goto(overlayPlot.url);
|
||||
await page.click('button[title="Edit"]');
|
||||
|
||||
await page.getByRole('tab', { name: 'Elements' }).click();
|
||||
await selectInspectorTab(page, 'Elements');
|
||||
|
||||
// Drag swg a, c, e into Y Axis 2
|
||||
await page
|
||||
@@ -181,7 +182,7 @@ test.describe('Overlay Plot', () => {
|
||||
.dragTo(page.locator('[aria-label="Element Item Group Y Axis 2"]'));
|
||||
|
||||
// Assert that Y Axis 1 and Y Axis 2 property groups are visible only
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
await selectInspectorTab(page, 'Config');
|
||||
|
||||
const yAxis1PropertyGroup = page.locator('[aria-label="Y Axis Properties"]');
|
||||
const yAxis2PropertyGroup = page.locator('[aria-label="Y Axis 2 Properties"]');
|
||||
@@ -195,7 +196,7 @@ test.describe('Overlay Plot', () => {
|
||||
const yAxis2Group = page.getByLabel('Y Axis 2');
|
||||
const yAxis3Group = page.getByLabel('Y Axis 3');
|
||||
|
||||
await page.getByRole('tab', { name: 'Elements' }).click();
|
||||
await selectInspectorTab(page, 'Elements');
|
||||
|
||||
// Drag swg b into Y Axis 3
|
||||
await page
|
||||
@@ -203,14 +204,14 @@ test.describe('Overlay Plot', () => {
|
||||
.dragTo(page.locator('[aria-label="Element Item Group Y Axis 3"]'));
|
||||
|
||||
// Assert that all Y Axis property groups are visible
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
await selectInspectorTab(page, 'Config');
|
||||
|
||||
await expect(yAxis1PropertyGroup).toBeVisible();
|
||||
await expect(yAxis2PropertyGroup).toBeVisible();
|
||||
await expect(yAxis3PropertyGroup).toBeVisible();
|
||||
|
||||
// Verify that the elements are in the correct buckets and in the correct order
|
||||
await page.getByRole('tab', { name: 'Elements' }).click();
|
||||
await selectInspectorTab(page, 'Elements');
|
||||
|
||||
expect(yAxis1Group.getByRole('listitem', { name: swgD.name })).toBeTruthy();
|
||||
expect(yAxis1Group.getByRole('listitem').nth(0).getByText(swgD.name)).toBeTruthy();
|
||||
@@ -241,7 +242,7 @@ test.describe('Overlay Plot', () => {
|
||||
await waitForPlotsToRender(page);
|
||||
await page.click('button[title="Edit"]');
|
||||
|
||||
await page.getByRole('tab', { name: 'Elements' }).click();
|
||||
await selectInspectorTab(page, 'Elements');
|
||||
|
||||
await page.locator(`#inspector-elements-tree >> text=${swgA.name}`).click();
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||
const { createDomainObjectWithDefaults, selectInspectorTab } = require('../../../../appActions');
|
||||
const uuid = require('uuid').v4;
|
||||
|
||||
test.describe('Scatter Plot', () => {
|
||||
@@ -54,10 +54,10 @@ test.describe('Scatter Plot', () => {
|
||||
// the SWG appears in the elements pool
|
||||
await page.goto(scatterPlot.url);
|
||||
await editButton.click();
|
||||
await page.getByRole('tab', { name: 'Elements' }).click();
|
||||
await selectInspectorTab(page, 'Elements');
|
||||
await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeVisible();
|
||||
await saveButton.click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
await page.locator('li[title="Save and Finish Editing"]').click();
|
||||
|
||||
// Create another sine wave generator within the scatter plot
|
||||
const swg2 = await createDomainObjectWithDefaults(page, {
|
||||
@@ -82,7 +82,7 @@ test.describe('Scatter Plot', () => {
|
||||
await editButton.click();
|
||||
|
||||
// Click the "Elements" tab
|
||||
await page.getByRole('tab', { name: 'Elements' }).click();
|
||||
await selectInspectorTab(page, 'Elements');
|
||||
await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeHidden();
|
||||
await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg2.name}`)).toBeVisible();
|
||||
await saveButton.click();
|
||||
|
||||
@@ -26,7 +26,11 @@ necessarily be used for reference when writing new tests in this area.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults, waitForPlotsToRender } = require('../../../../appActions');
|
||||
const {
|
||||
createDomainObjectWithDefaults,
|
||||
selectInspectorTab,
|
||||
waitForPlotsToRender
|
||||
} = require('../../../../appActions');
|
||||
|
||||
test.describe('Stacked Plot', () => {
|
||||
let stackedPlot;
|
||||
@@ -71,7 +75,7 @@ test.describe('Stacked Plot', () => {
|
||||
|
||||
await page.click('button[title="Edit"]');
|
||||
|
||||
await page.getByRole('tab', { name: 'Elements' }).click();
|
||||
await selectInspectorTab(page, 'Elements');
|
||||
|
||||
await swgBElementsPoolItem.click({ button: 'right' });
|
||||
await page
|
||||
@@ -103,7 +107,7 @@ test.describe('Stacked Plot', () => {
|
||||
|
||||
await page.click('button[title="Edit"]');
|
||||
|
||||
await page.getByRole('tab', { name: 'Elements' }).click();
|
||||
await selectInspectorTab(page, 'Elements');
|
||||
|
||||
const stackedPlotItem1 = page.locator('.c-plot--stacked-container').nth(0);
|
||||
const stackedPlotItem2 = page.locator('.c-plot--stacked-container').nth(1);
|
||||
@@ -135,7 +139,7 @@ test.describe('Stacked Plot', () => {
|
||||
|
||||
// Save (exit edit mode)
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
await page.locator('li[title="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}`);
|
||||
@@ -148,7 +152,7 @@ test.describe('Stacked Plot', () => {
|
||||
}) => {
|
||||
await page.goto(stackedPlot.url);
|
||||
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
await selectInspectorTab(page, 'Config');
|
||||
|
||||
// Click on the 1st plot
|
||||
await page.locator(`[aria-label="Stacked Plot Item ${swgA.name}"] canvas`).nth(1).click();
|
||||
@@ -189,7 +193,7 @@ test.describe('Stacked Plot', () => {
|
||||
// Go into edit mode
|
||||
await page.click('button[title="Edit"]');
|
||||
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
await selectInspectorTab(page, 'Config');
|
||||
|
||||
// Click on canvas for the 1st plot
|
||||
await page.locator(`[aria-label="Stacked Plot Item ${swgA.name}"]`).click();
|
||||
@@ -234,7 +238,7 @@ test.describe('Stacked Plot', () => {
|
||||
// Go into edit mode
|
||||
await page.click('button[title="Edit"]');
|
||||
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
await selectInspectorTab(page, 'Config');
|
||||
|
||||
let legendProperties = await page.locator('[aria-label="Legend Properties"]');
|
||||
await legendProperties.locator('[title="Display legends per sub plot."]~div input').uncheck();
|
||||
@@ -243,7 +247,7 @@ test.describe('Stacked Plot', () => {
|
||||
|
||||
// Save (exit edit mode)
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
await page.locator('li[title="Save and Finish Editing"]').click();
|
||||
|
||||
await assertAggregateLegendIsVisible(page);
|
||||
|
||||
|
||||
@@ -29,7 +29,8 @@ const {
|
||||
createDomainObjectWithDefaults,
|
||||
setRealTimeMode,
|
||||
setFixedTimeMode,
|
||||
waitForPlotsToRender
|
||||
waitForPlotsToRender,
|
||||
selectInspectorTab
|
||||
} = require('../../../../appActions');
|
||||
|
||||
test.describe('Plot Tagging', () => {
|
||||
@@ -41,7 +42,7 @@ test.describe('Plot Tagging', () => {
|
||||
* @param {Number} yEnd a telemetry item with a plot
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function createTags({ page, canvas, xEnd = 700, yEnd = 520 }) {
|
||||
async function createTags({ page, canvas, xEnd = 700, yEnd = 480 }) {
|
||||
await canvas.hover({ trial: true });
|
||||
|
||||
//Alt+Shift Drag Start to select some points to tag
|
||||
@@ -149,7 +150,7 @@ test.describe('Plot Tagging', () => {
|
||||
await waitForPlotsToRender(page);
|
||||
|
||||
await expect(page.getByRole('tab', { name: 'Annotations' })).not.toHaveClass(/is-current/);
|
||||
await page.getByRole('tab', { name: 'Annotations' }).click();
|
||||
await selectInspectorTab(page, 'Annotations');
|
||||
await expect(page.getByRole('tab', { name: 'Annotations' })).toHaveClass(/is-current/);
|
||||
|
||||
await expect(page.getByText('No tags to display for this item')).toBeVisible();
|
||||
@@ -284,7 +285,7 @@ test.describe('Plot Tagging', () => {
|
||||
page,
|
||||
canvas,
|
||||
xEnd: 700,
|
||||
yEnd: 240
|
||||
yEnd: 215
|
||||
});
|
||||
await basicTagsTests(page);
|
||||
await testTelemetryItem(page, alphaSineWave);
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2023, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
const { 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();
|
||||
});
|
||||
});
|
||||
@@ -78,85 +78,4 @@ 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"]');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -153,6 +153,7 @@ test.describe('Time conductor input fields real-time mode', () => {
|
||||
await expect(page.locator('.c-compact-tc__setting-value.icon-plus')).toContainText('00:00:01');
|
||||
|
||||
// Verify url parameters persist after mode switch
|
||||
await page.waitForNavigation({ waitUntil: 'networkidle' });
|
||||
expect(page.url()).toContain(`startDelta=${startDelta}`);
|
||||
expect(page.url()).toContain(`endDelta=${endDelta}`);
|
||||
});
|
||||
|
||||
@@ -25,15 +25,12 @@ const {
|
||||
openObjectTreeContextMenu,
|
||||
createDomainObjectWithDefaults
|
||||
} = require('../../../../appActions');
|
||||
import { MISSION_TIME } from '../../../../constants';
|
||||
|
||||
test.describe('Timer', () => {
|
||||
let timer;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
timer = await createDomainObjectWithDefaults(page, { type: 'timer' });
|
||||
await assertTimerElements(page, timer);
|
||||
});
|
||||
|
||||
test('Can perform actions on the Timer', async ({ page }) => {
|
||||
@@ -66,70 +63,6 @@ test.describe('Timer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Timer with target date', () => {
|
||||
let timer;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
timer = await createDomainObjectWithDefaults(page, { type: 'timer' });
|
||||
await assertTimerElements(page, timer);
|
||||
});
|
||||
|
||||
// Override clock
|
||||
test.use({
|
||||
clockOptions: {
|
||||
now: MISSION_TIME,
|
||||
shouldAdvanceTime: true
|
||||
}
|
||||
});
|
||||
|
||||
test('Can count down to a target date', async ({ page }) => {
|
||||
// Set the target date to 2024-11-24 03:30:00
|
||||
await page.getByTitle('More options').click();
|
||||
await page.getByRole('menuitem', { name: /Edit Properties.../ }).click();
|
||||
await page.getByPlaceholder('YYYY-MM-DD').fill('2024-11-24');
|
||||
await page.locator('input[name="hour"]').fill('3');
|
||||
await page.locator('input[name="min"]').fill('30');
|
||||
await page.locator('input[name="sec"]').fill('00');
|
||||
await page.getByLabel('Save').click();
|
||||
|
||||
// Get the current timer seconds value
|
||||
const timerSecValue = (await page.locator('.c-timer__value').innerText()).split(':').at(-1);
|
||||
await expect(page.locator('.c-timer__direction')).toHaveClass(/icon-minus/);
|
||||
|
||||
// Wait for the timer to count down and assert
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const newTimerValue = (await page.locator('.c-timer__value').innerText()).split(':').at(-1);
|
||||
return Number(newTimerValue);
|
||||
})
|
||||
.toBeLessThan(Number(timerSecValue));
|
||||
});
|
||||
|
||||
test('Can count up from a target date', async ({ page }) => {
|
||||
// Set the target date to 2020-11-23 03:30:00
|
||||
await page.getByTitle('More options').click();
|
||||
await page.getByRole('menuitem', { name: /Edit Properties.../ }).click();
|
||||
await page.getByPlaceholder('YYYY-MM-DD').fill('2020-11-23');
|
||||
await page.locator('input[name="hour"]').fill('3');
|
||||
await page.locator('input[name="min"]').fill('30');
|
||||
await page.locator('input[name="sec"]').fill('00');
|
||||
await page.getByLabel('Save').click();
|
||||
|
||||
// Get the current timer seconds value
|
||||
const timerSecValue = (await page.locator('.c-timer__value').innerText()).split(':').at(-1);
|
||||
await expect(page.locator('.c-timer__direction')).toHaveClass(/icon-plus/);
|
||||
|
||||
// Wait for the timer to count up and assert
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const newTimerValue = (await page.locator('.c-timer__value').innerText()).split(':').at(-1);
|
||||
return Number(newTimerValue);
|
||||
})
|
||||
.toBeGreaterThan(Number(timerSecValue));
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Actions that can be performed on a timer from context menus.
|
||||
* @typedef {'Start' | 'Stop' | 'Pause' | 'Restart at 0'} TimerAction
|
||||
@@ -208,17 +141,14 @@ function buttonTitleFromAction(action) {
|
||||
* @param {TimerAction} action
|
||||
*/
|
||||
async function assertTimerStateAfterAction(page, action) {
|
||||
const timerValue = page.locator('.c-timer__value');
|
||||
let timerStateClass;
|
||||
switch (action) {
|
||||
case 'Start':
|
||||
case 'Restart at 0':
|
||||
timerStateClass = 'is-started';
|
||||
expect(await timerValue.innerText()).toBe('0D 00:00:00');
|
||||
break;
|
||||
case 'Stop':
|
||||
timerStateClass = 'is-stopped';
|
||||
expect(await timerValue.innerText()).toBe('--:--:--');
|
||||
break;
|
||||
case 'Pause':
|
||||
timerStateClass = 'is-paused';
|
||||
@@ -227,25 +157,3 @@ async function assertTimerStateAfterAction(page, action) {
|
||||
|
||||
await expect.soft(page.locator('.c-timer')).toHaveClass(new RegExp(timerStateClass));
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that all the major components of a timer are present in the DOM.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {import('../../../../appActions').CreatedObjectInfo} timer
|
||||
*/
|
||||
async function assertTimerElements(page, timer) {
|
||||
const timerElement = page.locator('.c-timer');
|
||||
const resetButton = page.getByRole('button', { name: 'Reset' });
|
||||
const pausePlayButton = page
|
||||
.getByRole('button', { name: 'Pause' })
|
||||
.or(page.getByRole('button', { name: 'Start' }));
|
||||
const timerDirectionIcon = page.locator('.c-timer__direction');
|
||||
const timerValue = page.locator('.c-timer__value');
|
||||
|
||||
expect(await page.locator('.l-browse-bar__object-name').innerText()).toBe(timer.name);
|
||||
expect(timerElement).toBeAttached();
|
||||
expect(resetButton).toBeAttached();
|
||||
expect(pausePlayButton).toBeAttached();
|
||||
expect(timerDirectionIcon).toBeAttached();
|
||||
expect(timerValue).toBeAttached();
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults } = require('../../appActions');
|
||||
const { createDomainObjectWithDefaults, selectInspectorTab } = require('../../appActions');
|
||||
const { v4: uuid } = require('uuid');
|
||||
|
||||
test.describe('Grand Search', () => {
|
||||
@@ -61,7 +61,7 @@ test.describe('Grand Search', () => {
|
||||
`Clock D ${myItemsFolderName} Red Folder Blue Folder`
|
||||
);
|
||||
// Click the Elements pool to dismiss the search menu
|
||||
await page.getByRole('tab', { name: 'Elements' }).click();
|
||||
await selectInspectorTab(page, 'Elements');
|
||||
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeHidden();
|
||||
|
||||
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click();
|
||||
@@ -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')).toBeHidden();
|
||||
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeInViewport();
|
||||
|
||||
// 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')).toBeHidden();
|
||||
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).not.toBeInViewport();
|
||||
|
||||
// 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();
|
||||
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
// Click text=Save and Finish Editing
|
||||
await page.locator('text=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"]
|
||||
@@ -175,8 +175,7 @@ test.describe('Grand Search', () => {
|
||||
|
||||
let networkRequests = [];
|
||||
page.on('request', (request) => {
|
||||
const searchRequest =
|
||||
request.url().endsWith('_find') || request.url().includes('by_keystring');
|
||||
const searchRequest = request.url().endsWith('_find');
|
||||
const fetchRequest = request.resourceType() === 'fetch';
|
||||
if (searchRequest && fetchRequest) {
|
||||
networkRequests.push(request);
|
||||
|
||||
@@ -50,6 +50,8 @@ test('Verify that the create button appears and that the Folder Domain Object is
|
||||
|
||||
test('Verify that My Items Tree appears @ipad', async ({ page, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
//Test.slow annotation is currently broken. Needs to be fixed in https://github.com/nasa/openmct/issues/5374
|
||||
test.slow();
|
||||
//Go to baseURL
|
||||
await page.goto('./');
|
||||
|
||||
|
||||
@@ -89,6 +89,20 @@ 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, {
|
||||
@@ -103,7 +117,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.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
await page.keyboard.down('Control');
|
||||
|
||||
@@ -133,7 +147,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.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
await page.keyboard.down('Control');
|
||||
|
||||
@@ -200,7 +214,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.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
// Create Stacked Plot
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
@@ -211,7 +225,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.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
// Create Display Layout
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
@@ -231,7 +245,7 @@ test.describe('Verify tooltips', () => {
|
||||
targetPosition: { x: 500, y: 200 }
|
||||
});
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
await page.keyboard.down('Control');
|
||||
|
||||
@@ -240,13 +254,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();
|
||||
@@ -269,7 +283,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.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
await page.keyboard.down('Control');
|
||||
await page.getByText('SWG 1').nth(2).hover();
|
||||
@@ -293,7 +307,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.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
await page.keyboard.down('Control');
|
||||
await page.getByText('SWG 1').nth(2).hover();
|
||||
@@ -331,18 +345,18 @@ test.describe('Verify tooltips', () => {
|
||||
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 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 tooltip path for notebook embeds', async ({ page }) => {
|
||||
@@ -359,105 +373,26 @@ test.describe('Verify tooltips', () => {
|
||||
expect(tooltipText).toBe(sineWaveObject3.path);
|
||||
});
|
||||
|
||||
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('!', '#'));
|
||||
// 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'
|
||||
// });
|
||||
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Telemetry Table',
|
||||
name: 'Test Telemetry Table'
|
||||
});
|
||||
// await page.dragAndDrop(`text=${sineWaveObject1.name}`, '.c-telemetry-table');
|
||||
// await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-telemetry-table');
|
||||
|
||||
await page.dragAndDrop(`text=${sineWaveObject1.name}`, '.c-telemetry-table');
|
||||
await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-telemetry-table');
|
||||
// await page.locator('button[title="Save"]').click();
|
||||
// await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
await page.keyboard.down('Control');
|
||||
// // .c-telemetry-table__body
|
||||
|
||||
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.keyboard.down('Control');
|
||||
|
||||
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);
|
||||
});
|
||||
// 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);
|
||||
// });
|
||||
});
|
||||
|
||||
121
e2e/tests/performance/memory-usage-imagery.perf.spec.js
Normal file
121
e2e/tests/performance/memory-usage-imagery.perf.spec.js
Normal file
@@ -0,0 +1,121 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2023, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
This test suite is an initial example for memory leak testing using performance. This configuration and execution must
|
||||
be kept separate from the traditional performance measurements to avoid any "observer" effects associated with tracing
|
||||
or profiling playwright and/or the browser.
|
||||
|
||||
Based on a pattern identified in https://github.com/trentmwillis/devtools-protocol-demos/blob/master/testing-demos/memory-leak-by-heap.js
|
||||
and https://github.com/paulirish/automated-chrome-profiling/issues/3
|
||||
|
||||
Best path forward: https://github.com/cowchimp/headless-devtools/blob/master/src/Memory/example.js
|
||||
|
||||
*/
|
||||
|
||||
const { test, expect } = require('@playwright/test');
|
||||
|
||||
const filePath = 'e2e/test-data/PerformanceDisplayLayout.json';
|
||||
|
||||
// eslint-disable-next-line playwright/no-skipped-test
|
||||
test.describe.skip('Memory Performance tests', () => {
|
||||
test.beforeEach(async ({ page, browser }, testInfo) => {
|
||||
// Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
// Click a:has-text("My Items")
|
||||
await page.locator('a:has-text("My Items")').click({
|
||||
button: 'right'
|
||||
});
|
||||
|
||||
// Click text=Import from JSON
|
||||
await page.locator('text=Import from JSON').click();
|
||||
|
||||
// Upload Performance Display Layout.json
|
||||
await page.setInputFiles('#fileElem', filePath);
|
||||
|
||||
// Click text=OK
|
||||
await page.locator('text=OK').click();
|
||||
|
||||
await expect(
|
||||
page.locator('a:has-text("Performance Display Layout Display Layout")')
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('Embedded View Large for Imagery is performant in Fixed Time', async ({ page, browser }) => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
// To to Search Available after Launch
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
|
||||
// Fill Search input
|
||||
await page
|
||||
.locator('[aria-label="OpenMCT Search"] input[type="search"]')
|
||||
.fill('Performance Display Layout');
|
||||
//Search Result Appears and is clicked
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('a:has-text("Performance Display Layout")').first().click()
|
||||
]);
|
||||
|
||||
//Time to Example Imagery Frame loads within Display Layout
|
||||
await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible' });
|
||||
//Time to Example Imagery object loads
|
||||
await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible' });
|
||||
|
||||
const client = await page.context().newCDPSession(page);
|
||||
await client.send('HeapProfiler.enable');
|
||||
await client.send('HeapProfiler.startSampling');
|
||||
// await client.send('HeapProfiler.collectGarbage');
|
||||
await client.send('Performance.enable');
|
||||
|
||||
let performanceMetricsBefore = await client.send('Performance.getMetrics');
|
||||
console.log(performanceMetricsBefore.metrics);
|
||||
|
||||
//await client.send('Performance.disable');
|
||||
|
||||
//Open Large view
|
||||
await page.locator('button:has-text("Large View")').click();
|
||||
await client.send('HeapProfiler.takeHeapSnapshot');
|
||||
|
||||
//Time to Imagery Rendered in Large Frame
|
||||
await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible' });
|
||||
|
||||
//Time to Example Imagery object loads
|
||||
await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible' });
|
||||
|
||||
// Click Close Icon
|
||||
await page.locator('.c-click-icon').click();
|
||||
|
||||
//Time to Example Imagery Frame loads within Display Layout
|
||||
await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible' });
|
||||
//Time to Example Imagery object loads
|
||||
await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible' });
|
||||
|
||||
await client.send('HeapProfiler.collectGarbage');
|
||||
//await client.send('Performance.enable');
|
||||
|
||||
let performanceMetricsAfter = await client.send('Performance.getMetrics');
|
||||
console.log(performanceMetricsAfter.metrics);
|
||||
|
||||
//await client.send('Performance.disable');
|
||||
});
|
||||
});
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
const { test, expect } = require('@playwright/test');
|
||||
|
||||
const memoryLeakFilePath = 'e2e/test-data/memory-leak-detection.json';
|
||||
const filePath = 'e2e/test-data/memory-leak-detection.json';
|
||||
/**
|
||||
* Executes tests to verify that views are not leaking memory on navigation away. This sort of
|
||||
* memory leak is generally caused by a failure to clean up registered listeners.
|
||||
@@ -39,81 +39,70 @@ const memoryLeakFilePath = 'e2e/test-data/memory-leak-detection.json';
|
||||
* 7. Copy the exported file to ../test-data/memory-leak-detection.json
|
||||
*
|
||||
*/
|
||||
|
||||
const NAV_LEAK_TIMEOUT = 10 * 1000; // 10s
|
||||
test.describe('Navigation memory leak is not detected in', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
test.beforeEach(async ({ page, browser }, testInfo) => {
|
||||
// Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
// Click a:has-text("My Items")
|
||||
await page.locator('a:has-text("My Items")').click({
|
||||
button: 'right'
|
||||
});
|
||||
|
||||
// Click text=Import from JSON
|
||||
await page.locator('text=Import from JSON').click();
|
||||
|
||||
// Upload memory-leak-detection.json
|
||||
await page.setInputFiles('#fileElem', memoryLeakFilePath);
|
||||
await page.setInputFiles('#fileElem', filePath);
|
||||
|
||||
// Click text=OK
|
||||
await page.locator('text=OK').click();
|
||||
|
||||
await expect(page.locator('a:has-text("Memory Leak Detection")')).toBeVisible();
|
||||
});
|
||||
|
||||
test('plot view', async ({ page }) => {
|
||||
const result = await navigateToObjectAndDetectMemoryLeak(page, 'overlay-plot-single-1hz-swg', {
|
||||
timeout: NAV_LEAK_TIMEOUT
|
||||
});
|
||||
const result = await navigateToObjectAndDetectMemoryLeak(page, 'overlay-plot-single-1hz-swg');
|
||||
|
||||
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('stacked plot view', async ({ page }) => {
|
||||
const result = await navigateToObjectAndDetectMemoryLeak(page, 'stacked-plot-single-1hz-swg', {
|
||||
timeout: NAV_LEAK_TIMEOUT
|
||||
});
|
||||
const result = await navigateToObjectAndDetectMemoryLeak(page, 'stacked-plot-single-1hz-swg');
|
||||
|
||||
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('LAD table view', async ({ page }) => {
|
||||
const result = await navigateToObjectAndDetectMemoryLeak(page, 'lad-table-single-1hz-swg', {
|
||||
timeout: NAV_LEAK_TIMEOUT
|
||||
});
|
||||
test.only('LAD table view', async ({ page }) => {
|
||||
const result = await navigateToObjectAndDetectMemoryLeak(page, 'lad-table-single-1hz-swg');
|
||||
|
||||
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('LAD table set', async ({ page }) => {
|
||||
const result = await navigateToObjectAndDetectMemoryLeak(page, 'lad-table-set-single-1hz-swg', {
|
||||
timeout: NAV_LEAK_TIMEOUT
|
||||
});
|
||||
const result = await navigateToObjectAndDetectMemoryLeak(page, 'lad-table-set-single-1hz-swg');
|
||||
|
||||
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
//TODO: Figure out why using the `table-row` component inside the `table` component leaks TelemetryTableRow objects
|
||||
test('telemetry table view', async ({ page }) => {
|
||||
const result = await navigateToObjectAndDetectMemoryLeak(
|
||||
page,
|
||||
'telemetry-table-single-1hz-swg',
|
||||
{
|
||||
timeout: NAV_LEAK_TIMEOUT
|
||||
}
|
||||
'telemetry-table-single-1hz-swg'
|
||||
);
|
||||
|
||||
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
//TODO: Figure out why using the `SideBar` component inside the leaks Notebook objects
|
||||
test('notebook view', async ({ page }) => {
|
||||
const result = await navigateToObjectAndDetectMemoryLeak(
|
||||
page,
|
||||
'notebook-memory-leak-detection-test',
|
||||
{
|
||||
timeout: NAV_LEAK_TIMEOUT
|
||||
}
|
||||
'notebook-memory-leak-detection-test'
|
||||
);
|
||||
|
||||
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
|
||||
@@ -121,13 +110,7 @@ test.describe('Navigation memory leak is not detected in', () => {
|
||||
});
|
||||
|
||||
test('display layout of a single SWG alphanumeric', async ({ page }) => {
|
||||
const result = await navigateToObjectAndDetectMemoryLeak(
|
||||
page,
|
||||
'display-layout-single-1hz-swg',
|
||||
{
|
||||
timeout: NAV_LEAK_TIMEOUT
|
||||
}
|
||||
);
|
||||
const result = await navigateToObjectAndDetectMemoryLeak(page, 'display-layout-single-1hz-swg');
|
||||
|
||||
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
|
||||
expect(result).toBe(true);
|
||||
@@ -136,116 +119,85 @@ test.describe('Navigation memory leak is not detected in', () => {
|
||||
test('display layout of a single SWG plot', async ({ page }) => {
|
||||
const result = await navigateToObjectAndDetectMemoryLeak(
|
||||
page,
|
||||
'display-layout-single-overlay-plot',
|
||||
{
|
||||
timeout: NAV_LEAK_TIMEOUT
|
||||
}
|
||||
'display-layout-single-overlay-plot'
|
||||
);
|
||||
|
||||
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
//TODO: Figure out why `svg` in the CompassRose component leaks imagery
|
||||
test('example imagery view', async ({ page }) => {
|
||||
test.skip('example imagery view', async ({ page }) => {
|
||||
const result = await navigateToObjectAndDetectMemoryLeak(
|
||||
page,
|
||||
'example-imagery-memory-leak-test',
|
||||
{
|
||||
timeout: NAV_LEAK_TIMEOUT * 6 // 1 min
|
||||
}
|
||||
'example-imagery-memory-leak-test'
|
||||
);
|
||||
|
||||
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('display layout of example imagery views', async ({ page }) => {
|
||||
test.skip('display layout of example imagery views', async ({ page }) => {
|
||||
const result = await navigateToObjectAndDetectMemoryLeak(
|
||||
page,
|
||||
'display-layout-images-memory-leak-test',
|
||||
{
|
||||
timeout: NAV_LEAK_TIMEOUT * 6 // 1 min
|
||||
}
|
||||
'display-layout-images-memory-leak-test'
|
||||
);
|
||||
|
||||
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('display layout with plots of swgs, alphanumerics, and condition sets, ', async ({
|
||||
test.skip('display layout with plots of swgs, alphanumerics, and condition sets, ', async ({
|
||||
page
|
||||
}) => {
|
||||
const result = await navigateToObjectAndDetectMemoryLeak(
|
||||
page,
|
||||
'display-layout-simple-telemetry',
|
||||
{
|
||||
timeout: NAV_LEAK_TIMEOUT * 6 // 1 min
|
||||
}
|
||||
'display-layout-simple-telemetry'
|
||||
);
|
||||
|
||||
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('flexible layout with plots of swgs', async ({ page }) => {
|
||||
test.skip('flexible layout with plots of swgs', async ({ page }) => {
|
||||
const result = await navigateToObjectAndDetectMemoryLeak(
|
||||
page,
|
||||
'flexible-layout-plots-memory-leak-test',
|
||||
{
|
||||
timeout: NAV_LEAK_TIMEOUT
|
||||
}
|
||||
'flexible-layout-plots-memory-leak-test'
|
||||
);
|
||||
|
||||
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('flexible layout of example imagery views', async ({ page }) => {
|
||||
test.skip('flexible layout of example imagery views', async ({ page }) => {
|
||||
const result = await navigateToObjectAndDetectMemoryLeak(
|
||||
page,
|
||||
'flexible-layout-images-memory-leak-test',
|
||||
{
|
||||
timeout: NAV_LEAK_TIMEOUT * 6 // 1 min
|
||||
}
|
||||
'flexible-layout-images-memory-leak-test'
|
||||
);
|
||||
|
||||
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('tabbed view of display layouts and time strips', async ({ page }) => {
|
||||
test.skip('tabbed view of display layouts and time strips', async ({ page }) => {
|
||||
const result = await navigateToObjectAndDetectMemoryLeak(
|
||||
page,
|
||||
'tab-view-simple-memory-leak-test',
|
||||
{
|
||||
timeout: NAV_LEAK_TIMEOUT * 6 * 2 // 2 min
|
||||
}
|
||||
'tab-view-simple-memory-leak-test'
|
||||
);
|
||||
|
||||
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('time strip view of telemetry', async ({ page }) => {
|
||||
test.skip('time strip view of telemetry', async ({ page }) => {
|
||||
const result = await navigateToObjectAndDetectMemoryLeak(
|
||||
page,
|
||||
'time-strip-telemetry-memory-leak-test',
|
||||
{
|
||||
timeout: NAV_LEAK_TIMEOUT * 6 // 1 min
|
||||
}
|
||||
'time-strip-telemetry-memory-leak-test'
|
||||
);
|
||||
|
||||
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {*} objectName
|
||||
* @returns
|
||||
*/
|
||||
async function navigateToObjectAndDetectMemoryLeak(page, objectName) {
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
|
||||
// Fill Search input
|
||||
@@ -131,8 +131,8 @@ test.describe('Performance tests', () => {
|
||||
await page.evaluate(() => window.performance.mark('new-notebook-entry-created'));
|
||||
|
||||
// Enter Notebook Entry text
|
||||
await page.getByLabel('Notebook Entry Input').last().fill('New Entry');
|
||||
await page.locator('.c-ne__save-button').click();
|
||||
await page.locator('div.c-ne__text').last().fill('New Entry');
|
||||
await page.keyboard.press('Enter');
|
||||
await page.evaluate(() => window.performance.mark('new-notebook-entry-filled'));
|
||||
|
||||
//Individual Notebook Entry Search
|
||||
@@ -1,100 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2023, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
const { 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);
|
||||
});
|
||||
});
|
||||
@@ -32,7 +32,7 @@ const {
|
||||
waitForPlotsToRender
|
||||
} = require('../../appActions');
|
||||
|
||||
test.describe('Plot Tagging Performance', () => {
|
||||
test.describe.fixme('Plot Tagging Performance', () => {
|
||||
/**
|
||||
* Given a canvas and a set of points, tags the points on the canvas.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
@@ -41,7 +41,7 @@ test.describe('Plot Tagging Performance', () => {
|
||||
* @param {Number} yEnd a telemetry item with a plot
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function createTags({ page, canvas, xEnd = 700, yEnd = 520 }) {
|
||||
async function createTags({ page, canvas, xEnd = 700, yEnd = 480 }) {
|
||||
await canvas.hover({ trial: true });
|
||||
|
||||
//Alt+Shift Drag Start to select some points to tag
|
||||
@@ -265,7 +265,7 @@ test.describe('Plot Tagging Performance', () => {
|
||||
page,
|
||||
canvas,
|
||||
xEnd: 700,
|
||||
yEnd: 240
|
||||
yEnd: 215
|
||||
});
|
||||
await basicTagsTests(page);
|
||||
await testTelemetryItem(page, alphaSineWave);
|
||||
|
||||
@@ -26,12 +26,11 @@ Tests the branding associated with the default deployment. At least the about mo
|
||||
|
||||
const { test, expect } = require('../../../pluginFixtures');
|
||||
const percySnapshot = require('@percy/playwright');
|
||||
const VISUAL_URL = require('../../../constants').VISUAL_URL;
|
||||
|
||||
test.describe('Visual - Branding', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
//Go to baseURL and Hide Tree
|
||||
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
|
||||
await page.goto('./#/browse/mine?hideTree=true', { waitUntil: 'networkidle' });
|
||||
});
|
||||
|
||||
test('Visual - About Modal', async ({ page, theme }) => {
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2023, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
const { test } = require('../../../pluginFixtures.js');
|
||||
const { VISUAL_URL, MISSION_TIME } = require('../../../constants.js');
|
||||
const percySnapshot = require('@percy/playwright');
|
||||
|
||||
//Declare the scope of the visual test
|
||||
const inspectorPane = '.l-shell__pane-inspector';
|
||||
|
||||
test.describe('Visual - Controlled Clock', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
|
||||
});
|
||||
test.use({
|
||||
storageState: './e2e/test-data/overlay_plot_with_delay_storage.json',
|
||||
clockOptions: {
|
||||
now: MISSION_TIME,
|
||||
shouldAdvanceTime: true
|
||||
}
|
||||
});
|
||||
|
||||
test('Inspector from overlay_plot_with_delay_storage @localStorage', async ({ page, theme }) => {
|
||||
//Expand the Inspector Pane
|
||||
await page.getByRole('button', { name: 'Inspect' }).click();
|
||||
|
||||
await percySnapshot(page, `Inspector view of overlayPlot (theme: ${theme})`, {
|
||||
scope: inspectorPane
|
||||
});
|
||||
//Open Annotations Tab
|
||||
await page.getByRole('tab', { name: 'Annotations' }).click();
|
||||
|
||||
await percySnapshot(page, `Inspector view of Annotations Tab (theme: ${theme})`, {
|
||||
scope: inspectorPane
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -25,21 +25,14 @@ const {
|
||||
expandTreePaneItemByName,
|
||||
createDomainObjectWithDefaults
|
||||
} = require('../../../appActions.js');
|
||||
const VISUAL_URL = require('../../../constants.js').VISUAL_URL;
|
||||
|
||||
const percySnapshot = require('@percy/playwright');
|
||||
|
||||
//Declare the scope of the visual test
|
||||
const treePane = "[role=tree][aria-label='Main Tree']";
|
||||
|
||||
test.describe('Visual - Tree Pane', () => {
|
||||
test('Tree pane in various states', async ({ page, theme, openmctConfig }) => {
|
||||
test('Tree pane in various states @unstable', async ({ page, theme, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
|
||||
await page.goto('./#/browse/mine', { waitUntil: 'networkidle' });
|
||||
|
||||
//Open Tree
|
||||
await page.getByRole('button', { name: 'Browse' }).click();
|
||||
|
||||
//Create a Folder Structure
|
||||
const foo = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder',
|
||||
name: 'Foo Folder'
|
||||
@@ -67,6 +60,8 @@ test.describe('Visual - Tree Pane', () => {
|
||||
name: 'Z Clock'
|
||||
});
|
||||
|
||||
const treePane = "[role=tree][aria-label='Main Tree']";
|
||||
|
||||
await percySnapshot(page, `Tree Pane w/ collapsed tree (theme: ${theme})`, {
|
||||
scope: treePane
|
||||
});
|
||||
|
||||
@@ -25,31 +25,35 @@ Collection of Visual Tests set to run with browser clock manipulate made possibl
|
||||
clockOptions plugin fixture.
|
||||
*/
|
||||
|
||||
const { VISUAL_URL, MISSION_TIME } = require('../../constants');
|
||||
const { test, expect } = require('../../pluginFixtures');
|
||||
const percySnapshot = require('@percy/playwright');
|
||||
|
||||
test.describe('Visual - Controlled Clock', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
|
||||
//Go to baseURL and Hide Tree
|
||||
await page.goto('./#/browse/mine?hideTree=true', { waitUntil: 'networkidle' });
|
||||
});
|
||||
test.use({
|
||||
storageState: './e2e/test-data/overlay_plot_with_delay_storage.json',
|
||||
clockOptions: {
|
||||
now: MISSION_TIME,
|
||||
shouldAdvanceTime: false //Don't advance the clock
|
||||
}
|
||||
});
|
||||
|
||||
test('Overlay Plot Loading Indicator @localStorage', async ({ page, theme }) => {
|
||||
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
|
||||
await page.locator('a').filter({ hasText: 'Overlay Plot with 5s Delay' }).click();
|
||||
// Go to baseURL
|
||||
await page.goto('./#/browse/mine', { waitUntil: 'networkidle' });
|
||||
await page.getByTitle('Collapse Browse Pane').click();
|
||||
await page
|
||||
.locator('a')
|
||||
.filter({ hasText: 'Overlay Plot with Telemetry Object Overlay Plot' })
|
||||
.click();
|
||||
//Ensure that we're on the Unnamed Overlay Plot object
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText(
|
||||
'Overlay Plot with 5s Delay'
|
||||
'Overlay Plot with Telemetry Object'
|
||||
);
|
||||
|
||||
//Wait for canvas to be rendered and stop animating, but plot should not be loaded. Cannot use waitForPlotsToRender
|
||||
//Wait for canvas to be rendered and stop animating
|
||||
await page.locator('canvas >> nth=1').hover({ trial: true });
|
||||
|
||||
//Take snapshot of Sine Wave Generator within Overlay Plot
|
||||
|
||||
@@ -29,11 +29,11 @@ are only meant to run against openmct's app.js started by `npm run start` within
|
||||
const { test, expect } = require('../../pluginFixtures');
|
||||
const percySnapshot = require('@percy/playwright');
|
||||
const { createDomainObjectWithDefaults } = require('../../appActions');
|
||||
const { VISUAL_URL } = require('../../constants');
|
||||
|
||||
test.describe('Visual - Default', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
|
||||
//Go to baseURL and Hide Tree
|
||||
await page.goto('./#/browse/mine?hideTree=true', { waitUntil: 'networkidle' });
|
||||
});
|
||||
|
||||
test('Visual - Default Dashboard', async ({ page, theme }) => {
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2023, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* 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
|
||||
};
|
||||
}
|
||||
@@ -23,15 +23,13 @@
|
||||
const { expect, test } = require('../../pluginFixtures');
|
||||
const percySnapshot = require('@percy/playwright');
|
||||
const { createDomainObjectWithDefaults } = require('../../appActions');
|
||||
const VISUAL_URL = require('../../constants').VISUAL_URL;
|
||||
|
||||
test.describe('Visual - LAD Table', () => {
|
||||
/** @type {import('@playwright/test').Locator} */
|
||||
let ladTable;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
// Create LAD Table
|
||||
ladTable = await createDomainObjectWithDefaults(page, {
|
||||
type: 'LAD Table',
|
||||
@@ -57,6 +55,9 @@ test.describe('Visual - LAD Table', () => {
|
||||
});
|
||||
test('Toggled column widths behave accordingly', async ({ page, theme }) => {
|
||||
await page.goto(ladTable.url);
|
||||
//Close panes for visual consistency
|
||||
await page.getByTitle('Collapse Inspect Pane').click();
|
||||
await page.getByTitle('Collapse Browse Pane').click();
|
||||
|
||||
await expect(page.locator('button[title="Expand Columns"]')).toBeVisible();
|
||||
|
||||
|
||||
@@ -22,12 +22,15 @@
|
||||
|
||||
const { test } = require('../../pluginFixtures');
|
||||
const percySnapshot = require('@percy/playwright');
|
||||
const { expandTreePaneItemByName, createDomainObjectWithDefaults } = require('../../appActions');
|
||||
const {
|
||||
selectInspectorTab,
|
||||
expandTreePaneItemByName,
|
||||
createDomainObjectWithDefaults
|
||||
} = require('../../appActions');
|
||||
const {
|
||||
startAndAddRestrictedNotebookObject,
|
||||
enterTextEntry
|
||||
} = require('../../helper/notebookUtils');
|
||||
const { VISUAL_URL } = require('../../constants');
|
||||
|
||||
test.describe('Visual - Restricted Notebook', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
@@ -41,26 +44,23 @@ test.describe('Visual - Restricted Notebook', () => {
|
||||
});
|
||||
|
||||
test.describe('Visual - Notebook', () => {
|
||||
let notebook;
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
|
||||
notebook = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Notebook',
|
||||
name: 'Test Notebook'
|
||||
});
|
||||
//Go to baseURL and Hide Tree
|
||||
await page.goto('./#/browse/mine?hideTree=true', { waitUntil: 'networkidle' });
|
||||
});
|
||||
test('Accepts dropped objects as embeds', async ({ page, theme, openmctConfig }) => {
|
||||
test('Accepts dropped objects as embeds @unstable', async ({ page, theme, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
|
||||
const notebook = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Notebook',
|
||||
name: 'Embed Test Notebook'
|
||||
});
|
||||
// Create Overlay Plot
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Overlay Plot',
|
||||
name: 'Dropped Overlay Plot'
|
||||
});
|
||||
|
||||
//Open Tree
|
||||
await page.getByRole('button', { name: 'Browse' }).click();
|
||||
|
||||
await expandTreePaneItemByName(page, myItemsFolderName);
|
||||
|
||||
await page.goto(notebook.url);
|
||||
@@ -70,19 +70,18 @@ test.describe('Visual - Notebook', () => {
|
||||
await percySnapshot(page, `Notebook w/ dropped embed (theme: ${theme})`);
|
||||
});
|
||||
test("Blur 'Add tag' on Notebook", async ({ page, theme }) => {
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Notebook',
|
||||
name: 'Add Tag Test Notebook'
|
||||
});
|
||||
await enterTextEntry(page, 'Entry 0');
|
||||
|
||||
await percySnapshot(page, `Notebook Entry (theme: '${theme}')`);
|
||||
|
||||
// Open the Inspector
|
||||
await page.getByRole('button', { name: 'Inspect' }).click();
|
||||
// Open the Annotations tab
|
||||
await page.getByRole('tab', { name: 'Annotations' }).click();
|
||||
// Click on Annotations tab
|
||||
await selectInspectorTab(page, 'Annotations');
|
||||
|
||||
// Take snapshot of the notebook with the Annotations tab opened
|
||||
await percySnapshot(page, `Notebook Annotation (theme: '${theme}')`);
|
||||
|
||||
// Add annotation
|
||||
await page.locator('button:has-text("Add Tag")').click();
|
||||
|
||||
// Take snapshot of the notebook with the AutoComplete field visible
|
||||
|
||||
@@ -27,11 +27,11 @@
|
||||
const { test, expect } = require('../../pluginFixtures');
|
||||
const percySnapshot = require('@percy/playwright');
|
||||
const { createDomainObjectWithDefaults } = require('../../appActions');
|
||||
const VISUAL_URL = require('../../constants').VISUAL_URL;
|
||||
|
||||
test.describe("Visual - Check Notification Info Banner of 'Save successful'", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
|
||||
// Go to baseURL and Hide Tree
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
});
|
||||
|
||||
test("Create a clock, click on 'Save successful' banner and dismiss it", async ({
|
||||
@@ -43,6 +43,12 @@ test.describe("Visual - Check Notification Info Banner of 'Save successful'", ()
|
||||
type: 'Clock',
|
||||
name: 'Default Clock'
|
||||
});
|
||||
// Verify there is a button with aria-label="Review 1 Notification"
|
||||
expect(await page.locator('button[aria-label="Review 1 Notification"]').isVisible()).toBe(true);
|
||||
// Verify there is a button with aria-label="Clear all notifications"
|
||||
expect(await page.locator('button[aria-label="Clear all notifications"]').isVisible()).toBe(
|
||||
true
|
||||
);
|
||||
// Click on the div with role="alert" that has "Save successful" text
|
||||
await page.locator('div[role="alert"]:has-text("Save successful")').click();
|
||||
// Verify there is a div with role="dialog"
|
||||
|
||||
@@ -21,20 +21,16 @@
|
||||
*****************************************************************************/
|
||||
|
||||
const { test } = require('../../pluginFixtures');
|
||||
const {
|
||||
setBoundsToSpanAllActivities,
|
||||
setDraftStatusForPlan
|
||||
} = require('../../helper/planningUtils');
|
||||
const { setBoundsToSpanAllActivities } = require('../../helper/planningUtils');
|
||||
const { createDomainObjectWithDefaults, createPlanFromJSON } = require('../../appActions');
|
||||
const percySnapshot = require('@percy/playwright');
|
||||
const VISUAL_URL = require('../../constants').VISUAL_URL;
|
||||
const examplePlanSmall = require('../../test-data/examplePlans/ExamplePlan_Small2.json');
|
||||
|
||||
const snapshotScope = '.l-shell__pane-main .l-pane__contents';
|
||||
|
||||
test.describe('Visual - Planning', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
});
|
||||
|
||||
test('Plan View', async ({ page, theme }) => {
|
||||
@@ -54,7 +50,8 @@ test.describe('Visual - Planning', () => {
|
||||
name: 'Plan Visual Test (Draft)',
|
||||
json: examplePlanSmall
|
||||
});
|
||||
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
|
||||
await page.goto('./#/browse/mine');
|
||||
|
||||
await setDraftStatusForPlan(page, plan);
|
||||
|
||||
await setBoundsToSpanAllActivities(page, examplePlanSmall, plan.url);
|
||||
@@ -90,7 +87,7 @@ test.describe('Visual - Planning', () => {
|
||||
|
||||
await setDraftStatusForPlan(page, plan);
|
||||
|
||||
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
|
||||
await page.goto('./#/browse/mine');
|
||||
|
||||
await setBoundsToSpanAllActivities(page, examplePlanSmall, ganttChart.url);
|
||||
await percySnapshot(page, `Gantt Chart View w/ draft status (theme: ${theme})`, {
|
||||
@@ -98,3 +95,14 @@ test.describe('Visual - Planning', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Uses the Open MCT API to set the status of a plan to 'draft'.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {import('../../appActions').CreatedObjectInfo} plan
|
||||
*/
|
||||
async function setDraftStatusForPlan(page, plan) {
|
||||
await page.evaluate(async (planObject) => {
|
||||
await window.openmct.status.set(planObject.uuid, 'draft');
|
||||
}, plan);
|
||||
}
|
||||
|
||||
@@ -26,15 +26,15 @@ This test suite is dedicated to tests which verify search functionality.
|
||||
|
||||
const { test, expect } = require('../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults } = require('../../appActions');
|
||||
const { VISUAL_URL } = require('../../constants');
|
||||
|
||||
const percySnapshot = require('@percy/playwright');
|
||||
|
||||
test.describe('Grand Search', () => {
|
||||
let clock;
|
||||
let displayLayout;
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
|
||||
test.beforeEach(async ({ page, theme }) => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
await page.getByTitle('Collapse Browse Pane').click();
|
||||
|
||||
displayLayout = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Display Layout',
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import EventMetadataProvider from './EventMetadataProvider';
|
||||
import EventTelemetryProvider from './EventTelemetryProvider';
|
||||
import EventMetadataProvider from './EventMetadataProvider';
|
||||
|
||||
export default function EventGeneratorPlugin(options) {
|
||||
return function install(openmct) {
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import { createOpenMct, resetApplicationState } from '../../src/utils/testing';
|
||||
import EventMessageGeneratorPlugin from './plugin.js';
|
||||
import { createOpenMct, resetApplicationState } from '../../src/utils/testing';
|
||||
|
||||
describe('the plugin', () => {
|
||||
let openmct;
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
|
||||
import EventEmitter from 'EventEmitter';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import createExampleUser from './exampleUserCreator';
|
||||
|
||||
const STATUSES = [
|
||||
|
||||
@@ -29,8 +29,7 @@ define(['./WorkerInterface'], function (WorkerInterface) {
|
||||
randomness: 0,
|
||||
phase: 0,
|
||||
loadDelay: 0,
|
||||
infinityValues: false,
|
||||
exceedFloat32: false
|
||||
infinityValues: false
|
||||
};
|
||||
|
||||
function GeneratorProvider(openmct, StalenessProvider) {
|
||||
@@ -54,8 +53,7 @@ define(['./WorkerInterface'], function (WorkerInterface) {
|
||||
'randomness',
|
||||
'phase',
|
||||
'loadDelay',
|
||||
'infinityValues',
|
||||
'exceedFloat32'
|
||||
'infinityValues'
|
||||
];
|
||||
|
||||
request = request || {};
|
||||
|
||||
@@ -85,8 +85,7 @@
|
||||
data.offset,
|
||||
data.phase,
|
||||
data.randomness,
|
||||
data.infinityValues,
|
||||
data.exceedFloat32
|
||||
data.infinityValues
|
||||
),
|
||||
wavelengths: wavelengths(),
|
||||
intensities: intensities(),
|
||||
@@ -97,8 +96,7 @@
|
||||
data.offset,
|
||||
data.phase,
|
||||
data.randomness,
|
||||
data.infinityValues,
|
||||
data.exceedFloat32
|
||||
data.infinityValues
|
||||
)
|
||||
}
|
||||
});
|
||||
@@ -138,7 +136,6 @@
|
||||
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;
|
||||
@@ -149,28 +146,10 @@
|
||||
data.push({
|
||||
utc: nextStep,
|
||||
yesterday: nextStep - 60 * 60 * 24 * 1000,
|
||||
sin: sin(
|
||||
nextStep,
|
||||
period,
|
||||
amplitude,
|
||||
offset,
|
||||
phase,
|
||||
randomness,
|
||||
infinityValues,
|
||||
exceedFloat32
|
||||
),
|
||||
sin: sin(nextStep, period, amplitude, offset, phase, randomness, infinityValues),
|
||||
wavelengths: wavelengths(),
|
||||
intensities: intensities(),
|
||||
cos: cos(
|
||||
nextStep,
|
||||
period,
|
||||
amplitude,
|
||||
offset,
|
||||
phase,
|
||||
randomness,
|
||||
infinityValues,
|
||||
exceedFloat32
|
||||
)
|
||||
cos: cos(nextStep, period, amplitude, offset, phase, randomness, infinityValues)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -197,26 +176,9 @@
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
function cos(timestamp, period, amplitude, offset, phase, randomness, infinityValues) {
|
||||
if (infinityValues && Math.random() > 0.5) {
|
||||
return Number.POSITIVE_INFINITY;
|
||||
} else if (exceedFloat32 && Math.random() < 0.01) {
|
||||
return getRandomFloat32OverflowValue();
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -226,26 +188,9 @@
|
||||
);
|
||||
}
|
||||
|
||||
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) {
|
||||
function sin(timestamp, period, amplitude, offset, phase, randomness, infinityValues) {
|
||||
if (infinityValues && Math.random() > 0.5) {
|
||||
return Number.POSITIVE_INFINITY;
|
||||
} else if (exceedFloat32 && Math.random() < 0.01) {
|
||||
return getRandomFloat32OverflowValue();
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -255,13 +200,6 @@
|
||||
);
|
||||
}
|
||||
|
||||
// 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) {
|
||||
|
||||
@@ -20,11 +20,11 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import GeneratorMetadataProvider from './GeneratorMetadataProvider';
|
||||
import GeneratorProvider from './GeneratorProvider';
|
||||
import SinewaveLimitProvider from './SinewaveLimitProvider';
|
||||
import SinewaveStalenessProvider from './SinewaveStalenessProvider';
|
||||
import StateGeneratorProvider from './StateGeneratorProvider';
|
||||
import GeneratorMetadataProvider from './GeneratorMetadataProvider';
|
||||
|
||||
export default function (openmct) {
|
||||
openmct.types.addType('example.state-generator', {
|
||||
@@ -122,13 +122,6 @@ 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',
|
||||
@@ -147,7 +140,6 @@ export default function (openmct) {
|
||||
randomness: 0,
|
||||
loadDelay: 0,
|
||||
infinityValues: false,
|
||||
exceedFloat32: false,
|
||||
staleness: false
|
||||
};
|
||||
}
|
||||
|
||||
19
example/simpleVuePlugin/HelloWorld.vue
Normal file
19
example/simpleVuePlugin/HelloWorld.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<div class="example">{{ msg }}</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
msg: 'Hello world!'
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.example {
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
35
example/simpleVuePlugin/plugin.js
Normal file
35
example/simpleVuePlugin/plugin.js
Normal file
@@ -0,0 +1,35 @@
|
||||
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;
|
||||
298
index.html
298
index.html
@@ -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>
|
||||
|
||||
69
package.json
69
package.json
@@ -1,45 +1,43 @@
|
||||
{
|
||||
"name": "openmct",
|
||||
"version": "3.2.0-next",
|
||||
"version": "3.1.0-next",
|
||||
"description": "The Open MCT core platform",
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "7.22.5",
|
||||
"@braintree/sanitize-url": "6.0.4",
|
||||
"@braintree/sanitize-url": "6.0.2",
|
||||
"@deploysentinel/playwright": "0.3.4",
|
||||
"@percy/cli": "1.27.4",
|
||||
"@percy/cli": "1.26.0",
|
||||
"@percy/playwright": "1.0.4",
|
||||
"@playwright/test": "1.39.0",
|
||||
"@playwright/test": "1.36.2",
|
||||
"@types/eventemitter3": "1.2.0",
|
||||
"@types/jasmine": "5.1.2",
|
||||
"@types/jasmine": "4.3.4",
|
||||
"@types/lodash": "4.14.192",
|
||||
"@vue/compiler-sfc": "3.3.8",
|
||||
"@vue/compat": "^3.1.0",
|
||||
"@vue/compiler-sfc": "^3.1.0",
|
||||
"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.3.8",
|
||||
"cspell": "6.31.2",
|
||||
"css-loader": "6.8.1",
|
||||
"d3-axis": "3.0.0",
|
||||
"d3-scale": "4.0.2",
|
||||
"d3-scale": "3.3.0",
|
||||
"d3-selection": "3.0.0",
|
||||
"eslint": "8.54.0",
|
||||
"eslint-config-prettier": "9.0.0",
|
||||
"eslint-plugin-compat": "4.2.0",
|
||||
"eslint-plugin-no-unsanitized": "4.0.2",
|
||||
"eslint": "8.43.0",
|
||||
"eslint-config-prettier": "8.8.0",
|
||||
"eslint-plugin-compat": "4.1.4",
|
||||
"eslint-plugin-playwright": "0.12.0",
|
||||
"eslint-plugin-prettier": "4.2.1",
|
||||
"eslint-plugin-simple-import-sort": "10.0.0",
|
||||
"eslint-plugin-unicorn": "49.0.0",
|
||||
"eslint-plugin-vue": "9.18.1",
|
||||
"eslint-plugin-you-dont-need-lodash-underscore": "6.13.0",
|
||||
"eslint-plugin-vue": "9.15.0",
|
||||
"eslint-plugin-you-dont-need-lodash-underscore": "6.12.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.1.1",
|
||||
"jasmine-core": "5.0.0",
|
||||
"karma": "6.4.2",
|
||||
"karma-chrome-launcher": "3.2.0",
|
||||
"karma-cli": "2.0.0",
|
||||
@@ -52,44 +50,39 @@
|
||||
"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.1.1",
|
||||
"nyc": "15.1.0",
|
||||
"painterro": "1.2.87",
|
||||
"painterro": "1.2.78",
|
||||
"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.68.0",
|
||||
"sass": "1.63.4",
|
||||
"sass-loader": "13.3.2",
|
||||
"sinon": "17.0.0",
|
||||
"sinon": "15.1.0",
|
||||
"style-loader": "3.3.3",
|
||||
"tiny-emitter": "2.1.0",
|
||||
"typescript": "5.2.2",
|
||||
"uuid": "9.0.1",
|
||||
"vue": "3.3.8",
|
||||
"vue-eslint-parser": "9.3.2",
|
||||
"vue-loader": "16.8.3",
|
||||
"webpack": "5.89.0",
|
||||
"typescript": "5.1.3",
|
||||
"uuid": "9.0.0",
|
||||
"vue": "^3.1.0",
|
||||
"vue-eslint-parser": "9.3.1",
|
||||
"webpack": "5.88.0",
|
||||
"vue-loader": "^16.0.0",
|
||||
"webpack-cli": "5.1.1",
|
||||
"webpack-dev-server": "4.15.1",
|
||||
"webpack-merge": "5.10.0"
|
||||
"webpack-merge": "5.9.0"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rm -rf ./dist ./node_modules ./package-lock.json ./coverage ./html-test-results ./test-results ./.nyc_output ",
|
||||
"clean-test-lint": "npm run clean; npm install; npm run test; npm run lint; npm run lint:spelling",
|
||||
"start": "npx webpack serve --config ./.webpack/webpack.dev.js",
|
||||
"start:prod": "npx webpack serve --config ./.webpack/webpack.prod.js",
|
||||
"start:coverage": "npx webpack serve --config ./.webpack/webpack.coverage.js",
|
||||
"lint: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": "eslint example src e2e --ext .js openmct.js --max-warnings=0 && eslint example src --ext .vue",
|
||||
"lint:spelling": "cspell \"**/*.{js,md,vue}\" --show-context --gitignore",
|
||||
"lint:fix": "eslint example src e2e --ext .js,.vue openmct.js --fix",
|
||||
"build:prod": "webpack --config ./.webpack/webpack.prod.js",
|
||||
"build:dev": "webpack --config ./.webpack/webpack.dev.js",
|
||||
@@ -109,9 +102,7 @@
|
||||
"test:e2e:visual:full": "percy exec --config ./e2e/.percy.nightly.yml -- npx playwright test --config=e2e/playwright-visual.config.js --grep-invert @unstable",
|
||||
"test:e2e:full": "npx playwright test --config=e2e/playwright-ci.config.js --grep-invert @couchdb",
|
||||
"test:e2e:watch": "npx playwright test --ui --config=e2e/playwright-ci.config.js",
|
||||
"test:perf:contract": "npx playwright test --config=e2e/playwright-performance-dev.config.js",
|
||||
"test:perf:localhost": "npx playwright test --config=e2e/playwright-performance-prod.config.js --project=chrome",
|
||||
"test:perf:memory": "npx playwright test --config=e2e/playwright-performance-prod.config.js --project=chrome-memory",
|
||||
"test:perf": "npx playwright test --config=e2e/playwright-performance.config.js",
|
||||
"update-about-dialog-copyright": "perl -pi -e 's/20\\d\\d\\-202\\d/2014\\-2023/gm' ./src/ui/layout/AboutDialog.vue",
|
||||
"update-copyright-date": "npm run update-about-dialog-copyright && grep -lr --null --include=*.{js,scss,vue,ts,sh,html,md,frag} 'Copyright (c) 20' . | xargs -r0 perl -pi -e 's/Copyright\\s\\(c\\)\\s20\\d\\d\\-20\\d\\d/Copyright \\(c\\)\\ 2014\\-2023/gm'",
|
||||
"cov:e2e:report": "nyc report --reporter=lcovonly --report-dir=./coverage/e2e",
|
||||
@@ -126,7 +117,7 @@
|
||||
"url": "https://github.com/nasa/openmct.git"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.14.2 <22"
|
||||
"node": ">=16.19.1 <20"
|
||||
},
|
||||
"browserslist": [
|
||||
"Firefox ESR",
|
||||
|
||||
@@ -33,7 +33,7 @@ define([
|
||||
'./ui/registries/ToolbarRegistry',
|
||||
'./ui/router/ApplicationRouter',
|
||||
'./ui/router/Browse',
|
||||
'./ui/layout/AppLayout.vue',
|
||||
'./ui/layout/Layout.vue',
|
||||
'./ui/preview/plugin',
|
||||
'./api/Branding',
|
||||
'./plugins/licenses/plugin',
|
||||
@@ -43,6 +43,7 @@ define([
|
||||
'./plugins/duplicate/plugin',
|
||||
'./plugins/importFromJSONAction/plugin',
|
||||
'./plugins/exportAsJSONAction/plugin',
|
||||
'./ui/components/components',
|
||||
'vue'
|
||||
], function (
|
||||
EventEmitter,
|
||||
@@ -67,6 +68,7 @@ define([
|
||||
DuplicateActionPlugin,
|
||||
ImportFromJSONAction,
|
||||
ExportAsJSONAction,
|
||||
components,
|
||||
Vue
|
||||
) {
|
||||
/**
|
||||
@@ -428,6 +430,7 @@ define([
|
||||
};
|
||||
|
||||
MCT.prototype.plugins = plugins;
|
||||
MCT.prototype.components = components.default;
|
||||
|
||||
return MCT;
|
||||
});
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import { createOpenMct, resetApplicationState } from '../../utils/testing';
|
||||
import ActionCollection from './ActionCollection';
|
||||
import { createOpenMct, resetApplicationState } from '../../utils/testing';
|
||||
|
||||
describe('The ActionCollection', () => {
|
||||
let openmct;
|
||||
|
||||
@@ -20,9 +20,8 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import EventEmitter from 'EventEmitter';
|
||||
import _ from 'lodash';
|
||||
|
||||
import ActionCollection from './ActionCollection';
|
||||
import _ from 'lodash';
|
||||
|
||||
class ActionsAPI extends EventEmitter {
|
||||
constructor(openmct) {
|
||||
@@ -93,7 +92,7 @@ class ActionsAPI extends EventEmitter {
|
||||
if (this._actionCollections.has(key)) {
|
||||
let actionCollection = this._actionCollections.get(key);
|
||||
actionCollection.off('destroy', this._updateCachedActionCollections);
|
||||
delete actionCollection.applicableActions;
|
||||
|
||||
this._actionCollections.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,9 +20,9 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import ActionsAPI from './ActionsAPI';
|
||||
import { createOpenMct, resetApplicationState } from '../../utils/testing';
|
||||
import ActionCollection from './ActionCollection';
|
||||
import ActionsAPI from './ActionsAPI';
|
||||
|
||||
describe('The Actions API', () => {
|
||||
let openmct;
|
||||
|
||||
@@ -20,9 +20,9 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import EventEmitter from 'EventEmitter';
|
||||
import _ from 'lodash';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
/**
|
||||
* @readonly
|
||||
@@ -100,7 +100,7 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
creatable: false,
|
||||
cssClass: 'icon-notebook',
|
||||
initialize: function (domainObject) {
|
||||
domainObject.targets = domainObject.targets || [];
|
||||
domainObject.targets = domainObject.targets || {};
|
||||
domainObject._deleted = domainObject._deleted || false;
|
||||
domainObject.originalContextPath = domainObject.originalContextPath || '';
|
||||
domainObject.tags = domainObject.tags || [];
|
||||
@@ -117,10 +117,10 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
* @property {ANNOTATION_TYPES} annotationType the type of annotation to create (e.g., PLOT_SPATIAL)
|
||||
* @property {Tag[]} tags tags to add to the annotation, e.g., SCIENCE for science related annotations
|
||||
* @property {String} contentText Some text to add to the annotation, e.g. ("This annotation is about science")
|
||||
* @property {Array<Object>} targets The targets ID keystrings and their specific properties.
|
||||
* For plots, this will be a bounding box, e.g.: {keyString: "d8385009-789d-457b-acc7-d50ba2fd55ea", maxY: 100, minY: 0, maxX: 100, minX: 0}
|
||||
* @property {Object<string, Object>} targets The targets ID keystrings and their specific properties.
|
||||
* For plots, this will be a bounding box, e.g.: {maxY: 100, minY: 0, maxX: 100, minX: 0}
|
||||
* For notebooks, this will be an entry ID, e.g.: {entryId: "entry-ecb158f5-d23c-45e1-a704-649b382622ba"}
|
||||
* @property {DomainObject>[]} targetDomainObjects the domain objects this annotation points to (e.g., telemetry objects for a plot)
|
||||
* @property {DomainObject>} targetDomainObjects the targets ID keystrings and the domain objects this annotation points to (e.g., telemetry objects for a plot)
|
||||
*/
|
||||
/**
|
||||
* @method create
|
||||
@@ -141,15 +141,11 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
throw new Error(`Unknown annotation type: ${annotationType}`);
|
||||
}
|
||||
|
||||
if (!targets.length) {
|
||||
if (!Object.keys(targets).length) {
|
||||
throw new Error(`At least one target is required to create an annotation`);
|
||||
}
|
||||
|
||||
if (targets.some((target) => !target.keyString)) {
|
||||
throw new Error(`All targets require a keyString to create an annotation`);
|
||||
}
|
||||
|
||||
if (!targetDomainObjects.length) {
|
||||
if (!Object.keys(targetDomainObjects).length) {
|
||||
throw new Error(`At least one targetDomainObject is required to create an annotation`);
|
||||
}
|
||||
|
||||
@@ -185,7 +181,7 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
const success = await this.openmct.objects.save(createdObject);
|
||||
if (success) {
|
||||
this.emit('annotationCreated', createdObject);
|
||||
targetDomainObjects.forEach((targetDomainObject) => {
|
||||
Object.values(targetDomainObjects).forEach((targetDomainObject) => {
|
||||
this.#updateAnnotationModified(targetDomainObject);
|
||||
});
|
||||
|
||||
@@ -325,10 +321,7 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
}
|
||||
|
||||
#addTagMetaInformationToTags(tags) {
|
||||
// Convert to Set and back to Array to remove duplicates
|
||||
const uniqueTags = [...new Set(tags)];
|
||||
|
||||
return uniqueTags.map((tagKey) => {
|
||||
return tags.map((tagKey) => {
|
||||
const tagModel = this.availableTags[tagKey];
|
||||
tagModel.tagID = tagKey;
|
||||
|
||||
@@ -370,8 +363,7 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
const modelAddedToResults = await Promise.all(
|
||||
results.map(async (result) => {
|
||||
const targetModels = await Promise.all(
|
||||
result.targets.map(async (target) => {
|
||||
const targetID = target.keyString;
|
||||
Object.keys(result.targets).map(async (targetID) => {
|
||||
const targetModel = await this.openmct.objects.get(targetID);
|
||||
const targetKeyString = this.openmct.objects.makeKeyString(targetModel.identifier);
|
||||
const originalPathObjects = await this.openmct.objects.getOriginalPath(targetKeyString);
|
||||
@@ -418,12 +410,13 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
#breakApartSeparateTargets(results) {
|
||||
const separateResults = [];
|
||||
results.forEach((result) => {
|
||||
result.targets.forEach((target) => {
|
||||
const targetID = target.keyString;
|
||||
Object.keys(result.targets).forEach((targetID) => {
|
||||
const separatedResult = {
|
||||
...result
|
||||
};
|
||||
separatedResult.targets = [target];
|
||||
separatedResult.targets = {
|
||||
[targetID]: result.targets[targetID]
|
||||
};
|
||||
separatedResult.targetModels = result.targetModels.filter((targetModel) => {
|
||||
const targetKeyString = this.openmct.objects.makeKeyString(targetModel.identifier);
|
||||
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import ExampleTagsPlugin from '../../../example/exampleTags/plugin';
|
||||
import { createOpenMct, resetApplicationState } from '../../utils/testing';
|
||||
import ExampleTagsPlugin from '../../../example/exampleTags/plugin';
|
||||
|
||||
describe('The Annotation API', () => {
|
||||
let openmct;
|
||||
@@ -62,12 +62,11 @@ describe('The Annotation API', () => {
|
||||
key: 'anAnnotationKey',
|
||||
namespace: 'fooNameSpace'
|
||||
},
|
||||
targets: [
|
||||
{
|
||||
keyString: 'fooNameSpace:some-object',
|
||||
targets: {
|
||||
'fooNameSpace:some-object': {
|
||||
entryId: 'fooBarEntry'
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
mockObjectProvider = jasmine.createSpyObj('mock provider', ['create', 'update', 'get']);
|
||||
@@ -122,7 +121,7 @@ describe('The Annotation API', () => {
|
||||
tags: ['sometag'],
|
||||
contentText: 'fooContext',
|
||||
targetDomainObjects: [mockDomainObject],
|
||||
targets: [{ keyString: 'fooTarget' }]
|
||||
targets: { fooTarget: {} }
|
||||
};
|
||||
const annotationObject = await openmct.annotation.create(annotationCreationArguments);
|
||||
expect(annotationObject).toBeDefined();
|
||||
@@ -137,7 +136,7 @@ describe('The Annotation API', () => {
|
||||
tags: ['sometag'],
|
||||
contentText: 'fooContext',
|
||||
targetDomainObjects: [mockDomainObject],
|
||||
targets: [{ keyString: 'fooTarget' }]
|
||||
targets: { fooTarget: {} }
|
||||
};
|
||||
openmct.annotation.setNamespaceToSaveAnnotations('fooNameSpace');
|
||||
const annotationObject = await openmct.annotation.create(annotationCreationArguments);
|
||||
@@ -167,7 +166,7 @@ describe('The Annotation API', () => {
|
||||
tags: ['sometag'],
|
||||
contentText: 'fooContext',
|
||||
targetDomainObjects: [mockDomainObject],
|
||||
targets: [{ keyString: 'fooTarget' }]
|
||||
targets: { fooTarget: {} }
|
||||
};
|
||||
openmct.annotation.setNamespaceToSaveAnnotations('namespaceThatDoesNotExist');
|
||||
await openmct.annotation.create(annotationCreationArguments);
|
||||
@@ -184,7 +183,7 @@ describe('The Annotation API', () => {
|
||||
tags: ['sometag'],
|
||||
contentText: 'fooContext',
|
||||
targetDomainObjects: [mockDomainObject],
|
||||
targets: [{ keyString: 'fooTarget' }]
|
||||
targets: { fooTarget: {} }
|
||||
};
|
||||
openmct.annotation.setNamespaceToSaveAnnotations('immutableProvider');
|
||||
await openmct.annotation.create(annotationCreationArguments);
|
||||
@@ -203,7 +202,7 @@ describe('The Annotation API', () => {
|
||||
annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,
|
||||
tags: ['aWonderfulTag'],
|
||||
contentText: 'fooContext',
|
||||
targets: [{ keyString: 'fooNameSpace:some-object', entryId: 'fooBarEntry' }],
|
||||
targets: { 'fooNameSpace:some-object': { entryId: 'fooBarEntry' } },
|
||||
targetDomainObjects: [mockDomainObject]
|
||||
};
|
||||
});
|
||||
@@ -273,19 +272,17 @@ describe('The Annotation API', () => {
|
||||
let comparator;
|
||||
|
||||
beforeEach(() => {
|
||||
targets = [
|
||||
{
|
||||
keyString: 'fooTarget',
|
||||
targets = {
|
||||
fooTarget: {
|
||||
foo: 42
|
||||
}
|
||||
];
|
||||
otherTargets = [
|
||||
{
|
||||
keyString: 'fooTarget',
|
||||
};
|
||||
otherTargets = {
|
||||
fooTarget: {
|
||||
bar: 42
|
||||
}
|
||||
];
|
||||
comparator = (t1, t2) => t1[0].foo === t2[0].bar;
|
||||
};
|
||||
comparator = (t1, t2) => t1.fooTarget.foo === t2.fooTarget.bar;
|
||||
});
|
||||
|
||||
it('can add a comparator function', () => {
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import CompositionCollection from './CompositionCollection';
|
||||
import DefaultCompositionProvider from './DefaultCompositionProvider';
|
||||
import CompositionCollection from './CompositionCollection';
|
||||
|
||||
/**
|
||||
* @typedef {import('./CompositionProvider').default} CompositionProvider
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import _ from 'lodash';
|
||||
|
||||
import objectUtils from '../objects/object-utils';
|
||||
|
||||
/**
|
||||
@@ -186,10 +185,9 @@ export default class CompositionProvider {
|
||||
return;
|
||||
}
|
||||
|
||||
const onMutation = this.#onMutation.bind(this);
|
||||
this.#publicAPI.objects.eventEmitter.on('mutation', onMutation);
|
||||
this.#publicAPI.objects.eventEmitter.on('mutation', this.#onMutation.bind(this));
|
||||
this.topicListener = () => {
|
||||
this.#publicAPI.objects.eventEmitter.off('mutation', onMutation);
|
||||
this.#publicAPI.objects.eventEmitter.off('mutation', this.#onMutation.bind(this));
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -19,10 +19,9 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import { toRaw } from 'vue';
|
||||
|
||||
import objectUtils from '../objects/object-utils';
|
||||
import CompositionProvider from './CompositionProvider';
|
||||
import { toRaw } from 'vue';
|
||||
|
||||
/**
|
||||
* @typedef {import('../objects/ObjectAPI').DomainObject} DomainObject
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
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/DatetimeField.vue';
|
||||
import CheckBoxField from './components/controls/CheckBoxField.vue';
|
||||
import Datetime from './components/controls/Datetime.vue';
|
||||
import FileInput from './components/controls/FileInput.vue';
|
||||
import Locator from './components/controls/LocatorField.vue';
|
||||
import Locator from './components/controls/Locator.vue';
|
||||
import NumberField from './components/controls/NumberField.vue';
|
||||
import SelectField from './components/controls/SelectField.vue';
|
||||
import TextAreaField from './components/controls/TextAreaField.vue';
|
||||
import TextField from './components/controls/TextField.vue';
|
||||
import ToggleSwitchField from './components/controls/ToggleSwitchField.vue';
|
||||
|
||||
import mount from 'utils/mount';
|
||||
export const DEFAULT_CONTROLS_MAP = {
|
||||
autocomplete: AutoCompleteField,
|
||||
checkbox: CheckBoxField,
|
||||
@@ -87,7 +87,7 @@ export default class FormControl {
|
||||
onChange
|
||||
};
|
||||
},
|
||||
template: `<FormControlComponent :model="model" @on-change="onChange"></FormControlComponent>`
|
||||
template: `<FormControlComponent :model="model" @onChange="onChange"></FormControlComponent>`
|
||||
},
|
||||
{
|
||||
element,
|
||||
|
||||
@@ -20,12 +20,12 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import FormController from './FormController';
|
||||
import FormProperties from './components/FormProperties.vue';
|
||||
|
||||
import _ from 'lodash';
|
||||
import mount from 'utils/mount';
|
||||
|
||||
import FormProperties from './components/FormProperties.vue';
|
||||
import FormController from './FormController';
|
||||
|
||||
export default class FormsAPI {
|
||||
constructor(openmct) {
|
||||
this.openmct = openmct;
|
||||
@@ -171,7 +171,7 @@ export default class FormsAPI {
|
||||
};
|
||||
},
|
||||
template:
|
||||
'<FormProperties :model="formStructure" @on-change="onChange" @on-cancel="onCancel" @on-save="onSave"></FormProperties>'
|
||||
'<FormProperties :model="formStructure" @onChange="onChange" @onCancel="onCancel" @onSave="onSave"></FormProperties>'
|
||||
},
|
||||
{
|
||||
element,
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
:css-class="row.cssClass"
|
||||
:first="index < 1"
|
||||
:row="row"
|
||||
@on-change="onChange"
|
||||
@onChange="onChange"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
@@ -73,9 +73,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import FormRow from '@/api/forms/components/FormRow.vue';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -94,7 +93,6 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
emits: ['on-change', 'on-save', 'on-cancel'],
|
||||
data() {
|
||||
return {
|
||||
invalidProperties: {},
|
||||
@@ -145,13 +143,13 @@ export default {
|
||||
onChange(data) {
|
||||
this.invalidProperties[data.model.key] = data.invalid;
|
||||
|
||||
this.$emit('on-change', data);
|
||||
this.$emit('onChange', data);
|
||||
},
|
||||
onCancel() {
|
||||
this.$emit('on-cancel');
|
||||
this.$emit('onCancel');
|
||||
},
|
||||
onSave() {
|
||||
this.$emit('on-save');
|
||||
this.$emit('onSave');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="form-row c-form__row" :class="[{ first: first }, cssClass]" @on-change="onChange">
|
||||
<div class="form-row c-form__row" :class="[{ first: first }, cssClass]" @onChange="onChange">
|
||||
<label class="c-form-row__label" :title="row.description" :for="`form-${row.key}`">
|
||||
{{ row.name }}
|
||||
</label>
|
||||
@@ -51,7 +51,6 @@ export default {
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: ['on-change'],
|
||||
data() {
|
||||
return {
|
||||
formControl: this.openmct.forms.getFormControl(this.row.control),
|
||||
@@ -102,7 +101,7 @@ export default {
|
||||
this.valid = this.validateRow(data);
|
||||
data.invalid = !this.valid;
|
||||
|
||||
this.$emit('on-change', data);
|
||||
this.$emit('onChange', data);
|
||||
},
|
||||
validateRow(data) {
|
||||
let valid = true;
|
||||
|
||||
@@ -88,7 +88,6 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
emits: ['on-change'],
|
||||
data() {
|
||||
return {
|
||||
hideOptions: true,
|
||||
@@ -139,7 +138,7 @@ export default {
|
||||
value: newValue
|
||||
};
|
||||
|
||||
this.$emit('on-change', data);
|
||||
this.$emit('onChange', data);
|
||||
}
|
||||
},
|
||||
hideOptions(newValue) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user