Compare commits
1 Commits
notebook-c
...
include-e2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ee61c280f |
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -21,9 +21,9 @@ Closes <!--- Insert Issue Number(s) this PR addresses. Start by typing # will op
|
||||
### Reviewer Checklist
|
||||
|
||||
* [ ] Changes appear to address issue?
|
||||
* [ ] Reviewer has tested changes by following the provided instructions?
|
||||
* [ ] Changes appear not to be breaking changes?
|
||||
* [ ] Appropriate automated tests included?
|
||||
* [ ] Appropriate unit tests included?
|
||||
* [ ] Code style and in-line documentation are appropriate?
|
||||
* [ ] Commit messages meet standards?
|
||||
* [ ] Has associated issue been labelled unverified? (only applicable if this PR closes the issue)
|
||||
* [ ] Has associated issue been labelled bug? (only applicable if this PR is for a bug fix)
|
||||
|
||||
1
.github/codeql/codeql-config.yml
vendored
@@ -1 +0,0 @@
|
||||
name: 'Custom CodeQL config'
|
||||
12
.github/dependabot.yml
vendored
@@ -13,18 +13,14 @@ updates:
|
||||
- "pr:daveit"
|
||||
- "pr:platform"
|
||||
ignore:
|
||||
#We have to source the playwright container which is not detected by Dependabot
|
||||
#We have to source the container which is not detected by Dependabot
|
||||
- dependency-name: "@playwright/test"
|
||||
- dependency-name: "playwright-core"
|
||||
#Lots of noise in these type patch releases.
|
||||
#Lots of noise in these type patch releases.
|
||||
- dependency-name: "@babel/eslint-parser"
|
||||
update-types: ["version-update:semver-patch"]
|
||||
update-types: ["version-update:semver-patch"]
|
||||
- dependency-name: "eslint-plugin-vue"
|
||||
update-types: ["version-update:semver-patch"]
|
||||
- dependency-name: "babel-loader"
|
||||
update-types: ["version-update:semver-patch"]
|
||||
- dependency-name: "sinon"
|
||||
update-types: ["version-update:semver-patch"]
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
|
||||
31
.github/workflows/codeql-analysis.yml
vendored
@@ -1,10 +1,11 @@
|
||||
name: 'CodeQL'
|
||||
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master, 'release/*']
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [master, 'release/*']
|
||||
branches: [ master ]
|
||||
paths-ignore:
|
||||
- '**/*Spec.js'
|
||||
- '**/*.md'
|
||||
@@ -26,19 +27,17 @@ jobs:
|
||||
security-events: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
config-file: ./.github/codeql/codeql-config.yml
|
||||
languages: javascript
|
||||
queries: security-and-quality
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: javascript
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
|
||||
98
.github/workflows/lighthouse.yml
vendored
Normal file
@@ -0,0 +1,98 @@
|
||||
name: lighthouse
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Which branch do you want to test?' # Limited to branch for now
|
||||
required: false
|
||||
default: 'master'
|
||||
pull_request:
|
||||
types:
|
||||
- labeled
|
||||
jobs:
|
||||
lighthouse-pr:
|
||||
if: ${{ github.event.label.name == 'pr:lighthouse' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Master for Baseline
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: master #explicitly checkout master for baseline
|
||||
- name: Install Node 16
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v2
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
|
||||
- name: npm install with lighthouse cli
|
||||
run: npm install && npm install -g @lhci/cli
|
||||
- name: Run lhci against master to generate baseline and ignore exit codes
|
||||
run: lhci autorun || true
|
||||
- name: Perform clean checkout of PR
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: true
|
||||
- name: Install Node version which is compatible with PR
|
||||
uses: actions/setup-node@v3
|
||||
- name: npm install with lighthouse cli
|
||||
run: npm install && npm install -g @lhci/cli
|
||||
- name: Run lhci with PR
|
||||
run: lhci autorun
|
||||
env:
|
||||
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
|
||||
lighthouse-nightly:
|
||||
if: ${{ github.event.schedule }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Node 16
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v2
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
|
||||
- name: npm install with lighthouse cli
|
||||
run: npm install && npm install -g @lhci/cli
|
||||
- name: Run lhci against master to generate baseline
|
||||
run: lhci autorun
|
||||
env:
|
||||
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
|
||||
lighthouse-dispatch:
|
||||
if: ${{ github.event.workflow_dispatch }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.inputs.version }}
|
||||
- name: Install Node 14
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v3
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
|
||||
- name: npm install with lighthouse cli
|
||||
run: npm install && npm install -g @lhci/cli
|
||||
- name: Run lhci against master to generate baseline
|
||||
run: lhci autorun
|
||||
|
||||
8
.github/workflows/npm-prerelease.yml
vendored
@@ -16,11 +16,7 @@ jobs:
|
||||
with:
|
||||
node-version: 16
|
||||
- run: npm install
|
||||
- run: |
|
||||
echo "//registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN" >> ~/.npmrc
|
||||
npm whoami
|
||||
npm publish --access=public --tag unstable openmct
|
||||
# - run: npm test
|
||||
- run: npm test
|
||||
|
||||
publish-npm-prerelease:
|
||||
needs: build
|
||||
@@ -32,6 +28,6 @@ jobs:
|
||||
node-version: 16
|
||||
registry-url: https://registry.npmjs.org/
|
||||
- run: npm install
|
||||
- run: npm publish --access=public --tag unstable
|
||||
- run: npm publish --access public --tag unstable
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
12
.npmignore
@@ -10,21 +10,21 @@
|
||||
# https://github.com/nasa/openmct/issues/4992
|
||||
!/example/**/*
|
||||
|
||||
# We will remove this in https://github.com/nasa/openmct/issues/4922
|
||||
!/app.js
|
||||
|
||||
# ...except for these files in the above folders.
|
||||
/src/**/*Spec.js
|
||||
/src/**/test/
|
||||
# TODO move test utils into test/ folders
|
||||
/src/utils/testing.js
|
||||
|
||||
# Include e2e suite for openmct-e2e-testing-as-a-dependency
|
||||
!/e2e/
|
||||
|
||||
# Also include these special top-level files.
|
||||
!copyright-notice.js
|
||||
!copyright-notice.html
|
||||
!index.html
|
||||
!openmct.js
|
||||
!SECURITY.md
|
||||
|
||||
# Add e2e tests to npm package
|
||||
!/e2e/**/*
|
||||
|
||||
# ... except our test-data folder files.
|
||||
/e2e/test-data/*.json
|
||||
|
||||
12
README.md
@@ -30,8 +30,6 @@ Building and running Open MCT in your local dev environment is very easy. Be sur
|
||||
|
||||
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/).
|
||||
|
||||
## Documentation
|
||||
|
||||
Documentation is available on the [Open MCT website](https://nasa.github.io/openmct/documentation/).
|
||||
@@ -45,9 +43,11 @@ our documentation.
|
||||
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
|
||||
## Building Applications With Open MCT
|
||||
|
||||
For more on developing with Open MCT, see our documentation for a guide on [Developing Applications with Open MCT](./API.md#starting-an-open-mct-application).
|
||||
Open MCT is built using [`npm`](http://npmjs.com/) and [`webpack`](https://webpack.js.org/).
|
||||
|
||||
See our documentation for a guide on [building Applications with Open MCT](https://github.com/nasa/openmct/blob/master/API.md#starting-an-open-mct-application).
|
||||
|
||||
## Compatibility
|
||||
|
||||
@@ -64,7 +64,7 @@ that is intended to be added or removed as a single unit.
|
||||
As well as providing an extension mechanism, most of the core Open MCT codebase is also
|
||||
written as plugins.
|
||||
|
||||
For information on writing plugins, please see [our API documentation](./API.md#plugins).
|
||||
For information on writing plugins, please see [our API documentation](https://github.com/nasa/openmct/blob/master/API.md#plugins).
|
||||
|
||||
## Tests
|
||||
|
||||
@@ -100,7 +100,7 @@ To run the performance tests:
|
||||
The test suite is configured to all tests localed in `e2e/tests/` ending in `*.e2e.spec.js`. For more about the e2e test suite, please see the [README](./e2e/README.md)
|
||||
|
||||
### Security Tests
|
||||
Each commit is analyzed for known security vulnerabilities using [CodeQL](https://codeql.github.com/docs/codeql-language-guides/codeql-library-for-javascript/) and our overall security report is available on [LGTM](https://lgtm.com/projects/g/nasa/openmct/). The list of CWE coverage items is avaiable in the [CodeQL docs](https://codeql.github.com/codeql-query-help/javascript-cwe/). The CodeQL workflow is specified in the [CodeQL analysis file](./.github/workflows/codeql-analysis.yml) and the custom [CodeQL config](./.github/codeql/codeql-config.yml).
|
||||
Each commit is analyzed for known security vulnerabilities using [CodeQL](https://codeql.github.com/docs/codeql-language-guides/codeql-library-for-javascript/) and our overall security report is available on [LGTM](https://lgtm.com/projects/g/nasa/openmct/)
|
||||
|
||||
### Test Reporting and Code Coverage
|
||||
|
||||
|
||||
92
app.js
Normal file
@@ -0,0 +1,92 @@
|
||||
/*global process*/
|
||||
|
||||
/**
|
||||
* Usage:
|
||||
*
|
||||
* npm install minimist express
|
||||
* node app.js [options]
|
||||
*/
|
||||
|
||||
const options = require('minimist')(process.argv.slice(2));
|
||||
const express = require('express');
|
||||
const app = express();
|
||||
const fs = require('fs');
|
||||
const request = require('request');
|
||||
const __DEV__ = !process.env.NODE_ENV || process.env.NODE_ENV === 'development';
|
||||
|
||||
// Defaults
|
||||
options.port = options.port || options.p || 8080;
|
||||
options.host = options.host || 'localhost';
|
||||
options.directory = options.directory || options.D || '.';
|
||||
|
||||
// Show command line options
|
||||
if (options.help || options.h) {
|
||||
console.log("\nUsage: node app.js [options]\n");
|
||||
console.log("Options:");
|
||||
console.log(" --help, -h Show this message.");
|
||||
console.log(" --port, -p <number> Specify port.");
|
||||
console.log(" --directory, -D <bundle> Serve files from specified directory.");
|
||||
console.log("");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
app.disable('x-powered-by');
|
||||
|
||||
app.use('/proxyUrl', function proxyRequest(req, res, next) {
|
||||
console.log('Proxying request to: ', req.query.url);
|
||||
req.pipe(request({
|
||||
url: req.query.url,
|
||||
strictSSL: false
|
||||
}).on('error', next)).pipe(res);
|
||||
});
|
||||
|
||||
class WatchRunPlugin {
|
||||
apply(compiler) {
|
||||
compiler.hooks.emit.tapAsync('WatchRunPlugin', (compilation, callback) => {
|
||||
console.log('Begin compile at ' + new Date());
|
||||
callback();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const webpack = require('webpack');
|
||||
let webpackConfig;
|
||||
if (__DEV__) {
|
||||
webpackConfig = require('./webpack.dev');
|
||||
webpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin());
|
||||
webpackConfig.entry.openmct = [
|
||||
'webpack-hot-middleware/client?reload=true',
|
||||
webpackConfig.entry.openmct
|
||||
];
|
||||
webpackConfig.plugins.push(new WatchRunPlugin());
|
||||
} else {
|
||||
webpackConfig = require('./webpack.coverage');
|
||||
}
|
||||
|
||||
const compiler = webpack(webpackConfig);
|
||||
|
||||
app.use(require('webpack-dev-middleware')(
|
||||
compiler,
|
||||
{
|
||||
publicPath: '/dist',
|
||||
stats: 'errors-warnings'
|
||||
}
|
||||
));
|
||||
|
||||
if (__DEV__) {
|
||||
app.use(require('webpack-hot-middleware')(
|
||||
compiler,
|
||||
{}
|
||||
));
|
||||
}
|
||||
|
||||
// Expose index.html for development users.
|
||||
app.get('/', function (req, res) {
|
||||
fs.createReadStream('index.html').pipe(res);
|
||||
});
|
||||
|
||||
// Finally, open the HTTP server and log the instance to the console
|
||||
app.listen(options.port, options.host, function () {
|
||||
console.log('Open MCT application running at %s:%s', options.host, options.port);
|
||||
});
|
||||
|
||||
3
docs/footer.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<hr>
|
||||
</body>
|
||||
</html>
|
||||
209
docs/gendocs.js
Normal file
@@ -0,0 +1,209 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, 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.
|
||||
*****************************************************************************/
|
||||
|
||||
/*global require,process,__dirname,GLOBAL*/
|
||||
/*jslint nomen: false */
|
||||
|
||||
|
||||
// Usage:
|
||||
// node gendocs.js --in <source directory> --out <dest directory>
|
||||
|
||||
var CONSTANTS = {
|
||||
DIAGRAM_WIDTH: 800,
|
||||
DIAGRAM_HEIGHT: 500
|
||||
},
|
||||
TOC_HEAD = "# Table of Contents";
|
||||
|
||||
GLOBAL.window = GLOBAL.window || GLOBAL; // nomnoml expects window to be defined
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var fs = require("fs"),
|
||||
mkdirp = require("mkdirp"),
|
||||
path = require("path"),
|
||||
glob = require("glob"),
|
||||
marked = require("marked"),
|
||||
split = require("split"),
|
||||
stream = require("stream"),
|
||||
nomnoml = require('nomnoml'),
|
||||
toc = require("markdown-toc"),
|
||||
Canvas = require('canvas'),
|
||||
header = fs.readFileSync(path.resolve(__dirname, 'header.html')),
|
||||
footer = fs.readFileSync(path.resolve(__dirname, 'footer.html')),
|
||||
options = require("minimist")(process.argv.slice(2));
|
||||
|
||||
// Convert from nomnoml source to a target PNG file.
|
||||
function renderNomnoml(source, target) {
|
||||
var canvas =
|
||||
new Canvas(CONSTANTS.DIAGRAM_WIDTH, CONSTANTS.DIAGRAM_HEIGHT);
|
||||
nomnoml.draw(canvas, source, 1.0);
|
||||
canvas.pngStream().pipe(fs.createWriteStream(target));
|
||||
}
|
||||
|
||||
// Stream transform.
|
||||
// Pulls out nomnoml diagrams from fenced code blocks and renders them
|
||||
// as PNG files in the output directory, prefixed with a provided name.
|
||||
// The fenced code blocks will be replaced with Markdown in the
|
||||
// output of this stream.
|
||||
function nomnomlifier(outputDirectory, prefix) {
|
||||
var transform = new stream.Transform({ objectMode: true }),
|
||||
isBuilding = false,
|
||||
counter = 1,
|
||||
outputPath,
|
||||
source = "";
|
||||
|
||||
transform._transform = function (chunk, encoding, done) {
|
||||
if (!isBuilding) {
|
||||
if (chunk.trim().indexOf("```nomnoml") === 0) {
|
||||
var outputFilename = prefix + '-' + counter + '.png';
|
||||
outputPath = path.join(outputDirectory, outputFilename);
|
||||
this.push([
|
||||
"\n\n\n"
|
||||
].join(""));
|
||||
isBuilding = true;
|
||||
source = "";
|
||||
counter += 1;
|
||||
} else {
|
||||
// Otherwise, pass through
|
||||
this.push(chunk + '\n');
|
||||
}
|
||||
} else {
|
||||
if (chunk.trim() === "```") {
|
||||
// End nomnoml
|
||||
renderNomnoml(source, outputPath);
|
||||
isBuilding = false;
|
||||
} else {
|
||||
source += chunk + '\n';
|
||||
}
|
||||
}
|
||||
done();
|
||||
};
|
||||
|
||||
return transform;
|
||||
}
|
||||
|
||||
// Convert from Github-flavored Markdown to HTML
|
||||
function gfmifier(renderTOC) {
|
||||
var transform = new stream.Transform({ objectMode: true }),
|
||||
markdown = "";
|
||||
transform._transform = function (chunk, encoding, done) {
|
||||
markdown += chunk;
|
||||
done();
|
||||
};
|
||||
transform._flush = function (done) {
|
||||
if (renderTOC){
|
||||
// Prepend table of contents
|
||||
markdown =
|
||||
[ TOC_HEAD, toc(markdown).content, "", markdown ].join("\n");
|
||||
}
|
||||
this.push(header);
|
||||
this.push(marked(markdown));
|
||||
this.push(footer);
|
||||
done();
|
||||
};
|
||||
return transform;
|
||||
}
|
||||
|
||||
// Custom renderer for marked; converts relative links from md to html,
|
||||
// and makes headings linkable.
|
||||
function CustomRenderer() {
|
||||
var renderer = new marked.Renderer(),
|
||||
customRenderer = Object.create(renderer);
|
||||
customRenderer.heading = function (text, level) {
|
||||
var escapedText = (text || "").trim().toLowerCase().replace(/\W/g, "-"),
|
||||
aOpen = "<a name=\"" + escapedText + "\" href=\"#" + escapedText + "\">",
|
||||
aClose = "</a>";
|
||||
return aOpen + renderer.heading.apply(renderer, arguments) + aClose;
|
||||
};
|
||||
// Change links to .md files to .html
|
||||
customRenderer.link = function (href, title, text) {
|
||||
// ...but only if they look like relative paths
|
||||
return (href || "").indexOf(":") === -1 && href[0] !== "/" ?
|
||||
renderer.link(href.replace(/\.md/, ".html"), title, text) :
|
||||
renderer.link.apply(renderer, arguments);
|
||||
};
|
||||
return customRenderer;
|
||||
}
|
||||
|
||||
options['in'] = options['in'] || options.i;
|
||||
options.out = options.out || options.o;
|
||||
|
||||
marked.setOptions({
|
||||
renderer: new CustomRenderer(),
|
||||
gfm: true,
|
||||
tables: true,
|
||||
breaks: false,
|
||||
pedantic: false,
|
||||
sanitize: true,
|
||||
smartLists: true,
|
||||
smartypants: false
|
||||
});
|
||||
|
||||
// Convert all markdown files.
|
||||
// First, pull out nomnoml diagrams.
|
||||
// Then, convert remaining Markdown to HTML.
|
||||
glob(options['in'] + "/**/*.md", {}, function (err, files) {
|
||||
files.forEach(function (file) {
|
||||
var destination = file.replace(options['in'], options.out)
|
||||
.replace(/md$/, "html"),
|
||||
destPath = path.dirname(destination),
|
||||
prefix = path.basename(destination).replace(/\.html$/, ""),
|
||||
//Determine whether TOC should be rendered for this file based
|
||||
//on regex provided as command line option
|
||||
renderTOC = file.match(options['suppress-toc'] || "") === null;
|
||||
|
||||
mkdirp(destPath, function (err) {
|
||||
fs.createReadStream(file, { encoding: 'utf8' })
|
||||
.pipe(split())
|
||||
.pipe(nomnomlifier(destPath, prefix))
|
||||
.pipe(gfmifier(renderTOC))
|
||||
.pipe(fs.createWriteStream(destination, {
|
||||
encoding: 'utf8'
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Also copy over all HTML, CSS, or PNG files
|
||||
glob(options['in'] + "/**/*.@(html|css|png)", {}, function (err, files) {
|
||||
files.forEach(function (file) {
|
||||
var destination = file.replace(options['in'], options.out),
|
||||
destPath = path.dirname(destination),
|
||||
streamOptions = {};
|
||||
if (file.match(/png$/)) {
|
||||
streamOptions.encoding = null;
|
||||
} else {
|
||||
streamOptions.encoding = 'utf8';
|
||||
}
|
||||
|
||||
mkdirp(destPath, function (err) {
|
||||
fs.createReadStream(file, streamOptions)
|
||||
.pipe(fs.createWriteStream(destination, streamOptions));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
}());
|
||||
9
docs/header.html
Normal file
@@ -0,0 +1,9 @@
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet"
|
||||
href="//nasa.github.io/openmct/static/res/css/styles.css">
|
||||
<link rel="stylesheet"
|
||||
href="//nasa.github.io/openmct/static/res/css/documentation.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
|
||||
## Sections
|
||||
|
||||
* The [API](api/) uses inline documentation
|
||||
using [TypeScript](https://www.typescriptlang.org) and some legacy [JSDoc](https://jsdoc.app/). It describes the JavaScript objects and
|
||||
* The [API](api/) document is generated from inline documentation
|
||||
using [JSDoc](http://usejsdoc.org/), and describes the JavaScript objects and
|
||||
functions that make up the software platform.
|
||||
|
||||
* The [Development Process](process/) document describes the
|
||||
|
||||
@@ -151,7 +151,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`.
|
||||
- `@addInit` - Initializes the browser with an injected and artificial state. Useful for loading non-default plugins. Likely will not work outside of app.js.
|
||||
- `@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.
|
||||
|
||||
@@ -46,7 +46,6 @@
|
||||
*/
|
||||
|
||||
const Buffer = require('buffer').Buffer;
|
||||
const genUuid = require('uuid').v4;
|
||||
|
||||
/**
|
||||
* This common function creates a domain object with the default options. It is the preferred way of creating objects
|
||||
@@ -57,10 +56,6 @@ const genUuid = require('uuid').v4;
|
||||
* @returns {Promise<CreatedObjectInfo>} An object containing information about the newly created domain object.
|
||||
*/
|
||||
async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine' }) {
|
||||
if (!name) {
|
||||
name = `${type}:${genUuid()}`;
|
||||
}
|
||||
|
||||
const parentUrl = await getHashUrlToDomainObject(page, parent);
|
||||
|
||||
// Navigate to the parent object. This is necessary to create the object
|
||||
@@ -75,14 +70,11 @@ async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine
|
||||
await page.click(`li:text("${type}")`);
|
||||
|
||||
// Modify the name input field of the domain object to accept 'name'
|
||||
const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]');
|
||||
await nameInput.fill("");
|
||||
await nameInput.fill(name);
|
||||
|
||||
// Fill the "Notes" section with information about the
|
||||
// currently running test and its project.
|
||||
const notesInput = page.locator('form[name="mctForm"] #notes-textarea');
|
||||
await notesInput.fill(page.testNotes);
|
||||
if (name) {
|
||||
const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]');
|
||||
await nameInput.fill("");
|
||||
await nameInput.fill(name);
|
||||
}
|
||||
|
||||
// Click OK button and wait for Navigate event
|
||||
await Promise.all([
|
||||
@@ -104,8 +96,8 @@ async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
uuid,
|
||||
name: name || `Unnamed ${type}`,
|
||||
uuid: uuid,
|
||||
url: objectUrl
|
||||
};
|
||||
}
|
||||
@@ -233,14 +225,15 @@ async function getHashUrlToDomainObject(page, uuid) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Utilizes the OpenMCT API to detect if the UI is in Edit mode.
|
||||
* Utilizes the OpenMCT API to detect if the given object has an active transaction (is in Edit mode).
|
||||
* @private
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @return {Promise<boolean>} true if the Open MCT is in Edit Mode
|
||||
* @param {string | import('../src/api/objects/ObjectAPI').Identifier} identifier
|
||||
* @return {Promise<boolean>} true if the object has an active transaction, false otherwise
|
||||
*/
|
||||
async function _isInEditMode(page, identifier) {
|
||||
// eslint-disable-next-line no-return-await
|
||||
return await page.evaluate(() => window.openmct.editor.isEditing());
|
||||
return await page.evaluate((objectIdentifier) => window.openmct.objects.isTransactionActive(objectIdentifier), identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,8 +20,6 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
const { createDomainObjectWithDefaults } = require('../appActions');
|
||||
|
||||
const NOTEBOOK_DROP_AREA = '.c-notebook__drag-area';
|
||||
|
||||
/**
|
||||
@@ -40,17 +38,24 @@ async function enterTextEntry(page, text) {
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function dragAndDropEmbed(page, notebookObject) {
|
||||
// Create example telemetry object
|
||||
const swg = await createDomainObjectWithDefaults(page, {
|
||||
type: "Sine Wave Generator"
|
||||
});
|
||||
// Navigate to notebook
|
||||
await page.goto(notebookObject.url);
|
||||
// Expand the tree to reveal the notebook
|
||||
await page.click('button[title="Show selected item in tree"]');
|
||||
// Drag and drop the SWG into the notebook
|
||||
await page.dragAndDrop(`text=${swg.name}`, NOTEBOOK_DROP_AREA);
|
||||
async function dragAndDropEmbed(page, myItemsFolderName) {
|
||||
// Click button:has-text("Create")
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
// Click li:has-text("Sine Wave Generator")
|
||||
await page.locator('li:has-text("Sine Wave Generator")').click();
|
||||
// Click form[name="mctForm"] >> text=My Items
|
||||
await page.locator(`form[name="mctForm"] >> text=${myItemsFolderName}`).click();
|
||||
// Click text=OK
|
||||
await page.locator('text=OK').click();
|
||||
// Click text=Open MCT My Items >> span >> nth=3
|
||||
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
|
||||
// Click text=Unnamed CUSTOM_NAME
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=Unnamed CUSTOM_NAME').click()
|
||||
]);
|
||||
|
||||
await page.dragAndDrop('text=UNNAMED SINE WAVE GENERATOR', NOTEBOOK_DROP_AREA);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
|
||||
@@ -14,7 +14,7 @@ const config = {
|
||||
testIgnore: '**/*.perf.spec.js', //Ignore performance tests and define in playwright-perfromance.config.js
|
||||
timeout: 60 * 1000,
|
||||
webServer: {
|
||||
command: 'npm run start:coverage',
|
||||
command: 'cross-env NODE_ENV=test npm run start',
|
||||
url: 'http://localhost:8080/#',
|
||||
timeout: 200 * 1000,
|
||||
reuseExistingServer: false
|
||||
|
||||
@@ -12,7 +12,10 @@ const config = {
|
||||
testIgnore: '**/*.perf.spec.js',
|
||||
timeout: 30 * 1000,
|
||||
webServer: {
|
||||
command: 'npm run start:coverage',
|
||||
env: {
|
||||
NODE_ENV: 'test'
|
||||
},
|
||||
command: 'npm run start',
|
||||
url: 'http://localhost:8080/#',
|
||||
timeout: 120 * 1000,
|
||||
reuseExistingServer: true
|
||||
|
||||
@@ -6,12 +6,12 @@ const CI = process.env.CI === 'true';
|
||||
|
||||
/** @type {import('@playwright/test').PlaywrightTestConfig} */
|
||||
const config = {
|
||||
retries: 1, //Only for debugging purposes for trace: 'on-first-retry'
|
||||
retries: 1, //Only for debugging purposes because trace is enabled only on first retry
|
||||
testDir: 'tests/performance/',
|
||||
timeout: 60 * 1000,
|
||||
workers: 1, //Only run in serial with 1 worker
|
||||
webServer: {
|
||||
command: 'npm run start', //coverage not generated
|
||||
command: 'cross-env NODE_ENV=test npm run start',
|
||||
url: 'http://localhost:8080/#',
|
||||
timeout: 200 * 1000,
|
||||
reuseExistingServer: !CI
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
|
||||
/** @type {import('@playwright/test').PlaywrightTestConfig<{ theme: string }>} */
|
||||
const config = {
|
||||
retries: 0, // Visual tests should never retry due to snapshot comparison errors. Leaving as a shim
|
||||
retries: 1, // visual tests should never retry due to snapshot comparison errors. Leaving as a shim
|
||||
testDir: 'tests/visual',
|
||||
testMatch: '**/*.visual.spec.js', // only run visual tests
|
||||
timeout: 60 * 1000,
|
||||
workers: 1, //Lower stress on Circle CI Agent for Visual tests https://github.com/percy/cli/discussions/1067
|
||||
webServer: {
|
||||
command: 'npm run start:coverage',
|
||||
command: 'cross-env NODE_ENV=test npm run start',
|
||||
url: 'http://localhost:8080/#',
|
||||
timeout: 200 * 1000,
|
||||
reuseExistingServer: !process.env.CI
|
||||
@@ -31,7 +31,7 @@ const config = {
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'chrome-snow-theme', //Runs the same visual tests but with snow-theme enabled
|
||||
name: 'chrome-snow-theme',
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
theme: 'snow'
|
||||
|
||||
@@ -126,21 +126,13 @@ exports.test = test.extend({
|
||||
// This should follow in the Project's configuration. Can be set to 'snow' in playwright config.js
|
||||
theme: [theme, { option: true }],
|
||||
// eslint-disable-next-line no-shadow
|
||||
page: async ({ page, theme }, use, testInfo) => {
|
||||
page: async ({ page, theme }, use) => {
|
||||
// eslint-disable-next-line playwright/no-conditional-in-test
|
||||
if (theme === 'snow') {
|
||||
//inject snow theme
|
||||
await page.addInitScript({ path: path.join(__dirname, './helper', './useSnowTheme.js') });
|
||||
}
|
||||
|
||||
// Attach info about the currently running test and its project.
|
||||
// This will be used by appActions to fill in the created
|
||||
// domain object's notes.
|
||||
page.testNotes = [
|
||||
`${testInfo.titlePath.join('\n')}`,
|
||||
`${testInfo.project.name}`
|
||||
].join('\n');
|
||||
|
||||
await use(page);
|
||||
},
|
||||
myItemsFolderName: [myItemsFolderName, { option: true }],
|
||||
@@ -148,5 +140,22 @@ exports.test = test.extend({
|
||||
openmctConfig: async ({ myItemsFolderName }, use) => {
|
||||
await use({ myItemsFolderName });
|
||||
}
|
||||
// objectCreateOptions: [objectCreateOptions, {option: true}],
|
||||
// eslint-disable-next-line no-shadow
|
||||
// domainObject: [async ({ page, objectCreateOptions }, use) => {
|
||||
// // FIXME: This is a false-positive caused by a bug in the eslint-plugin-playwright rule.
|
||||
// // eslint-disable-next-line playwright/no-conditional-in-test
|
||||
// if (objectCreateOptions === null) {
|
||||
// await use(page);
|
||||
|
||||
// return;
|
||||
// }
|
||||
|
||||
// //Go to baseURL
|
||||
// await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
// const uuid = await getOrCreateDomainObject(page, objectCreateOptions);
|
||||
// await use({ uuid });
|
||||
// }, { auto: true }]
|
||||
});
|
||||
exports.expect = expect;
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
const { test, expect } = require('../../pluginFixtures.js');
|
||||
const { test, expect } = require('../../baseFixtures.js');
|
||||
const { createDomainObjectWithDefaults } = require('../../appActions.js');
|
||||
|
||||
test.describe('AppActions', () => {
|
||||
@@ -50,11 +50,11 @@ test.describe('AppActions', () => {
|
||||
});
|
||||
|
||||
await page.goto(timer1.url, { waitUntil: 'networkidle' });
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(timer1.name);
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toHaveText('Timer Foo');
|
||||
await page.goto(timer2.url, { waitUntil: 'networkidle' });
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(timer2.name);
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toHaveText('Timer Bar');
|
||||
await page.goto(timer3.url, { waitUntil: 'networkidle' });
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(timer3.name);
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toHaveText('Timer Baz');
|
||||
});
|
||||
|
||||
await test.step('Create multiple nested objects in a row', async () => {
|
||||
@@ -74,11 +74,11 @@ test.describe('AppActions', () => {
|
||||
parent: folder2.uuid
|
||||
});
|
||||
await page.goto(folder1.url, { waitUntil: 'networkidle' });
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(folder1.name);
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toHaveText('Folder Foo');
|
||||
await page.goto(folder2.url, { waitUntil: 'networkidle' });
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(folder2.name);
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toHaveText('Folder Bar');
|
||||
await page.goto(folder3.url, { waitUntil: 'networkidle' });
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(folder3.name);
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toHaveText('Folder Baz');
|
||||
|
||||
expect(folder1.url).toBe(`${e2eFolder.url}/${folder1.uuid}`);
|
||||
expect(folder2.url).toBe(`${e2eFolder.url}/${folder1.uuid}/${folder2.uuid}`);
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
/*
|
||||
This test suite is dedicated to testing our use of the playwright framework as it
|
||||
relates to how we've extended it (i.e. ./e2e/baseFixtures.js) and assumptions made in our dev environment
|
||||
(`npm start` and ./e2e/webpack-dev-middleware.js)
|
||||
(app.js and ./e2e/webpack-dev-middleware.js)
|
||||
*/
|
||||
|
||||
const { test } = require('../../baseFixtures.js');
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
*/
|
||||
|
||||
// Structure: Some standard Imports. Please update the required pathing.
|
||||
const { test, expect } = require('../../pluginFixtures');
|
||||
const { test, expect } = require('../../baseFixtures');
|
||||
const { createDomainObjectWithDefaults } = require('../../appActions');
|
||||
|
||||
/**
|
||||
@@ -144,5 +144,5 @@ async function renameTimerFrom3DotMenu(page, timerUrl, newNameForTimer) {
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(newNameForTimer);
|
||||
|
||||
// Click Ok button to Save
|
||||
await page.locator('button:has-text("OK")').click();
|
||||
await page.locator('text=OK').click();
|
||||
}
|
||||
|
||||
@@ -43,14 +43,14 @@ test('Generate Visual Test Data @localStorage', async ({ page, context }) => {
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
|
||||
// add sine wave generator with defaults
|
||||
await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click();
|
||||
await page.locator('li:has-text("Sine Wave Generator")').click();
|
||||
|
||||
//Add a 5000 ms Delay
|
||||
await page.locator('[aria-label="Loading Delay \\(ms\\)"]').fill('5000');
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('button:has-text("OK")').click(),
|
||||
page.locator('text=OK').click(),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
@@ -58,7 +58,7 @@ test('Generate Visual Test Data @localStorage', async ({ page, context }) => {
|
||||
// focus the overlay plot
|
||||
await page.goto(overlayPlot.url);
|
||||
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText(overlayPlot.name);
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Overlay Plot');
|
||||
//Save localStorage for future test execution
|
||||
await context.storageState({ path: './e2e/test-data/VisualTestData_storage.json' });
|
||||
});
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
*
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../pluginFixtures');
|
||||
const { test, expect } = require('../../baseFixtures');
|
||||
|
||||
test.describe("CouchDB Status Indicator @couchdb", () => {
|
||||
test.use({ failOnConsoleError: false });
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
This test suite is dedicated to tests which verify the basic operations surrounding the example event generator.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../../pluginFixtures');
|
||||
const { test, expect } = require('../../../baseFixtures');
|
||||
const { createDomainObjectWithDefaults } = require('../../../appActions');
|
||||
|
||||
test.describe('Example Event Generator CRUD Operations', () => {
|
||||
|
||||
@@ -96,7 +96,7 @@ test.describe('Sine Wave Generator', () => {
|
||||
//Click text=OK
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.click('button:has-text("OK")')
|
||||
page.click('text=OK')
|
||||
]);
|
||||
|
||||
// Verify that the Sine Wave Generator is displayed and correct
|
||||
|
||||
@@ -25,7 +25,6 @@ This test suite is dedicated to tests which verify form functionality in isolati
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../baseFixtures');
|
||||
const { createDomainObjectWithDefaults } = require('../../appActions');
|
||||
const path = require('path');
|
||||
|
||||
const TEST_FOLDER = 'test folder';
|
||||
@@ -44,7 +43,7 @@ test.describe('Form Validation Behavior', () => {
|
||||
await page.press('text=Properties Title Notes >> input[type="text"]', 'Tab');
|
||||
|
||||
//Required Field Form Validation
|
||||
await expect(page.locator('button:has-text("OK")')).toBeDisabled();
|
||||
await expect(page.locator('text=OK')).toBeDisabled();
|
||||
await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(/invalid/);
|
||||
|
||||
//Correct Form Validation for missing title and trigger validation with 'Tab'
|
||||
@@ -53,13 +52,13 @@ test.describe('Form Validation Behavior', () => {
|
||||
await page.press('text=Properties Title Notes >> input[type="text"]', 'Tab');
|
||||
|
||||
//Required Field Form Validation is corrected
|
||||
await expect(page.locator('button:has-text("OK")')).toBeEnabled();
|
||||
await expect(page.locator('text=OK')).toBeEnabled();
|
||||
await expect(page.locator('.c-form-row__state-indicator').first()).not.toHaveClass(/invalid/);
|
||||
|
||||
//Finish Creating Domain Object
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.click('button:has-text("OK")')
|
||||
page.click('text=OK')
|
||||
]);
|
||||
|
||||
//Verify that the Domain Object has been created with the corrected title property
|
||||
@@ -92,44 +91,6 @@ test.describe('Persistence operations @addInit', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Persistence operations @couchdb', () => {
|
||||
test.use({ failOnConsoleError: false });
|
||||
test('Editing object properties should generate a single persistence operation', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/5616'
|
||||
});
|
||||
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
// Create a new 'Clock' object with default settings
|
||||
const clock = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Clock'
|
||||
});
|
||||
|
||||
// Count all persistence operations (PUT requests) for this specific object
|
||||
let putRequestCount = 0;
|
||||
page.on('request', req => {
|
||||
if (req.method() === 'PUT' && req.url().endsWith(clock.uuid)) {
|
||||
putRequestCount += 1;
|
||||
}
|
||||
});
|
||||
|
||||
// Open the edit form for the clock object
|
||||
await page.click('button[title="More options"]');
|
||||
await page.click('li[title="Edit properties of this object."]');
|
||||
|
||||
// Modify the display format from default 12hr -> 24hr and click 'Save'
|
||||
await page.locator('select[aria-label="12 or 24 hour clock"]').selectOption({ value: 'clock24' });
|
||||
await page.click('button[aria-label="Save"]');
|
||||
|
||||
await expect.poll(() => putRequestCount, {
|
||||
message: 'Verify a single PUT request was made to persist the object',
|
||||
timeout: 1000
|
||||
}).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Form Correctness by Object Type', () => {
|
||||
test.fixme('Verify correct behavior of number object (SWG)', async ({page}) => {});
|
||||
test.fixme('Verify correct behavior of number object Timer', async ({page}) => {});
|
||||
|
||||
@@ -81,7 +81,7 @@ test.describe('Move & link item tests', () => {
|
||||
await page.locator('li.icon-move').click();
|
||||
await page.locator(`form[name="mctForm"] >> text=${myItemsFolderName}`).click();
|
||||
|
||||
await page.locator('button:has-text("OK")').click();
|
||||
await page.locator('text=OK').click();
|
||||
|
||||
// Expect that Child Folder is in My Items, the root folder
|
||||
expect(page.locator(`text=${myItemsFolderName} >> nth=0:has(text=Child Folder)`)).toBeTruthy();
|
||||
@@ -95,11 +95,11 @@ test.describe('Move & link item tests', () => {
|
||||
// Create Telemetry Table
|
||||
let telemetryTable = 'Test Telemetry Table';
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
await page.locator('li[role="menuitem"]:has-text("Telemetry Table")').click();
|
||||
await page.locator('li:has-text("Telemetry Table")').click();
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"]').click();
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(telemetryTable);
|
||||
|
||||
await page.locator('button:has-text("OK")').click();
|
||||
await page.locator('text=OK').click();
|
||||
|
||||
// Finish editing and save Telemetry Table
|
||||
await page.locator('.c-button--menu.c-button--major.icon-save').click();
|
||||
@@ -108,7 +108,7 @@ test.describe('Move & link item tests', () => {
|
||||
// Create New Folder Basic Domain Object
|
||||
let folder = 'Test Folder';
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
await page.locator('li[role="menuitem"]:has-text("Folder")').click();
|
||||
await page.locator('li:has-text("Folder")').click();
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"]').click();
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder);
|
||||
|
||||
@@ -120,7 +120,7 @@ test.describe('Move & link item tests', () => {
|
||||
|
||||
// Continue test regardless of assertion and create it in My Items
|
||||
await page.locator(`form[name="mctForm"] >> text=${myItemsFolderName}`).click();
|
||||
await page.locator('button:has-text("OK")').click();
|
||||
await page.locator('text=OK').click();
|
||||
|
||||
// Open My Items
|
||||
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
|
||||
@@ -196,7 +196,7 @@ test.describe('Move & link item tests', () => {
|
||||
await page.locator('li.icon-link').click();
|
||||
await page.locator(`form[name="mctForm"] >> text=${myItemsFolderName}`).click();
|
||||
|
||||
await page.locator('button:has-text("OK")').click();
|
||||
await page.locator('text=OK').click();
|
||||
|
||||
// Expect that Child Folder is in My Items, the root folder
|
||||
expect(page.locator(`text=${myItemsFolderName} >> nth=0:has(text=Child Folder)`)).toBeTruthy();
|
||||
|
||||
@@ -40,11 +40,11 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
await page.locator('li[role="menuitem"]:has-text("Condition Set")').click();
|
||||
await page.locator('li:has-text("Condition Set")').click();
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.click('button:has-text("OK")')
|
||||
page.click('text=OK')
|
||||
]);
|
||||
|
||||
//Save localStorage for future test execution
|
||||
@@ -163,9 +163,9 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
|
||||
// Click hamburger button
|
||||
await page.locator('[title="More options"]').click();
|
||||
|
||||
// Click 'Remove' and press OK
|
||||
await page.locator('li[role="menuitem"]:has-text("Remove")').click();
|
||||
await page.locator('button:has-text("OK")').click();
|
||||
// Click text=Remove
|
||||
await page.locator('text=Remove').click();
|
||||
await page.locator('text=OK').click();
|
||||
|
||||
//Expect Unnamed Condition Set to be removed in Main View
|
||||
const numberOfConditionSetsAtEnd = await page.locator('a:has-text("Unnamed Condition Set Condition Set")').count();
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults, setStartOffset, setFixedTimeMode, setRealTimeMode } = require('../../../../appActions');
|
||||
|
||||
test.describe('Display Layout', () => {
|
||||
test.describe('Testing Display Layout @unstable', () => {
|
||||
let sineWaveObject;
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
@@ -55,12 +55,12 @@ test.describe('Display Layout', () => {
|
||||
// On getting data, check if the value found in the Display Layout is the most recent value
|
||||
// from the Sine Wave Generator
|
||||
const getTelemValuePromise = await subscribeToTelemetry(page, sineWaveObject.uuid);
|
||||
const formattedTelemetryValue = getTelemValuePromise;
|
||||
const formattedTelemetryValue = await getTelemValuePromise;
|
||||
const displayLayoutValuePromise = await page.waitForSelector(`text="${formattedTelemetryValue}"`);
|
||||
const displayLayoutValue = await displayLayoutValuePromise.textContent();
|
||||
const trimmedDisplayValue = displayLayoutValue.trim();
|
||||
|
||||
expect(trimmedDisplayValue).toBe(formattedTelemetryValue);
|
||||
await expect(trimmedDisplayValue).toBe(formattedTelemetryValue);
|
||||
});
|
||||
test('alpha-numeric widget telemetry value exactly matches latest telemetry value received in fixed time', async ({ page }) => {
|
||||
// Create a Display Layout
|
||||
@@ -86,12 +86,12 @@ test.describe('Display Layout', () => {
|
||||
|
||||
// On getting data, check if the value found in the Display Layout is the most recent value
|
||||
// from the Sine Wave Generator
|
||||
const formattedTelemetryValue = getTelemValuePromise;
|
||||
const formattedTelemetryValue = await getTelemValuePromise;
|
||||
const displayLayoutValuePromise = await page.waitForSelector(`text="${formattedTelemetryValue}"`);
|
||||
const displayLayoutValue = await displayLayoutValuePromise.textContent();
|
||||
const trimmedDisplayValue = displayLayoutValue.trim();
|
||||
|
||||
expect(trimmedDisplayValue).toBe(formattedTelemetryValue);
|
||||
await expect(trimmedDisplayValue).toBe(formattedTelemetryValue);
|
||||
});
|
||||
test('items in a display layout can be removed with object tree context menu when viewing the display layout', async ({ page }) => {
|
||||
// Create a Display Layout
|
||||
@@ -116,20 +116,16 @@ test.describe('Display Layout', () => {
|
||||
|
||||
// Bring up context menu and remove
|
||||
await page.locator('.c-tree__item.is-alias .c-tree__item__name:text("Test Sine Wave Generator")').first().click({ button: 'right' });
|
||||
await page.locator('li[role="menuitem"]:has-text("Remove")').click();
|
||||
await page.locator('button:has-text("OK")').click();
|
||||
await page.locator('text=Remove').click();
|
||||
await page.locator('text=OK').click();
|
||||
|
||||
// delete
|
||||
|
||||
expect(await page.locator('.l-layout .l-layout__frame').count()).toEqual(0);
|
||||
expect.soft(await page.locator('.l-layout .l-layout__frame').count()).toEqual(0);
|
||||
});
|
||||
test('items in a display layout can be removed with object tree context menu when viewing another item', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/3117'
|
||||
});
|
||||
// Create a Display Layout
|
||||
const displayLayout = await createDomainObjectWithDefaults(page, {
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Display Layout',
|
||||
name: "Test Display Layout"
|
||||
});
|
||||
@@ -148,18 +144,18 @@ test.describe('Display Layout', () => {
|
||||
// Expand the Display Layout so we can remove the sine wave generator
|
||||
await page.locator('.c-tree__item.is-navigated-object .c-disclosure-triangle').click();
|
||||
|
||||
// Go to the original Sine Wave Generator to navigate away from the Display Layout
|
||||
await page.goto(sineWaveObject.url);
|
||||
// Click the original Sine Wave Generator to navigate away from the Display Layout
|
||||
await page.locator('.c-tree__item .c-tree__item__name:text("Test Sine Wave Generator")').click();
|
||||
|
||||
// Bring up context menu and remove
|
||||
await page.locator('.c-tree__item.is-alias .c-tree__item__name:text("Test Sine Wave Generator")').click({ button: 'right' });
|
||||
await page.locator('li[role="menuitem"]:has-text("Remove")').click();
|
||||
await page.locator('button:has-text("OK")').click();
|
||||
await page.locator('text=Remove').click();
|
||||
await page.locator('text=OK').click();
|
||||
|
||||
// navigate back to the display layout to confirm it has been removed
|
||||
await page.goto(displayLayout.url);
|
||||
await page.locator('.c-tree__item .c-tree__item__name:text("Test Display Layout")').click();
|
||||
|
||||
expect(await page.locator('.l-layout .l-layout__frame').count()).toEqual(0);
|
||||
expect.soft(await page.locator('.l-layout .l-layout__frame').count()).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -23,13 +23,12 @@
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||
|
||||
test.describe('Flexible Layout', () => {
|
||||
let sineWaveObject;
|
||||
test.describe('Testing Flexible Layout @unstable', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
// Create Sine Wave Generator
|
||||
sineWaveObject = await createDomainObjectWithDefaults(page, {
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
name: "Test Sine Wave Generator"
|
||||
});
|
||||
@@ -55,81 +54,13 @@ test.describe('Flexible Layout', () => {
|
||||
await page.dragAndDrop('text=Test Sine Wave Generator', '.c-fl__container.is-empty');
|
||||
await page.dragAndDrop('text=Test Clock', '.c-fl__container.is-empty');
|
||||
// Check that panes can be dragged while Flexible Layout is in Edit mode
|
||||
let dragWrapper = page.locator('.c-fl-container__frames-holder .c-fl-frame__drag-wrapper').first();
|
||||
let dragWrapper = await page.locator('.c-fl-container__frames-holder .c-fl-frame__drag-wrapper').first();
|
||||
await expect(dragWrapper).toHaveAttribute('draggable', 'true');
|
||||
// Save Flexible Layout
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
// 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();
|
||||
dragWrapper = await page.locator('.c-fl-container__frames-holder .c-fl-frame__drag-wrapper').first();
|
||||
await expect(dragWrapper).toHaveAttribute('draggable', 'false');
|
||||
});
|
||||
test('items in a flexible layout can be removed with object tree context menu when viewing the flexible layout', async ({ page }) => {
|
||||
// Create a Display Layout
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Flexible Layout',
|
||||
name: "Test Flexible Layout"
|
||||
});
|
||||
// Edit Flexible Layout
|
||||
await page.locator('[title="Edit"]').click();
|
||||
|
||||
// Expand the 'My Items' folder in the left tree
|
||||
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').first().click();
|
||||
// Add the Sine Wave Generator to the Flexible Layout and save changes
|
||||
await page.dragAndDrop('text=Test Sine Wave Generator', '.c-fl__container.is-empty');
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
expect.soft(await page.locator('.c-fl-container__frame').count()).toEqual(1);
|
||||
|
||||
// Expand the Flexible Layout so we can remove the sine wave generator
|
||||
await page.locator('.c-tree__item.is-navigated-object .c-disclosure-triangle').click();
|
||||
|
||||
// Bring up context menu and remove
|
||||
await page.locator('.c-tree__item.is-alias .c-tree__item__name:text("Test Sine Wave Generator")').first().click({ button: 'right' });
|
||||
await page.locator('li[role="menuitem"]:has-text("Remove")').click();
|
||||
await page.locator('button:has-text("OK")').click();
|
||||
|
||||
// Verify that the item has been removed from the layout
|
||||
expect(await page.locator('.c-fl-container__frame').count()).toEqual(0);
|
||||
});
|
||||
test('items in a flexible layout can be removed with object tree context menu when viewing another item', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/3117'
|
||||
});
|
||||
// Create a Flexible Layout
|
||||
const flexibleLayout = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Flexible Layout',
|
||||
name: "Test Flexible Layout"
|
||||
});
|
||||
// Edit Flexible Layout
|
||||
await page.locator('[title="Edit"]').click();
|
||||
|
||||
// Expand the 'My Items' folder in the left tree
|
||||
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();
|
||||
// Add the Sine Wave Generator to the Flexible Layout and save changes
|
||||
await page.dragAndDrop('text=Test Sine Wave Generator', '.c-fl__container.is-empty');
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
expect.soft(await page.locator('.c-fl-container__frame').count()).toEqual(1);
|
||||
|
||||
// Expand the Flexible Layout so we can remove the sine wave generator
|
||||
await page.locator('.c-tree__item.is-navigated-object .c-disclosure-triangle').click();
|
||||
|
||||
// Go to the original Sine Wave Generator to navigate away from the Flexible Layout
|
||||
await page.goto(sineWaveObject.url);
|
||||
|
||||
// Bring up context menu and remove
|
||||
await page.locator('.c-tree__item.is-alias .c-tree__item__name:text("Test Sine Wave Generator")').click({ button: 'right' });
|
||||
await page.locator('li[role="menuitem"]:has-text("Remove")').click();
|
||||
await page.locator('button:has-text("OK")').click();
|
||||
|
||||
// navigate back to the display layout to confirm it has been removed
|
||||
await page.goto(flexibleLayout.url);
|
||||
|
||||
// Verify that the item has been removed from the layout
|
||||
expect(await page.locator('.c-fl-container__frame').count()).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, 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 dedicated to testing the Gauge component.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../../../baseFixtures');
|
||||
const { createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||
const uuid = require('uuid').v4;
|
||||
|
||||
test.describe('Gauge', () => {
|
||||
let gauge;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Open a browser, navigate to the main page, and wait until all networkevents to resolve
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
// Create the gauge
|
||||
gauge = await createDomainObjectWithDefaults(page, { type: 'Gauge' });
|
||||
});
|
||||
|
||||
test('Can add and remove telemetry sources @unstable', async ({ page }) => {
|
||||
const editButtonLocator = page.locator('button[title="Edit"]');
|
||||
const saveButtonLocator = page.locator('button[title="Save"]');
|
||||
|
||||
// Create a sine wave generator within the gauge
|
||||
const swg1 = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
name: `swg-${uuid()}`,
|
||||
parent: gauge.uuid
|
||||
});
|
||||
|
||||
// Navigate to the gauge and verify that
|
||||
// the SWG appears in the elements pool
|
||||
await page.goto(gauge.url);
|
||||
await editButtonLocator.click();
|
||||
await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeVisible();
|
||||
await saveButtonLocator.click();
|
||||
await page.locator('li[title="Save and Finish Editing"]').click();
|
||||
|
||||
// Create another sine wave generator within the gauge
|
||||
const swg2 = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
name: `swg-${uuid()}`,
|
||||
parent: gauge.uuid
|
||||
});
|
||||
|
||||
// Verify that the 'Replace telemetry source' modal appears and accept it
|
||||
await expect.soft(page.locator('text=This action will replace the current telemetry source. Do you want to continue?')).toBeVisible();
|
||||
await page.click('text=Ok');
|
||||
|
||||
// Navigate to the gauge and verify that the new SWG
|
||||
// appears in the elements pool and the old one is gone
|
||||
await page.goto(gauge.url);
|
||||
await editButtonLocator.click();
|
||||
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 saveButtonLocator.click();
|
||||
|
||||
// Right click on the new SWG in the elements pool and delete it
|
||||
await page.locator(`#inspector-elements-tree >> text=${swg2.name}`).click({
|
||||
button: 'right'
|
||||
});
|
||||
await page.locator('li[title="Remove this object from its containing object."]').click();
|
||||
|
||||
// Verify that the 'Remove object' confirmation modal appears and accept it
|
||||
await expect.soft(page.locator('text=Warning! This action will remove this object. Are you sure you want to continue?')).toBeVisible();
|
||||
await page.click('text=Ok');
|
||||
|
||||
// Verify that the elements pool shows no elements
|
||||
await expect(page.locator('text="No contained elements"')).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -40,10 +40,10 @@ test.describe('Example Imagery Object', () => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
// Create a default 'Example Imagery' object
|
||||
const exampleImagery = await createDomainObjectWithDefaults(page, { type: 'Example Imagery' });
|
||||
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 expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
});
|
||||
|
||||
@@ -188,7 +188,7 @@ test.describe('Example Imagery in Display Layout', () => {
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
// Click text=Example Imagery
|
||||
await page.click('li[role="menuitem"]:has-text("Example Imagery")');
|
||||
await page.click('text=Example Imagery');
|
||||
|
||||
// Clear and set Image load delay to minimum value
|
||||
await page.locator('input[type="number"]').fill('');
|
||||
@@ -197,7 +197,7 @@ test.describe('Example Imagery in Display Layout', () => {
|
||||
// Click text=OK
|
||||
await Promise.all([
|
||||
page.waitForNavigation({waitUntil: 'networkidle'}),
|
||||
page.click('button:has-text("OK")'),
|
||||
page.click('text=OK'),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
@@ -275,7 +275,7 @@ test.describe('Example Imagery in Flexible layout', () => {
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
// Click text=Example Imagery
|
||||
await page.click('li[role="menuitem"]:has-text("Example Imagery")');
|
||||
await page.click('text=Example Imagery');
|
||||
|
||||
// Clear and set Image load delay to minimum value
|
||||
await page.locator('input[type="number"]').fill('');
|
||||
@@ -284,7 +284,7 @@ test.describe('Example Imagery in Flexible layout', () => {
|
||||
// Click text=OK
|
||||
await Promise.all([
|
||||
page.waitForNavigation({waitUntil: 'networkidle'}),
|
||||
page.click('button:has-text("OK")'),
|
||||
page.click('text=OK'),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
@@ -317,7 +317,7 @@ test.describe('Example Imagery in Tabs View', () => {
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
// Click text=Example Imagery
|
||||
await page.click('li[role="menuitem"]:has-text("Example Imagery")');
|
||||
await page.click('text=Example Imagery');
|
||||
|
||||
// Clear and set Image load delay to minimum value
|
||||
await page.locator('input[type="number"]').fill('');
|
||||
@@ -326,7 +326,7 @@ test.describe('Example Imagery in Tabs View', () => {
|
||||
// Click text=OK
|
||||
await Promise.all([
|
||||
page.waitForNavigation({waitUntil: 'networkidle'}),
|
||||
page.click('button:has-text("OK")'),
|
||||
page.click('text=OK'),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
|
||||
@@ -26,7 +26,7 @@ This test suite is dedicated to tests which verify the basic operations surround
|
||||
|
||||
// FIXME: Remove this eslint exception once tests are implemented
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { test, expect } = require('../../../../baseFixtures');
|
||||
const { expandTreePaneItemByName, createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||
const nbUtils = require('../../../../helper/notebookUtils');
|
||||
|
||||
|
||||
@@ -1,271 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, 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 dedicated to tests which verify the basic operations surrounding Notebooks with CouchDB.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||
|
||||
test.describe('Notebook Tests with CouchDB @couchdb', () => {
|
||||
let testNotebook;
|
||||
test.beforeEach(async ({ page }) => {
|
||||
//Navigate to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
// Create Notebook
|
||||
testNotebook = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Notebook',
|
||||
name: "TestNotebook"
|
||||
});
|
||||
});
|
||||
|
||||
test('Inspect Notebook Entry Network Requests', async ({ page }) => {
|
||||
// Expand sidebar
|
||||
await page.locator('.c-notebook__toggle-nav-button').click();
|
||||
|
||||
// Collect all request events to count and assert after notebook action
|
||||
let addingNotebookElementsRequests = [];
|
||||
page.on('request', (request) => addingNotebookElementsRequests.push(request));
|
||||
|
||||
let [notebookUrlRequest, allDocsRequest] = await Promise.all([
|
||||
// Waits for the next request with the specified url
|
||||
page.waitForRequest(`**/openmct/${testNotebook.uuid}`),
|
||||
page.waitForRequest('**/openmct/_all_docs?include_docs=true'),
|
||||
// Triggers the request
|
||||
page.click('[aria-label="Add Page"]'),
|
||||
// Ensures that there are no other network requests
|
||||
page.waitForLoadState('networkidle')
|
||||
]);
|
||||
// Assert that only two requests are made
|
||||
// Network Requests are:
|
||||
// 1) The actual POST to create the page
|
||||
// 2) The shared worker event from 👆 request
|
||||
expect(addingNotebookElementsRequests.length).toBe(2);
|
||||
|
||||
// Assert on request object
|
||||
expect(notebookUrlRequest.postDataJSON().metadata.name).toBe('TestNotebook');
|
||||
expect(notebookUrlRequest.postDataJSON().model.persisted).toBeGreaterThanOrEqual(notebookUrlRequest.postDataJSON().model.modified);
|
||||
expect(allDocsRequest.postDataJSON().keys).toContain(testNotebook.uuid);
|
||||
|
||||
// Add an entry
|
||||
// Network Requests are:
|
||||
// 1) The actual POST to create the entry
|
||||
// 2) The shared worker event from 👆 POST request
|
||||
addingNotebookElementsRequests = [];
|
||||
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"]').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"]').fill(`First Entry`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
expect(addingNotebookElementsRequests.length).toBeLessThanOrEqual(2);
|
||||
|
||||
// Add some tags
|
||||
// Network Requests are for each tag creation are:
|
||||
// 1) Getting the original path of the parent object
|
||||
// 2) Getting the original path of the grandparent object (recursive call)
|
||||
// 3) Creating the annotation/tag object
|
||||
// 4) The shared worker event from 👆 POST request
|
||||
// 5) Mutate notebook domain object's annotationModified property
|
||||
// 6) The shared worker event from 👆 POST request
|
||||
// 7) Notebooks fetching new annotations due to annotationModified changed
|
||||
// 8) The update of the notebook domain's object's modified property
|
||||
// 9) The shared worker event from 👆 POST request
|
||||
// 10) Entry is timestamped
|
||||
// 11) The shared worker event from 👆 POST request
|
||||
|
||||
addingNotebookElementsRequests = [];
|
||||
await page.hover(`button:has-text("Add Tag")`);
|
||||
await page.locator(`button:has-text("Add Tag")`).click();
|
||||
await page.locator('[placeholder="Type to select tag"]').click();
|
||||
await page.locator('[aria-label="Autocomplete Options"] >> text=Driving').click();
|
||||
await page.waitForSelector('[aria-label="Tag"]:has-text("Driving")');
|
||||
page.waitForLoadState('networkidle');
|
||||
expect(filterNonFetchRequests(addingNotebookElementsRequests).length).toBeLessThanOrEqual(11);
|
||||
|
||||
addingNotebookElementsRequests = [];
|
||||
await page.hover(`button:has-text("Add Tag")`);
|
||||
await page.locator(`button:has-text("Add Tag")`).click();
|
||||
await page.locator('[placeholder="Type to select tag"]').click();
|
||||
await page.locator('[aria-label="Autocomplete Options"] >> text=Drilling').click();
|
||||
await page.waitForSelector('[aria-label="Tag"]:has-text("Drilling")');
|
||||
page.waitForLoadState('networkidle');
|
||||
expect(filterNonFetchRequests(addingNotebookElementsRequests).length).toBeLessThanOrEqual(11);
|
||||
|
||||
addingNotebookElementsRequests = [];
|
||||
await page.hover(`button:has-text("Add Tag")`);
|
||||
await page.locator(`button:has-text("Add Tag")`).click();
|
||||
await page.locator('[placeholder="Type to select tag"]').click();
|
||||
await page.locator('[aria-label="Autocomplete Options"] >> text=Science').click();
|
||||
await page.waitForSelector('[aria-label="Tag"]:has-text("Science")');
|
||||
page.waitForLoadState('networkidle');
|
||||
expect(filterNonFetchRequests(addingNotebookElementsRequests).length).toBeLessThanOrEqual(11);
|
||||
|
||||
// Delete all the tags
|
||||
// Network requests are:
|
||||
// 1) Send POST to mutate _delete property to true on annotation with tag
|
||||
// 2) The shared worker event from 👆 POST request
|
||||
// 3) Timestamp update on entry
|
||||
// 4) The shared worker event from 👆 POST request
|
||||
// This happens for 3 tags so 12 requests
|
||||
addingNotebookElementsRequests = [];
|
||||
await page.hover('[aria-label="Tag"]:has-text("Driving")');
|
||||
await page.locator('[aria-label="Tag"]:has-text("Driving") ~ .c-completed-tag-deletion').click();
|
||||
await page.waitForSelector('[aria-label="Tag"]:has-text("Driving")', {state: 'hidden'});
|
||||
await page.hover('[aria-label="Tag"]:has-text("Drilling")');
|
||||
await page.locator('[aria-label="Tag"]:has-text("Drilling") ~ .c-completed-tag-deletion').click();
|
||||
await page.waitForSelector('[aria-label="Tag"]:has-text("Drilling")', {state: 'hidden'});
|
||||
page.hover('[aria-label="Tag"]:has-text("Science")');
|
||||
await page.locator('[aria-label="Tag"]:has-text("Science") ~ .c-completed-tag-deletion').click();
|
||||
await page.waitForSelector('[aria-label="Tag"]:has-text("Science")', {state: 'hidden'});
|
||||
page.waitForLoadState('networkidle');
|
||||
expect(filterNonFetchRequests(addingNotebookElementsRequests).length).toBeLessThanOrEqual(12);
|
||||
|
||||
// Add two more pages
|
||||
await page.click('[aria-label="Add Page"]');
|
||||
await page.click('[aria-label="Add Page"]');
|
||||
|
||||
// Add three entries
|
||||
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"]').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"]').fill(`First Entry`);
|
||||
|
||||
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"] >> nth=1').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"] >> nth=1').fill(`Second Entry`);
|
||||
|
||||
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"] >> nth=2').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"] >> nth=2').fill(`Third Entry`);
|
||||
|
||||
// Add three tags
|
||||
await page.hover(`button:has-text("Add Tag") >> nth=2`);
|
||||
await page.locator(`button:has-text("Add Tag") >> nth=2`).click();
|
||||
await page.locator('[placeholder="Type to select tag"]').click();
|
||||
await page.locator('[aria-label="Autocomplete Options"] >> text=Science').click();
|
||||
await page.waitForSelector('[aria-label="Tag"]:has-text("Science")');
|
||||
|
||||
await page.hover(`button:has-text("Add Tag") >> nth=2`);
|
||||
await page.locator(`button:has-text("Add Tag") >> nth=2`).click();
|
||||
await page.locator('[placeholder="Type to select tag"]').click();
|
||||
await page.locator('[aria-label="Autocomplete Options"] >> text=Drilling').click();
|
||||
await page.waitForSelector('[aria-label="Tag"]:has-text("Drilling")');
|
||||
|
||||
await page.hover(`button:has-text("Add Tag") >> nth=2`);
|
||||
await page.locator(`button:has-text("Add Tag") >> nth=2`).click();
|
||||
await page.locator('[placeholder="Type to select tag"]').click();
|
||||
await page.locator('[aria-label="Autocomplete Options"] >> text=Driving').click();
|
||||
await page.waitForSelector('[aria-label="Tag"]:has-text("Driving")');
|
||||
page.waitForLoadState('networkidle');
|
||||
|
||||
// Add a fourth entry
|
||||
// Network requests are:
|
||||
// 1) Send POST to add new entry
|
||||
// 2) The shared worker event from 👆 POST request
|
||||
// 3) Timestamp update on entry
|
||||
// 4) The shared worker event from 👆 POST request
|
||||
addingNotebookElementsRequests = [];
|
||||
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"] >> nth=3').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"] >> nth=3').fill(`Fourth Entry`);
|
||||
await page.locator('[aria-label="Notebook Entry Input"] >> nth=3').press('Enter');
|
||||
page.waitForLoadState('networkidle');
|
||||
|
||||
expect(filterNonFetchRequests(addingNotebookElementsRequests).length).toBeLessThanOrEqual(4);
|
||||
|
||||
// Add a fifth entry
|
||||
// Network requests are:
|
||||
// 1) Send POST to add new entry
|
||||
// 2) The shared worker event from 👆 POST request
|
||||
// 3) Timestamp update on entry
|
||||
// 4) The shared worker event from 👆 POST request
|
||||
addingNotebookElementsRequests = [];
|
||||
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"] >> nth=4').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"] >> nth=4').fill(`Fifth Entry`);
|
||||
await page.locator('[aria-label="Notebook Entry Input"] >> nth=4').press('Enter');
|
||||
page.waitForLoadState('networkidle');
|
||||
|
||||
expect(filterNonFetchRequests(addingNotebookElementsRequests).length).toBeLessThanOrEqual(4);
|
||||
|
||||
// Add a sixth entry
|
||||
// 1) Send POST to add new entry
|
||||
// 2) The shared worker event from 👆 POST request
|
||||
// 3) Timestamp update on entry
|
||||
// 4) The shared worker event from 👆 POST request
|
||||
addingNotebookElementsRequests = [];
|
||||
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"] >> nth=5').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"] >> nth=5').fill(`Sixth Entry`);
|
||||
await page.locator('[aria-label="Notebook Entry Input"] >> nth=5').press('Enter');
|
||||
page.waitForLoadState('networkidle');
|
||||
|
||||
expect(filterNonFetchRequests(addingNotebookElementsRequests).length).toBeLessThanOrEqual(4);
|
||||
});
|
||||
|
||||
test('Search tests', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/akhenry/openmct-yamcs/issues/69'
|
||||
});
|
||||
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"]').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"]').fill(`First Entry`);
|
||||
await page.locator('[aria-label="Notebook Entry Input"]').press('Enter');
|
||||
|
||||
// Add three tags
|
||||
await page.hover(`button:has-text("Add Tag")`);
|
||||
await page.locator(`button:has-text("Add Tag")`).click();
|
||||
await page.locator('[placeholder="Type to select tag"]').click();
|
||||
await page.locator('[aria-label="Autocomplete Options"] >> text=Science').click();
|
||||
await page.waitForSelector('[aria-label="Tag"]:has-text("Science")');
|
||||
|
||||
await page.hover(`button:has-text("Add Tag")`);
|
||||
await page.locator(`button:has-text("Add Tag")`).click();
|
||||
await page.locator('[placeholder="Type to select tag"]').click();
|
||||
await page.locator('[aria-label="Autocomplete Options"] >> text=Drilling').click();
|
||||
await page.waitForSelector('[aria-label="Tag"]:has-text("Drilling")');
|
||||
|
||||
await page.hover(`button:has-text("Add Tag")`);
|
||||
await page.locator(`button:has-text("Add Tag")`).click();
|
||||
await page.locator('[placeholder="Type to select tag"]').click();
|
||||
await page.locator('[aria-label="Autocomplete Options"] >> text=Driving').click();
|
||||
await page.waitForSelector('[aria-label="Tag"]:has-text("Driving")');
|
||||
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Sc');
|
||||
await expect(page.locator('[aria-label="Search Result"]').first()).toContainText("Science");
|
||||
await expect(page.locator('[aria-label="Search Result"]').first()).not.toContainText("Driving");
|
||||
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Xq');
|
||||
await expect(page.locator('text=No results found')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
// Try to reduce indeterminism of browser requests by only returning fetch requests.
|
||||
// Filter out preflight CORS, fetching stylesheets, page icons, etc. that can occur during tests
|
||||
function filterNonFetchRequests(requests) {
|
||||
return requests.filter(request => {
|
||||
return (request.resourceType() === 'fetch');
|
||||
});
|
||||
}
|
||||
@@ -36,27 +36,27 @@ test.describe('Restricted Notebook', () => {
|
||||
});
|
||||
|
||||
test('Can be renamed @addInit', async ({ page }) => {
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText(`${notebook.name}`);
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText(`Unnamed ${CUSTOM_NAME}`);
|
||||
});
|
||||
|
||||
test('Can be deleted if there are no locked pages @addInit', async ({ page }) => {
|
||||
test('Can be deleted if there are no locked pages @addInit', async ({ page, openmctConfig }) => {
|
||||
await openObjectTreeContextMenu(page, notebook.url);
|
||||
|
||||
const menuOptions = page.locator('.c-menu ul');
|
||||
await expect.soft(menuOptions).toContainText('Remove');
|
||||
|
||||
const restrictedNotebookTreeObject = page.locator(`a:has-text("${notebook.name}")`);
|
||||
const restrictedNotebookTreeObject = page.locator(`a:has-text("Unnamed ${CUSTOM_NAME}")`);
|
||||
|
||||
// notebook tree object exists
|
||||
expect.soft(await restrictedNotebookTreeObject.count()).toEqual(1);
|
||||
|
||||
// Click Remove Text
|
||||
await page.locator('li[role="menuitem"]:has-text("Remove")').click();
|
||||
await page.locator('text=Remove').click();
|
||||
|
||||
// Click 'OK' on confirmation window and wait for save banner to appear
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('button:has-text("OK")').click(),
|
||||
page.locator('text=OK').click(),
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
|
||||
@@ -134,7 +134,7 @@ test.describe('Restricted Notebook with at least one entry and with the page loc
|
||||
// Click text=Ok
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('button:has-text("OK")').click()
|
||||
page.locator('text=Ok').click()
|
||||
]);
|
||||
|
||||
// deleted page, should no longer exist
|
||||
@@ -145,9 +145,10 @@ test.describe('Restricted Notebook with at least one entry and with the page loc
|
||||
|
||||
test.describe('Restricted Notebook with a page locked and with an embed @addInit', () => {
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
const notebook = await startAndAddRestrictedNotebookObject(page);
|
||||
await nbUtils.dragAndDropEmbed(page, notebook);
|
||||
test.beforeEach(async ({ page, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
await startAndAddRestrictedNotebookObject(page);
|
||||
await nbUtils.dragAndDropEmbed(page, myItemsFolderName);
|
||||
});
|
||||
|
||||
test('Allows embeds to be deleted if page unlocked @addInit', async ({ page }) => {
|
||||
|
||||
@@ -36,17 +36,15 @@ async function createNotebookAndEntry(page, iterations = 1) {
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
const notebook = createDomainObjectWithDefaults(page, { type: 'Notebook' });
|
||||
createDomainObjectWithDefaults(page, { type: 'Notebook' });
|
||||
|
||||
for (let iteration = 0; iteration < iterations; iteration++) {
|
||||
// Create an entry
|
||||
// Click text=To start a new entry, click here or drag and drop any object
|
||||
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
|
||||
const entryLocator = `[aria-label="Notebook Entry Input"] >> nth = ${iteration}`;
|
||||
await page.locator(entryLocator).click();
|
||||
await page.locator(entryLocator).fill(`Entry ${iteration}`);
|
||||
}
|
||||
|
||||
return notebook;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -55,7 +53,7 @@ async function createNotebookAndEntry(page, iterations = 1) {
|
||||
* @param {number} [iterations = 1] - the number of entries (and tags) to create
|
||||
*/
|
||||
async function createNotebookEntryAndTags(page, iterations = 1) {
|
||||
const notebook = await createNotebookAndEntry(page, iterations);
|
||||
await createNotebookAndEntry(page, iterations);
|
||||
|
||||
for (let iteration = 0; iteration < iterations; iteration++) {
|
||||
// Hover and click "Add Tag" button
|
||||
@@ -77,16 +75,16 @@ async function createNotebookEntryAndTags(page, iterations = 1) {
|
||||
// Select the "Science" tag
|
||||
await page.locator('[aria-label="Autocomplete Options"] >> text=Science').click();
|
||||
}
|
||||
|
||||
return notebook;
|
||||
}
|
||||
|
||||
test.describe('Tagging in Notebooks @addInit', () => {
|
||||
test('Can load tags', async ({ page }) => {
|
||||
|
||||
await createNotebookAndEntry(page);
|
||||
// Click text=To start a new entry, click here or drag and drop any object
|
||||
await page.locator('button:has-text("Add Tag")').click();
|
||||
|
||||
// Click [placeholder="Type to select tag"]
|
||||
await page.locator('[placeholder="Type to select tag"]').click();
|
||||
|
||||
await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Science");
|
||||
@@ -99,7 +97,9 @@ test.describe('Tagging in Notebooks @addInit', () => {
|
||||
await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Science");
|
||||
await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Driving");
|
||||
|
||||
// Click button:has-text("Add Tag")
|
||||
await page.locator('button:has-text("Add Tag")').click();
|
||||
// Click [placeholder="Type to select tag"]
|
||||
await page.locator('[placeholder="Type to select tag"]').click();
|
||||
|
||||
await expect(page.locator('[aria-label="Autocomplete Options"]')).not.toContainText("Science");
|
||||
@@ -108,56 +108,43 @@ test.describe('Tagging in Notebooks @addInit', () => {
|
||||
});
|
||||
test('Can search for tags', async ({ page }) => {
|
||||
await createNotebookEntryAndTags(page);
|
||||
// Click [aria-label="OpenMCT Search"] input[type="search"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
|
||||
// Fill [aria-label="OpenMCT Search"] input[type="search"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc');
|
||||
await expect(page.locator('[aria-label="Search Result"]')).toContainText("Science");
|
||||
await expect(page.locator('[aria-label="Search Result"]')).not.toContainText("Driving");
|
||||
await expect(page.locator('[aria-label="Search Result"]')).toContainText("Driving");
|
||||
|
||||
// Click [aria-label="OpenMCT Search"] input[type="search"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
|
||||
// Fill [aria-label="OpenMCT Search"] input[type="search"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Sc');
|
||||
await expect(page.locator('[aria-label="Search Result"]')).toContainText("Science");
|
||||
await expect(page.locator('[aria-label="Search Result"]')).not.toContainText("Driving");
|
||||
await expect(page.locator('[aria-label="Search Result"]')).toContainText("Driving");
|
||||
|
||||
// Click [aria-label="OpenMCT Search"] input[type="search"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
|
||||
// Fill [aria-label="OpenMCT Search"] input[type="search"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Xq');
|
||||
await expect(page.locator('text=No results found')).toBeVisible();
|
||||
await expect(page.locator('[aria-label="Search Result"]')).toBeHidden();
|
||||
await expect(page.locator('[aria-label="Search Result"]')).toBeHidden();
|
||||
});
|
||||
|
||||
test('Can delete tags', async ({ page }) => {
|
||||
await createNotebookEntryAndTags(page);
|
||||
await page.locator('[aria-label="Notebook Entries"]').click();
|
||||
// Delete Driving
|
||||
await page.hover('[aria-label="Tag"]:has-text("Driving")');
|
||||
await page.locator('[aria-label="Tag"]:has-text("Driving") ~ .c-completed-tag-deletion').click();
|
||||
await page.hover('.c-tag__label:has-text("Driving")');
|
||||
await page.locator('.c-tag__label:has-text("Driving") ~ .c-completed-tag-deletion').click();
|
||||
|
||||
await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Science");
|
||||
await expect(page.locator('[aria-label="Notebook Entry"]')).not.toContainText("Driving");
|
||||
|
||||
// Fill [aria-label="OpenMCT Search"] input[type="search"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc');
|
||||
await expect(page.locator('[aria-label="Search Result"]')).not.toContainText("Driving");
|
||||
});
|
||||
|
||||
test('Can delete entries without tags', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/5823'
|
||||
});
|
||||
|
||||
await createNotebookEntryAndTags(page);
|
||||
|
||||
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
|
||||
const entryLocator = `[aria-label="Notebook Entry Input"] >> nth = 1`;
|
||||
await page.locator(entryLocator).click();
|
||||
await page.locator(entryLocator).fill(`An entry without tags`);
|
||||
await page.locator('[aria-label="Notebook Entry Input"] >> nth=1').press('Enter');
|
||||
|
||||
await page.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?')).toBeVisible();
|
||||
await page.locator('button:has-text("Ok")').click();
|
||||
await expect(page.locator('text=This action will permanently delete this entry. Do you wish to continue?')).toBeHidden();
|
||||
});
|
||||
|
||||
test('Can delete objects with tags and neither return in search', async ({ page }) => {
|
||||
await createNotebookEntryAndTags(page);
|
||||
// Delete Notebook
|
||||
@@ -166,6 +153,7 @@ test.describe('Tagging in Notebooks @addInit', () => {
|
||||
await page.locator('button:has-text("OK")').click();
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
// Fill [aria-label="OpenMCT Search"] input[type="search"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Unnamed');
|
||||
await expect(page.locator('text=No results found')).toBeVisible();
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sci');
|
||||
@@ -177,10 +165,10 @@ test.describe('Tagging in Notebooks @addInit', () => {
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
const clock = await createDomainObjectWithDefaults(page, { type: 'Clock' });
|
||||
await createDomainObjectWithDefaults(page, { type: 'Clock' });
|
||||
|
||||
const ITERATIONS = 4;
|
||||
const notebook = await createNotebookEntryAndTags(page, ITERATIONS);
|
||||
await createNotebookEntryAndTags(page, ITERATIONS);
|
||||
|
||||
for (let iteration = 0; iteration < ITERATIONS; iteration++) {
|
||||
const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`;
|
||||
@@ -193,11 +181,11 @@ test.describe('Tagging in Notebooks @addInit', () => {
|
||||
page.goto('./#/browse/mine?hideTree=false'),
|
||||
page.click('.c-disclosure-triangle')
|
||||
]);
|
||||
// Click Clock
|
||||
await page.click(`text=${clock.name}`);
|
||||
// Click Unnamed Clock
|
||||
await page.click('text="Unnamed Clock"');
|
||||
|
||||
// Click Notebook
|
||||
await page.click(`text=${notebook.name}`);
|
||||
// Click Unnamed Notebook
|
||||
await page.click('text="Unnamed Notebook"');
|
||||
|
||||
for (let iteration = 0; iteration < ITERATIONS; iteration++) {
|
||||
const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`;
|
||||
@@ -211,13 +199,14 @@ test.describe('Tagging in Notebooks @addInit', () => {
|
||||
page.waitForLoadState('networkidle')
|
||||
]);
|
||||
|
||||
// Click Notebook
|
||||
await page.click(`text="${notebook.name}"`);
|
||||
// Click Unnamed Notebook
|
||||
await page.click('text="Unnamed Notebook"');
|
||||
|
||||
for (let iteration = 0; iteration < ITERATIONS; iteration++) {
|
||||
const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`;
|
||||
await expect(page.locator(entryLocator)).toContainText("Science");
|
||||
await expect(page.locator(entryLocator)).toContainText("Driving");
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -110,10 +110,10 @@ async function createSinewaveOverlayPlot(page, myItemsFolderName) {
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
|
||||
// add overlay plot with defaults
|
||||
await page.locator('li[role="menuitem"]:has-text("Overlay Plot")').click();
|
||||
await page.locator('li:has-text("Overlay Plot")').click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('button:has-text("OK")').click(),
|
||||
page.locator('text=OK').click(),
|
||||
//Wait for Save Banner to appear1
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
@@ -129,10 +129,10 @@ async function createSinewaveOverlayPlot(page, myItemsFolderName) {
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
|
||||
// add sine wave generator with defaults
|
||||
await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click();
|
||||
await page.locator('li:has-text("Sine Wave Generator")').click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('button:has-text("OK")').click(),
|
||||
page.locator('text=OK').click(),
|
||||
//Wait for Save Banner to appear1
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
|
||||
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 18 KiB |
@@ -88,10 +88,10 @@ async function makeOverlayPlot(page, myItemsFolderName) {
|
||||
// create overlay plot
|
||||
|
||||
await page.locator('button.c-create-button').click();
|
||||
await page.locator('li[role="menuitem"]:has-text("Overlay Plot")').click();
|
||||
await page.locator('li:has-text("Overlay Plot")').click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation({ waitUntil: 'networkidle'}),
|
||||
page.locator('button:has-text("OK")').click(),
|
||||
page.locator('text=OK').click(),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
@@ -106,7 +106,7 @@ async function makeOverlayPlot(page, myItemsFolderName) {
|
||||
// create a sinewave generator
|
||||
|
||||
await page.locator('button.c-create-button').click();
|
||||
await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click();
|
||||
await page.locator('li:has-text("Sine Wave Generator")').click();
|
||||
|
||||
// set amplitude to 6, offset 4, period 2
|
||||
|
||||
@@ -123,7 +123,7 @@ async function makeOverlayPlot(page, myItemsFolderName) {
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation({ waitUntil: 'networkidle'}),
|
||||
page.locator('button:has-text("OK")').click(),
|
||||
page.locator('text=OK').click(),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
|
||||
@@ -88,11 +88,11 @@ async function makeStackedPlot(page, myItemsFolderName) {
|
||||
|
||||
// create stacked plot
|
||||
await page.locator('button.c-create-button').click();
|
||||
await page.locator('li[role="menuitem"]:has-text("Stacked Plot")').click();
|
||||
await page.locator('li:has-text("Stacked Plot")').click();
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation({ waitUntil: 'networkidle'}),
|
||||
page.locator('button:has-text("OK")').click(),
|
||||
page.locator('text=OK').click(),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
@@ -146,11 +146,11 @@ async function saveStackedPlot(page) {
|
||||
async function createSineWaveGenerator(page) {
|
||||
//Create sine wave generator
|
||||
await page.locator('button.c-create-button').click();
|
||||
await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click();
|
||||
await page.locator('li:has-text("Sine Wave Generator")').click();
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation({ waitUntil: 'networkidle'}),
|
||||
page.locator('button:has-text("OK")').click(),
|
||||
page.locator('text=OK').click(),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
|
||||
@@ -68,10 +68,10 @@ async function makeOverlayPlot(page) {
|
||||
// create overlay plot
|
||||
|
||||
await page.locator('button.c-create-button').click();
|
||||
await page.locator('li[role="menuitem"]:has-text("Overlay Plot")').click();
|
||||
await page.locator('li:has-text("Overlay Plot")').click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation({ waitUntil: 'networkidle'}),
|
||||
page.locator('button:has-text("OK")').click(),
|
||||
page.locator('text=OK').click(),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
@@ -86,13 +86,13 @@ async function makeOverlayPlot(page) {
|
||||
// create a sinewave generator
|
||||
|
||||
await page.locator('button.c-create-button').click();
|
||||
await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click();
|
||||
await page.locator('li:has-text("Sine Wave Generator")').click();
|
||||
|
||||
// Click OK to make generator
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation({ waitUntil: 'networkidle'}),
|
||||
page.locator('button:has-text("OK")').click(),
|
||||
page.locator('text=OK').click(),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, 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 dedicated to testing the rendering and interaction of plots.
|
||||
*
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||
|
||||
test.describe('Plot Integrity Testing @unstable', () => {
|
||||
let sineWaveGeneratorObject;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
//Open a browser, navigate to the main page, and wait until all networkevents to resolve
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
sineWaveGeneratorObject = await createDomainObjectWithDefaults(page, { type: 'Sine Wave Generator' });
|
||||
});
|
||||
|
||||
test('Plots do not re-request data when a plot is clicked', async ({ page }) => {
|
||||
//Navigate to Sine Wave Generator
|
||||
await page.goto(sineWaveGeneratorObject.url);
|
||||
//Capture the number of plots points and store as const name numberOfPlotPoints
|
||||
//Click on the plot canvas
|
||||
await page.locator('canvas').nth(1).click();
|
||||
//No request was made to get historical data
|
||||
const createMineFolderRequests = [];
|
||||
page.on('request', req => {
|
||||
// eslint-disable-next-line playwright/no-conditional-in-test
|
||||
createMineFolderRequests.push(req);
|
||||
});
|
||||
expect(createMineFolderRequests.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
@@ -1,93 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, 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 dedicated to testing the Scatter Plot component.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||
const uuid = require('uuid').v4;
|
||||
|
||||
test.describe('Scatter Plot', () => {
|
||||
let scatterPlot;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Open a browser, navigate to the main page, and wait until all networkevents to resolve
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
// Create the Scatter Plot
|
||||
scatterPlot = await createDomainObjectWithDefaults(page, { type: 'Scatter Plot' });
|
||||
});
|
||||
|
||||
test('Can add and remove telemetry sources', async ({ page }) => {
|
||||
const editButtonLocator = page.locator('button[title="Edit"]');
|
||||
const saveButtonLocator = page.locator('button[title="Save"]');
|
||||
|
||||
// Create a sine wave generator within the scatter plot
|
||||
const swg1 = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
name: `swg-${uuid()}`,
|
||||
parent: scatterPlot.uuid
|
||||
});
|
||||
|
||||
// Navigate to the scatter plot and verify that
|
||||
// the SWG appears in the elements pool
|
||||
await page.goto(scatterPlot.url);
|
||||
await editButtonLocator.click();
|
||||
await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeVisible();
|
||||
await saveButtonLocator.click();
|
||||
await page.locator('li[title="Save and Finish Editing"]').click();
|
||||
|
||||
// Create another sine wave generator within the scatter plot
|
||||
const swg2 = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
name: `swg-${uuid()}`,
|
||||
parent: scatterPlot.uuid
|
||||
});
|
||||
|
||||
// Verify that the 'Replace telemetry source' modal appears and accept it
|
||||
await expect.soft(page.locator('text=This action will replace the current telemetry source. Do you want to continue?')).toBeVisible();
|
||||
await page.click('text=Ok');
|
||||
|
||||
// Navigate to the scatter plot and verify that the new SWG
|
||||
// appears in the elements pool and the old one is gone
|
||||
await page.goto(scatterPlot.url);
|
||||
await editButtonLocator.click();
|
||||
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 saveButtonLocator.click();
|
||||
|
||||
// Right click on the new SWG in the elements pool and delete it
|
||||
await page.locator(`#inspector-elements-tree >> text=${swg2.name}`).click({
|
||||
button: 'right'
|
||||
});
|
||||
await page.locator('li[title="Remove this object from its containing object."]').click();
|
||||
|
||||
// Verify that the 'Remove object' confirmation modal appears and accept it
|
||||
await expect.soft(page.locator('text=Warning! This action will remove this object. Are you sure you want to continue?')).toBeVisible();
|
||||
await page.click('text=Ok');
|
||||
|
||||
// Verify that the elements pool shows no elements
|
||||
await expect(page.locator('text="No contained elements"')).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -20,7 +20,7 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { test, expect } = require('../../../../baseFixtures');
|
||||
const { setFixedTimeMode, setRealTimeMode, setStartOffset, setEndOffset } = require('../../../../appActions');
|
||||
|
||||
test.describe('Time conductor operations', () => {
|
||||
@@ -168,23 +168,3 @@ test.describe('Time conductor input fields real-time mode', () => {
|
||||
// select an option and verify the offsets are updated correctly
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Time Conductor History', () => {
|
||||
test("shows milliseconds on hover @unstable", async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/4386'
|
||||
});
|
||||
// Navigate to Open MCT in Fixed Time Mode, UTC Time System
|
||||
// with startBound at 2022-01-01 00:00:00.000Z
|
||||
// and endBound at 2022-01-01 00:00:00.200Z
|
||||
await page.goto('./#/browse/mine?view=grid&tc.mode=fixed&tc.startBound=1640995200000&tc.endBound=1640995200200&tc.timeSystem=utc&hideInspector=true', { waitUntil: 'networkidle' });
|
||||
await page.locator("[aria-label='Time Conductor History']").hover({ trial: true});
|
||||
await page.locator("[aria-label='Time Conductor History']").click();
|
||||
|
||||
// Validate history item format
|
||||
const historyItem = page.locator('text="2022-01-01 00:00:00 + 200ms"');
|
||||
await expect(historyItem).toBeEnabled();
|
||||
await expect(historyItem).toHaveAttribute('title', '2022-01-01 00:00:00.000 - 2022-01-01 00:00:00.200');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,7 +30,7 @@ test.describe('Timer', () => {
|
||||
timer = await createDomainObjectWithDefaults(page, { type: 'timer' });
|
||||
});
|
||||
|
||||
test('Can perform actions on the Timer', async ({ page }) => {
|
||||
test('Can perform actions on the Timer', async ({ page, openmctConfig }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/4313'
|
||||
|
||||
@@ -31,7 +31,7 @@ test.describe('Grand Search', () => {
|
||||
test('Can search for objects, and subsequent search dropdown behaves properly', async ({ page, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
|
||||
const createdObjects = await createObjectsForSearch(page);
|
||||
await createObjectsForSearch(page, myItemsFolderName);
|
||||
|
||||
// Click [aria-label="OpenMCT Search"] input[type="search"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
|
||||
@@ -41,8 +41,8 @@ test.describe('Grand Search', () => {
|
||||
await expect(page.locator('[aria-label="Search Result"] >> nth=1')).toContainText(`Clock B ${myItemsFolderName} Red Folder Blue Folder`);
|
||||
await expect(page.locator('[aria-label="Search Result"] >> nth=2')).toContainText(`Clock C ${myItemsFolderName} Red Folder Blue Folder`);
|
||||
await expect(page.locator('[aria-label="Search Result"] >> nth=3')).toContainText(`Clock D ${myItemsFolderName} Red Folder Blue Folder`);
|
||||
// Click the Elements pool to dismiss the search menu
|
||||
await page.locator('.l-pane__label:has-text("Elements")').click();
|
||||
// Click text=Elements >> nth=0
|
||||
await page.locator('text=Elements').first().click();
|
||||
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeHidden();
|
||||
|
||||
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click();
|
||||
@@ -77,7 +77,7 @@ test.describe('Grand Search', () => {
|
||||
await expect(page.locator('.is-object-type-clock')).toBeVisible();
|
||||
|
||||
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').fill('Disp');
|
||||
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toContainText(createdObjects.displayLayout.name);
|
||||
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toContainText('Unnamed Display Layout');
|
||||
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).not.toContainText('Folder');
|
||||
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Clock C');
|
||||
@@ -185,7 +185,7 @@ async function createFolderObject(page, folderName) {
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folderName);
|
||||
|
||||
// Create folder object
|
||||
await page.locator('button:has-text("OK")').click();
|
||||
await page.locator('text=OK').click();
|
||||
}
|
||||
|
||||
async function waitForSearchCompletion(page) {
|
||||
@@ -197,56 +197,75 @@ async function waitForSearchCompletion(page) {
|
||||
* Creates some domain objects for searching
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function createObjectsForSearch(page) {
|
||||
async function createObjectsForSearch(page, myItemsFolderName) {
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
const redFolder = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder',
|
||||
name: 'Red Folder'
|
||||
});
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
await page.locator('li:has-text("Folder") >> nth=1').click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"]').fill('Red Folder'),
|
||||
await page.locator(`text=Save In Open MCT ${myItemsFolderName} >> span`).nth(3).click(),
|
||||
page.locator('button:has-text("OK")').click()
|
||||
]);
|
||||
|
||||
const blueFolder = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder',
|
||||
name: 'Blue Folder',
|
||||
parent: redFolder.uuid
|
||||
});
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
await page.locator('li:has-text("Folder") >> nth=2').click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"]').fill('Blue Folder'),
|
||||
await page.locator('form[name="mctForm"] >> text=Red Folder').click(),
|
||||
page.locator('button:has-text("OK")').click()
|
||||
]);
|
||||
|
||||
const clockA = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Clock',
|
||||
name: 'Clock A',
|
||||
parent: blueFolder.uuid
|
||||
});
|
||||
const clockB = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Clock',
|
||||
name: 'Clock B',
|
||||
parent: blueFolder.uuid
|
||||
});
|
||||
const clockC = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Clock',
|
||||
name: 'Clock C',
|
||||
parent: blueFolder.uuid
|
||||
});
|
||||
const clockD = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Clock',
|
||||
name: 'Clock D',
|
||||
parent: blueFolder.uuid
|
||||
});
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
await page.locator('li[title="A digital clock that uses system time and supports a variety of display formats and timezones."]').click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"] >> nth=0').fill('Clock A'),
|
||||
await page.locator('form[name="mctForm"] >> text=Blue Folder').click(),
|
||||
page.locator('button:has-text("OK")').click()
|
||||
]);
|
||||
|
||||
const displayLayout = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Display Layout'
|
||||
});
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
await page.locator('li[title="A digital clock that uses system time and supports a variety of display formats and timezones."]').click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"] >> nth=0').fill('Clock B'),
|
||||
await page.locator('form[name="mctForm"] >> text=Blue Folder').click(),
|
||||
page.locator('button:has-text("OK")').click()
|
||||
]);
|
||||
|
||||
// Go back into edit mode for the display layout
|
||||
await page.locator('button[title="Edit"]').click();
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
await page.locator('li[title="A digital clock that uses system time and supports a variety of display formats and timezones."]').click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"] >> nth=0').fill('Clock C'),
|
||||
await page.locator('form[name="mctForm"] >> text=Blue Folder').click(),
|
||||
page.locator('button:has-text("OK")').click()
|
||||
]);
|
||||
|
||||
return {
|
||||
redFolder,
|
||||
blueFolder,
|
||||
clockA,
|
||||
clockB,
|
||||
clockC,
|
||||
clockD,
|
||||
displayLayout
|
||||
};
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
await page.locator('li[title="A digital clock that uses system time and supports a variety of display formats and timezones."]').click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"] >> nth=0').fill('Clock D'),
|
||||
await page.locator('form[name="mctForm"] >> text=Blue Folder').click(),
|
||||
page.locator('button:has-text("OK")').click()
|
||||
]);
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator(`a:has-text("${myItemsFolderName}") >> nth=0`).click()
|
||||
]);
|
||||
// Click button:has-text("Create")
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
// Click li:has-text("Notebook")
|
||||
await page.locator('li:has-text("Display Layout")').click();
|
||||
// Click button:has-text("OK")
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('button:has-text("OK")').click()
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ test.describe('Performance tests', () => {
|
||||
await page.setInputFiles('#fileElem', filePath);
|
||||
|
||||
// Click text=OK
|
||||
await page.locator('button:has-text("OK")').click();
|
||||
await page.locator('text=OK').click();
|
||||
|
||||
await expect(page.locator('a:has-text("Performance Display Layout Display Layout")')).toBeVisible();
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
/*
|
||||
Collection of Visual Tests set to run in a default context. The tests within this suite
|
||||
are only meant to run against openmct started by `npm start` within the
|
||||
are only meant to run against openmct's app.js started by `npm run start` within the
|
||||
`./e2e/playwright-visual.config.js` file.
|
||||
|
||||
*/
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
/*
|
||||
Collection of Visual Tests set to run in a default context. The tests within this suite
|
||||
are only meant to run against openmct started by `npm start` within the
|
||||
are only meant to run against openmct's app.js started by `npm run start` within the
|
||||
`./e2e/playwright-visual.config.js` file.
|
||||
|
||||
These should only use functional expect statements to verify assumptions about the state
|
||||
|
||||
@@ -75,8 +75,7 @@
|
||||
const TWO_HOURS = ONE_HOUR * 2;
|
||||
const ONE_DAY = ONE_HOUR * 24;
|
||||
|
||||
// openmct.install(openmct.plugins.LocalStorage());
|
||||
openmct.install(openmct.plugins.CouchDB("http://localhost:5984/openmct"));
|
||||
openmct.install(openmct.plugins.LocalStorage());
|
||||
|
||||
openmct.install(openmct.plugins.example.Generator());
|
||||
openmct.install(openmct.plugins.example.EventGeneratorPlugin());
|
||||
|
||||
12
jsdoc.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"source": {
|
||||
"include": [
|
||||
"src/"
|
||||
],
|
||||
"includePattern": "src/.+\\.js$",
|
||||
"excludePattern": ".+\\Spec\\.js$|lib/.+"
|
||||
},
|
||||
"plugins": [
|
||||
"plugins/markdown"
|
||||
]
|
||||
}
|
||||
@@ -23,32 +23,14 @@
|
||||
/*global module,process*/
|
||||
|
||||
module.exports = (config) => {
|
||||
let webpackConfig;
|
||||
let browsers;
|
||||
let singleRun;
|
||||
|
||||
if (process.env.KARMA_DEBUG) {
|
||||
webpackConfig = require('./webpack.dev.js');
|
||||
browsers = ['ChromeDebugging'];
|
||||
singleRun = false;
|
||||
} else {
|
||||
webpackConfig = require('./webpack.coverage.js');
|
||||
browsers = ['ChromeHeadless'];
|
||||
singleRun = true;
|
||||
}
|
||||
|
||||
const webpackConfig = require('./webpack.coverage.js');
|
||||
delete webpackConfig.output;
|
||||
// karma doesn't support webpack entry
|
||||
delete webpackConfig.entry;
|
||||
|
||||
config.set({
|
||||
basePath: '',
|
||||
frameworks: ['jasmine', 'webpack'],
|
||||
frameworks: ['jasmine'],
|
||||
files: [
|
||||
'indexTest.js',
|
||||
// included means: should the files be included in the browser using <script> tag?
|
||||
// We don't want them as a <script> because the shared worker source
|
||||
// needs loaded remotely by the shared worker process.
|
||||
{
|
||||
pattern: 'dist/couchDBChangesFeed.js*',
|
||||
included: false
|
||||
@@ -64,7 +46,7 @@ module.exports = (config) => {
|
||||
],
|
||||
port: 9876,
|
||||
reporters: ['spec', 'junit', 'coverage-istanbul'],
|
||||
browsers,
|
||||
browsers: [process.env.NODE_ENV === 'debug' ? 'ChromeDebugging' : 'ChromeHeadless'],
|
||||
client: {
|
||||
jasmine: {
|
||||
random: false,
|
||||
@@ -88,7 +70,6 @@ module.exports = (config) => {
|
||||
},
|
||||
coverageIstanbulReporter: {
|
||||
fixWebpackSourcePaths: true,
|
||||
skipFilesWithNoCoverage: true,
|
||||
dir: "coverage/unit", //Sets coverage file to be consumed by codecov.io
|
||||
reports: ['lcovonly']
|
||||
},
|
||||
@@ -109,7 +90,7 @@ module.exports = (config) => {
|
||||
stats: 'errors-warnings'
|
||||
},
|
||||
concurrency: 1,
|
||||
singleRun,
|
||||
singleRun: true,
|
||||
browserNoActivityTimeout: 400000
|
||||
});
|
||||
};
|
||||
|
||||
96
lighthouserc.yml
Normal file
@@ -0,0 +1,96 @@
|
||||
---
|
||||
ci:
|
||||
collect:
|
||||
urls:
|
||||
- http://localhost/
|
||||
numberOfRuns: 5
|
||||
settings:
|
||||
onlyCategories:
|
||||
- performance
|
||||
- best-practices
|
||||
upload:
|
||||
target: temporary-public-storage
|
||||
assert:
|
||||
preset: lighthouse:recommended
|
||||
assertions:
|
||||
### Applicable assertions
|
||||
bootup-time:
|
||||
- warn
|
||||
- minScore: 0.88 #Original value was calculated at 0.88
|
||||
dom-size:
|
||||
- error
|
||||
- maxNumericValue: 200 #Original value was calculated at 188
|
||||
first-contentful-paint:
|
||||
- error
|
||||
- minScore: 0.07 #Original value was calculated at 0.08
|
||||
mainthread-work-breakdown:
|
||||
- warn
|
||||
- minScore: 0.8 #Original value was calculated at 0.8
|
||||
unused-javascript:
|
||||
- warn
|
||||
- maxLength: 1
|
||||
- error
|
||||
- maxNumericValue: 2000 #Original value was calculated at 1855
|
||||
unused-css-rules: warn
|
||||
installable-manifest: warn
|
||||
service-worker: warn
|
||||
### Disabled seo, accessibility, and pwa assertions, below
|
||||
categories:seo: 'off'
|
||||
categories:accessibility: 'off'
|
||||
categories:pwa: 'off'
|
||||
accesskeys: 'off'
|
||||
apple-touch-icon: 'off'
|
||||
aria-allowed-attr: 'off'
|
||||
aria-command-name: 'off'
|
||||
aria-hidden-body: 'off'
|
||||
aria-hidden-focus: 'off'
|
||||
aria-input-field-name: 'off'
|
||||
aria-meter-name: 'off'
|
||||
aria-progressbar-name: 'off'
|
||||
aria-required-attr: 'off'
|
||||
aria-required-children: 'off'
|
||||
aria-required-parent: 'off'
|
||||
aria-roles: 'off'
|
||||
aria-toggle-field-name: 'off'
|
||||
aria-tooltip-name: 'off'
|
||||
aria-treeitem-name: 'off'
|
||||
aria-valid-attr: 'off'
|
||||
aria-valid-attr-value: 'off'
|
||||
button-name: 'off'
|
||||
bypass: 'off'
|
||||
canonical: 'off'
|
||||
color-contrast: 'off'
|
||||
content-width: 'off'
|
||||
crawlable-anchors: 'off'
|
||||
csp-xss: 'off'
|
||||
font-display: 'off'
|
||||
font-size: 'off'
|
||||
maskable-icon: 'off'
|
||||
heading-order: 'off'
|
||||
hreflang: 'off'
|
||||
html-has-lang: 'off'
|
||||
html-lang-valid: 'off'
|
||||
http-status-code: 'off'
|
||||
image-alt: 'off'
|
||||
input-image-alt: 'off'
|
||||
is-crawlable: 'off'
|
||||
label: 'off'
|
||||
link-name: 'off'
|
||||
link-text: 'off'
|
||||
list: 'off'
|
||||
listitem: 'off'
|
||||
meta-description: 'off'
|
||||
meta-refresh: 'off'
|
||||
meta-viewport: 'off'
|
||||
object-alt: 'off'
|
||||
plugins: 'off'
|
||||
robots-txt: 'off'
|
||||
splash-screen: 'off'
|
||||
tabindex: 'off'
|
||||
tap-targets: 'off'
|
||||
td-headers-attr: 'off'
|
||||
th-has-data-cells: 'off'
|
||||
themed-omnibox: 'off'
|
||||
valid-lang: 'off'
|
||||
video-caption: 'off'
|
||||
viewport: 'off'
|
||||
45
openmct.js
@@ -30,53 +30,8 @@ if (document.currentScript) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {object} BuildInfo
|
||||
* @property {string} version
|
||||
* @property {string} buildDate
|
||||
* @property {string} revision
|
||||
* @property {string} branch
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} OpenMCT
|
||||
* @property {BuildInfo} buildInfo
|
||||
* @property {*} selection
|
||||
* @property {import('./src/api/time/TimeAPI').default} time
|
||||
* @property {import('./src/api/composition/CompositionAPI').default} composition
|
||||
* @property {*} objectViews
|
||||
* @property {*} inspectorViews
|
||||
* @property {*} propertyEditors
|
||||
* @property {*} toolbars
|
||||
* @property {*} types
|
||||
* @property {import('./src/api/objects/ObjectAPI').default} objects
|
||||
* @property {import('./src/api/telemetry/TelemetryAPI').default} telemetry
|
||||
* @property {import('./src/api/indicators/IndicatorAPI').default} indicators
|
||||
* @property {import('./src/api/user/UserAPI').default} user
|
||||
* @property {import('./src/api/notifications/NotificationAPI').default} notifications
|
||||
* @property {import('./src/api/Editor').default} editor
|
||||
* @property {import('./src/api/overlays/OverlayAPI')} overlays
|
||||
* @property {import('./src/api/menu/MenuAPI').default} menus
|
||||
* @property {import('./src/api/actions/ActionsAPI').default} actions
|
||||
* @property {import('./src/api/status/StatusAPI').default} status
|
||||
* @property {*} priority
|
||||
* @property {import('./src/ui/router/ApplicationRouter')} router
|
||||
* @property {import('./src/api/faultmanagement/FaultManagementAPI').default} faults
|
||||
* @property {import('./src/api/forms/FormsAPI').default} forms
|
||||
* @property {import('./src/api/Branding').default} branding
|
||||
* @property {import('./src/api/annotation/AnnotationAPI').default} annotation
|
||||
* @property {{(plugin: OpenMCTPlugin) => void}} install
|
||||
* @property {{() => string}} getAssetPath
|
||||
* @property {{(domElement: HTMLElement, isHeadlessMode: boolean) => void}} start
|
||||
* @property {{() => void}} startHeadless
|
||||
* @property {{() => void}} destroy
|
||||
* @property {OpenMCTPlugin[]} plugins
|
||||
* @property {OpenMCTComponent[]} components
|
||||
*/
|
||||
|
||||
const MCT = require('./src/MCT');
|
||||
|
||||
/** @type {OpenMCT} */
|
||||
const openmct = new MCT();
|
||||
|
||||
module.exports = openmct;
|
||||
|
||||
67
package.json
@@ -1,35 +1,40 @@
|
||||
{
|
||||
"name": "openmct",
|
||||
"version": "2.1.4-SNAPSHOT",
|
||||
"version": "2.1.1-SNAPSHOT",
|
||||
"description": "The Open MCT core platform",
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "7.18.9",
|
||||
"@braintree/sanitize-url": "6.0.2",
|
||||
"@percy/cli": "1.16.0",
|
||||
"@braintree/sanitize-url": "6.0.0",
|
||||
"@percy/cli": "1.10.3",
|
||||
"@percy/playwright": "1.0.4",
|
||||
"@playwright/test": "1.25.2",
|
||||
"@types/jasmine": "4.3.0",
|
||||
"@types/lodash": "4.14.189",
|
||||
"babel-loader": "9.0.0",
|
||||
"@types/eventemitter3": "^1.0.0",
|
||||
"@types/jasmine": "^4.0.1",
|
||||
"@types/karma": "^6.3.2",
|
||||
"@types/lodash": "^4.14.178",
|
||||
"@types/mocha": "^9.1.0",
|
||||
"babel-loader": "8.2.5",
|
||||
"babel-plugin-istanbul": "6.1.1",
|
||||
"codecov": "3.8.3",
|
||||
"comma-separated-values": "3.6.4",
|
||||
"codecov":"3.8.3",
|
||||
"copy-webpack-plugin": "11.0.0",
|
||||
"cross-env": "7.0.3",
|
||||
"css-loader": "6.7.1",
|
||||
"d3-axis": "3.0.0",
|
||||
"d3-scale": "3.3.0",
|
||||
"d3-selection": "3.0.0",
|
||||
"eslint": "8.27.0",
|
||||
"eslint": "8.23.1",
|
||||
"eslint-plugin-compat": "4.0.2",
|
||||
"eslint-plugin-playwright": "0.11.2",
|
||||
"eslint-plugin-vue": "9.7.0",
|
||||
"eslint-plugin-vue": "9.3.0",
|
||||
"eslint-plugin-you-dont-need-lodash-underscore": "6.12.0",
|
||||
"eventemitter3": "1.2.0",
|
||||
"express": "4.13.1",
|
||||
"file-saver": "2.0.5",
|
||||
"git-rev-sync": "3.0.2",
|
||||
"html2canvas": "1.4.1",
|
||||
"imports-loader": "4.0.1",
|
||||
"jasmine-core": "4.5.0",
|
||||
"jasmine-core": "4.4.0",
|
||||
"karma": "6.3.20",
|
||||
"karma-chrome-launcher": "3.1.1",
|
||||
"karma-cli": "2.0.0",
|
||||
@@ -45,43 +50,42 @@
|
||||
"mini-css-extract-plugin": "2.6.1",
|
||||
"moment": "2.29.4",
|
||||
"moment-duration-format": "2.3.2",
|
||||
"moment-timezone": "0.5.38",
|
||||
"nyc": "15.1.0",
|
||||
"moment-timezone": "0.5.37",
|
||||
"nyc":"15.1.0",
|
||||
"painterro": "1.2.78",
|
||||
"playwright-core": "1.25.2",
|
||||
"plotly.js-basic-dist": "2.14.0",
|
||||
"plotly.js-gl2d-dist": "2.14.0",
|
||||
"printj": "1.3.1",
|
||||
"request": "2.88.2",
|
||||
"resolve-url-loader": "5.0.0",
|
||||
"sass": "1.56.1",
|
||||
"sass": "1.55.0",
|
||||
"sass-loader": "13.0.2",
|
||||
"sinon": "14.0.1",
|
||||
"sinon": "14.0.0",
|
||||
"style-loader": "^3.3.1",
|
||||
"typescript": "4.9.3",
|
||||
"uuid": "9.0.0",
|
||||
"vue": "2.6.14",
|
||||
"vue-eslint-parser": "9.1.0",
|
||||
"vue-loader": "15.9.8",
|
||||
"vue-template-compiler": "2.6.14",
|
||||
"webpack": "5.74.0",
|
||||
"webpack-cli": "5.0.0",
|
||||
"webpack-dev-server": "4.11.1",
|
||||
"webpack-cli": "4.10.0",
|
||||
"webpack-dev-middleware": "5.3.3",
|
||||
"webpack-hot-middleware": "2.25.2",
|
||||
"webpack-merge": "5.8.0"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rm -rf ./dist ./node_modules ./package-lock.json",
|
||||
"clean-test-lint": "npm run clean; npm install; npm run test; npm run lint",
|
||||
"start": "npx webpack serve --config ./webpack.dev.js",
|
||||
"start:coverage": "npx webpack serve --config ./webpack.coverage.js",
|
||||
"start": "node app.js",
|
||||
"lint": "eslint example src e2e --ext .js,.vue openmct.js --max-warnings=0",
|
||||
"lint:fix": "eslint example src e2e --ext .js,.vue openmct.js --fix",
|
||||
"build:prod": "webpack --config webpack.prod.js",
|
||||
"build:prod": "cross-env webpack --config webpack.prod.js",
|
||||
"build:dev": "webpack --config webpack.dev.js",
|
||||
"build:coverage": "webpack --config webpack.coverage.js",
|
||||
"build:watch": "webpack --config webpack.dev.js --watch",
|
||||
"info": "npx envinfo --system --browsers --npmPackages --binaries --languages --markdown",
|
||||
"test": "karma start",
|
||||
"test:debug": "KARMA_DEBUG=true karma start",
|
||||
"test": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run",
|
||||
"test:debug": "cross-env NODE_ENV=debug karma start --no-single-run",
|
||||
"test:e2e": "npx playwright test",
|
||||
"test:e2e:couchdb": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @couchdb",
|
||||
"test:e2e:stable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep-invert \"@unstable|@couchdb\"",
|
||||
@@ -91,13 +95,14 @@
|
||||
"test:e2e:visual": "percy exec --config ./e2e/.percy.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:perf": "npx playwright test --config=e2e/playwright-performance.config.js",
|
||||
"test:watch": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --no-single-run",
|
||||
"update-about-dialog-copyright": "perl -pi -e 's/20\\d\\d\\-202\\d/2014\\-2022/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\\-2022/gm'",
|
||||
"cov:e2e:report": "nyc report --reporter=lcovonly --report-dir=./coverage/e2e",
|
||||
"cov:e2e:full:publish": "codecov --disable=gcov -f ./coverage/e2e/lcov.info -F e2e-full",
|
||||
"cov:e2e:stable:publish": "codecov --disable=gcov -f ./coverage/e2e/lcov.info -F e2e-stable",
|
||||
"cov:unit:publish": "codecov --disable=gcov -f ./coverage/unit/lcov.info -F unit",
|
||||
"prepare": "npm run build:prod && npx tsc"
|
||||
"cov:e2e:report":"nyc report --reporter=lcovonly --report-dir=./coverage/e2e",
|
||||
"cov:e2e:full:publish":"codecov --disable=gcov -f ./coverage/e2e/lcov.info -F e2e-full",
|
||||
"cov:e2e:stable:publish":"codecov --disable=gcov -f ./coverage/e2e/lcov.info -F e2e-stable",
|
||||
"cov:unit:publish":"codecov --disable=gcov -f ./coverage/unit/lcov.info -F unit",
|
||||
"prepare": "npm run build:prod"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -106,6 +111,9 @@
|
||||
"engines": {
|
||||
"node": ">=14.19.1"
|
||||
},
|
||||
"overrides": {
|
||||
"core-js": "3.21.1"
|
||||
},
|
||||
"browserslist": [
|
||||
"Firefox ESR",
|
||||
"not IE 11",
|
||||
@@ -114,5 +122,6 @@
|
||||
"ios_saf > 15"
|
||||
],
|
||||
"author": "",
|
||||
"license": "Apache-2.0"
|
||||
"license": "Apache-2.0",
|
||||
"private": true
|
||||
}
|
||||
|
||||
10
src/MCT.js
@@ -19,7 +19,7 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
/* eslint-disable no-undef */
|
||||
|
||||
define([
|
||||
'EventEmitter',
|
||||
'./api/api',
|
||||
@@ -81,11 +81,13 @@ define([
|
||||
/**
|
||||
* The Open MCT application. This may be configured by installing plugins
|
||||
* or registering extensions before the application is started.
|
||||
* @constructor
|
||||
* @class MCT
|
||||
* @memberof module:openmct
|
||||
* @augments {EventEmitter}
|
||||
*/
|
||||
function MCT() {
|
||||
EventEmitter.call(this);
|
||||
/* eslint-disable no-undef */
|
||||
this.buildInfo = {
|
||||
version: __OPENMCT_VERSION__,
|
||||
buildDate: __OPENMCT_BUILD_DATE__,
|
||||
@@ -99,7 +101,7 @@ define([
|
||||
* Tracks current selection state of the application.
|
||||
* @private
|
||||
*/
|
||||
['selection', () => new Selection.default(this)],
|
||||
['selection', () => new Selection(this)],
|
||||
|
||||
/**
|
||||
* MCT's time conductor, which may be used to synchronize view contents
|
||||
@@ -123,7 +125,7 @@ define([
|
||||
* @memberof module:openmct.MCT#
|
||||
* @name composition
|
||||
*/
|
||||
['composition', () => new api.CompositionAPI.default(this)],
|
||||
['composition', () => new api.CompositionAPI(this)],
|
||||
|
||||
/**
|
||||
* Registry for views of domain objects which should appear in the
|
||||
|
||||
@@ -23,7 +23,8 @@
|
||||
let brandingOptions = {};
|
||||
|
||||
/**
|
||||
* @typedef {object} BrandingOptions
|
||||
* @typedef {Object} BrandingOptions
|
||||
* @memberOf openmct/branding
|
||||
* @property {string} smallLogoImage URL to the image to use as the applications logo.
|
||||
* This logo will appear on every screen and when clicked will launch the about dialog.
|
||||
* @property {string} aboutHtml Custom content for the about screen. When defined the
|
||||
|
||||
@@ -56,12 +56,18 @@ export default class Editor extends EventEmitter {
|
||||
* Save any unsaved changes from this editing session. This will
|
||||
* end the current transaction.
|
||||
*/
|
||||
async save() {
|
||||
save() {
|
||||
const transaction = this.openmct.objects.getActiveTransaction();
|
||||
await transaction.commit();
|
||||
this.editing = false;
|
||||
this.emit('isEditing', false);
|
||||
this.openmct.objects.endTransaction();
|
||||
|
||||
return transaction.commit()
|
||||
.then(() => {
|
||||
this.editing = false;
|
||||
this.emit('isEditing', false);
|
||||
}).catch(error => {
|
||||
throw error;
|
||||
}).finally(() => {
|
||||
this.openmct.objects.endTransaction();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import {
|
||||
createOpenMct, resetApplicationState
|
||||
} from '../utils/testing';
|
||||
|
||||
describe('The Editor API', () => {
|
||||
let openmct;
|
||||
|
||||
beforeEach((done) => {
|
||||
openmct = createOpenMct();
|
||||
openmct.on('start', done);
|
||||
|
||||
spyOn(openmct.objects, 'endTransaction');
|
||||
|
||||
openmct.startHeadless();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
return resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
it('opens a transaction on edit', () => {
|
||||
expect(
|
||||
openmct.objects.isTransactionActive()
|
||||
).toBeFalse();
|
||||
openmct.editor.edit();
|
||||
expect(
|
||||
openmct.objects.isTransactionActive()
|
||||
).toBeTrue();
|
||||
});
|
||||
|
||||
it('closes an open transaction on successful save', async () => {
|
||||
spyOn(openmct.objects, 'getActiveTransaction')
|
||||
.and.returnValue({
|
||||
commit: () => Promise.resolve(true)
|
||||
});
|
||||
|
||||
openmct.editor.edit();
|
||||
await openmct.editor.save();
|
||||
|
||||
expect(
|
||||
openmct.objects.endTransaction
|
||||
).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not close an open transaction on failed save', async () => {
|
||||
spyOn(openmct.objects, 'getActiveTransaction')
|
||||
.and.returnValue({
|
||||
commit: () => Promise.reject()
|
||||
});
|
||||
|
||||
openmct.editor.edit();
|
||||
await openmct.editor.save().catch(() => {});
|
||||
|
||||
expect(
|
||||
openmct.objects.endTransaction
|
||||
).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -22,7 +22,6 @@
|
||||
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import EventEmitter from 'EventEmitter';
|
||||
import _ from 'lodash';
|
||||
|
||||
/**
|
||||
* @readonly
|
||||
@@ -43,28 +42,19 @@ const ANNOTATION_TYPES = Object.freeze({
|
||||
|
||||
const ANNOTATION_TYPE = 'annotation';
|
||||
|
||||
const ANNOTATION_LAST_CREATED = 'annotationLastCreated';
|
||||
|
||||
/**
|
||||
* @typedef {Object} Tag
|
||||
* @property {String} key a unique identifier for the tag
|
||||
* @property {String} backgroundColor eg. "#cc0000"
|
||||
* @property {String} foregroundColor eg. "#ffffff"
|
||||
*/
|
||||
|
||||
export default class AnnotationAPI extends EventEmitter {
|
||||
|
||||
/**
|
||||
* @param {OpenMCT} openmct
|
||||
*/
|
||||
constructor(openmct) {
|
||||
super();
|
||||
this.openmct = openmct;
|
||||
this.availableTags = {};
|
||||
|
||||
this.ANNOTATION_TYPES = ANNOTATION_TYPES;
|
||||
this.ANNOTATION_TYPE = ANNOTATION_TYPE;
|
||||
this.ANNOTATION_LAST_CREATED = ANNOTATION_LAST_CREATED;
|
||||
|
||||
this.openmct.types.addType(ANNOTATION_TYPE, {
|
||||
name: 'Annotation',
|
||||
@@ -73,7 +63,6 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
cssClass: 'icon-notebook',
|
||||
initialize: function (domainObject) {
|
||||
domainObject.targets = domainObject.targets || {};
|
||||
domainObject._deleted = domainObject._deleted || false;
|
||||
domainObject.originalContextPath = domainObject.originalContextPath || '';
|
||||
domainObject.tags = domainObject.tags || [];
|
||||
domainObject.contentText = domainObject.contentText || '';
|
||||
@@ -123,7 +112,6 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
namespace
|
||||
},
|
||||
tags,
|
||||
_deleted: false,
|
||||
annotationType,
|
||||
contentText,
|
||||
originalContextPath
|
||||
@@ -139,7 +127,6 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
const success = await this.openmct.objects.save(createdObject);
|
||||
if (success) {
|
||||
this.emit('annotationCreated', createdObject);
|
||||
this.#updateAnnotationModified(domainObject);
|
||||
|
||||
return createdObject;
|
||||
} else {
|
||||
@@ -147,32 +134,14 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
#updateAnnotationModified(domainObject) {
|
||||
this.openmct.objects.mutate(domainObject, this.ANNOTATION_LAST_CREATED, Date.now());
|
||||
}
|
||||
|
||||
/**
|
||||
* @method defineTag
|
||||
* @param {String} key a unique identifier for the tag
|
||||
* @param {Tag} tagsDefinition the definition of the tag to add
|
||||
*/
|
||||
defineTag(tagKey, tagsDefinition) {
|
||||
this.availableTags[tagKey] = tagsDefinition;
|
||||
}
|
||||
|
||||
/**
|
||||
* @method isAnnotation
|
||||
* @param {import('../objects/ObjectAPI').DomainObject} domainObject domainObject the domainObject in question
|
||||
* @returns {Boolean} Returns true if the domain object is an annotation
|
||||
*/
|
||||
isAnnotation(domainObject) {
|
||||
return domainObject && (domainObject.type === ANNOTATION_TYPE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @method getAvailableTags
|
||||
* @returns {Tag[]} Returns an array of the available tags that have been loaded
|
||||
*/
|
||||
getAvailableTags() {
|
||||
if (this.availableTags) {
|
||||
const rearrangedToArray = Object.keys(this.availableTags).map(tagKey => {
|
||||
@@ -188,26 +157,18 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @method getAnnotations
|
||||
* @param {String} query - The keystring of the domain object to search for annotations for
|
||||
* @returns {import('../objects/ObjectAPI').DomainObject[]} Returns an array of domain objects that match the search query
|
||||
*/
|
||||
async getAnnotations(query) {
|
||||
const searchResults = (await Promise.all(this.openmct.objects.search(query, null, this.openmct.objects.SEARCH_TYPES.ANNOTATIONS))).flat();
|
||||
async getAnnotation(query, searchType) {
|
||||
let foundAnnotation = null;
|
||||
|
||||
return searchResults;
|
||||
const searchResults = (await Promise.all(this.openmct.objects.search(query, null, searchType))).flat();
|
||||
if (searchResults) {
|
||||
foundAnnotation = searchResults[0];
|
||||
}
|
||||
|
||||
return foundAnnotation;
|
||||
}
|
||||
|
||||
/**
|
||||
* @method addSingleAnnotationTag
|
||||
* @param {import('../objects/ObjectAPI').DomainObject=} existingAnnotation - An optional annotation to add the tag to. If not specified, we will create an annotation.
|
||||
* @param {import('../objects/ObjectAPI').DomainObject} targetDomainObject - The domain object the annotation will point to.
|
||||
* @param {Object=} targetSpecificDetails - Optional object to add to the target object. E.g., for notebooks this would be an entryID
|
||||
* @param {AnnotationType} annotationType - The type of annotation this is for.
|
||||
* @returns {import('../objects/ObjectAPI').DomainObject[]} Returns the annotation that was either created or passed as an existingAnnotation
|
||||
*/
|
||||
async addSingleAnnotationTag(existingAnnotation, targetDomainObject, targetSpecificDetails, annotationType, tag) {
|
||||
async addAnnotationTag(existingAnnotation, targetDomainObject, targetSpecificDetails, annotationType, tag) {
|
||||
if (!existingAnnotation) {
|
||||
const targets = {};
|
||||
const targetKeyString = this.openmct.objects.makeKeyString(targetDomainObject.identifier);
|
||||
@@ -225,44 +186,27 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
|
||||
return newAnnotation;
|
||||
} else {
|
||||
if (!existingAnnotation.tags.includes(tag)) {
|
||||
throw new Error(`Existing annotation did not contain tag ${tag}`);
|
||||
}
|
||||
|
||||
if (existingAnnotation._deleted) {
|
||||
this.unDeleteAnnotation(existingAnnotation);
|
||||
}
|
||||
const tagArray = [tag, ...existingAnnotation.tags];
|
||||
this.openmct.objects.mutate(existingAnnotation, 'tags', tagArray);
|
||||
|
||||
return existingAnnotation;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @method deleteAnnotations
|
||||
* @param {import('../objects/ObjectAPI').DomainObject[]} existingAnnotation - An array of annotations to delete (set _deleted to true)
|
||||
*/
|
||||
deleteAnnotations(annotations) {
|
||||
if (!annotations) {
|
||||
throw new Error('Asked to delete null annotations! 🙅♂️');
|
||||
removeAnnotationTag(existingAnnotation, tagToRemove) {
|
||||
if (existingAnnotation && existingAnnotation.tags.includes(tagToRemove)) {
|
||||
const cleanedArray = existingAnnotation.tags.filter(extantTag => extantTag !== tagToRemove);
|
||||
this.openmct.objects.mutate(existingAnnotation, 'tags', cleanedArray);
|
||||
} else {
|
||||
throw new Error(`Asked to remove tag (${tagToRemove}) that doesn't exist`, existingAnnotation);
|
||||
}
|
||||
|
||||
annotations.forEach(annotation => {
|
||||
if (!annotation._deleted) {
|
||||
this.openmct.objects.mutate(annotation, '_deleted', true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @method deleteAnnotations
|
||||
* @param {import('../objects/ObjectAPI').DomainObject} existingAnnotation - An annotations to undelete (set _deleted to false)
|
||||
*/
|
||||
unDeleteAnnotation(annotation) {
|
||||
if (!annotation) {
|
||||
throw new Error('Asked to undelete null annotation! 🙅♂️');
|
||||
removeAnnotationTags(existingAnnotation) {
|
||||
// just removes tags on the annotation as we can't really delete objects
|
||||
if (existingAnnotation && existingAnnotation.tags) {
|
||||
this.openmct.objects.mutate(existingAnnotation, 'tags', []);
|
||||
}
|
||||
|
||||
this.openmct.objects.mutate(annotation, '_deleted', false);
|
||||
}
|
||||
|
||||
#getMatchingTags(query) {
|
||||
@@ -322,40 +266,16 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
return modelAddedToResults;
|
||||
}
|
||||
|
||||
#combineSameTargets(results) {
|
||||
const combinedResults = [];
|
||||
results.forEach(currentAnnotation => {
|
||||
const existingAnnotation = combinedResults.find((annotationToFind) => {
|
||||
return _.isEqual(currentAnnotation.targets, annotationToFind.targets);
|
||||
});
|
||||
if (!existingAnnotation) {
|
||||
combinedResults.push(currentAnnotation);
|
||||
} else {
|
||||
existingAnnotation.tags.push(...currentAnnotation.tags);
|
||||
}
|
||||
});
|
||||
|
||||
return combinedResults;
|
||||
}
|
||||
|
||||
/**
|
||||
* @method searchForTags
|
||||
* @param {String} query A query to match against tags. E.g., "dr" will match the tags "drilling" and "driving"
|
||||
* @param {Object} [abortController] An optional abort method to stop the query
|
||||
* @param {Object} abortController An optional abort method to stop the query
|
||||
* @returns {Promise} returns a model of matching tags with their target domain objects attached
|
||||
*/
|
||||
async searchForTags(query, abortController) {
|
||||
const matchingTagKeys = this.#getMatchingTags(query);
|
||||
if (!matchingTagKeys.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const searchResults = (await Promise.all(this.openmct.objects.search(matchingTagKeys, abortController, this.openmct.objects.SEARCH_TYPES.TAGS))).flat();
|
||||
const filteredDeletedResults = searchResults.filter((result) => {
|
||||
return !(result._deleted);
|
||||
});
|
||||
const combinedSameTargets = this.#combineSameTargets(filteredDeletedResults);
|
||||
const appliedTagSearchResults = this.#addTagMetaInformationToResults(combinedSameTargets, matchingTagKeys);
|
||||
const appliedTagSearchResults = this.#addTagMetaInformationToResults(searchResults, matchingTagKeys);
|
||||
const appliedTargetsModels = await this.#addTargetModelsToResults(appliedTagSearchResults);
|
||||
const resultsWithValidPath = appliedTargetsModels.filter(result => {
|
||||
return this.openmct.objects.isReachable(result.targetModels?.[0]?.originalPath);
|
||||
|
||||
@@ -94,6 +94,7 @@ describe("The Annotation API", () => {
|
||||
openmct.startHeadless();
|
||||
});
|
||||
afterEach(async () => {
|
||||
openmct.objects.providers = {};
|
||||
await resetApplicationState(openmct);
|
||||
});
|
||||
it("is defined", () => {
|
||||
@@ -125,44 +126,34 @@ describe("The Annotation API", () => {
|
||||
|
||||
describe("Tagging", () => {
|
||||
it("can create a tag", async () => {
|
||||
const annotationObject = await openmct.annotation.addSingleAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
|
||||
const annotationObject = await openmct.annotation.addAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
|
||||
expect(annotationObject).toBeDefined();
|
||||
expect(annotationObject.type).toEqual('annotation');
|
||||
expect(annotationObject.tags).toContain('aWonderfulTag');
|
||||
});
|
||||
it("can delete a tag", async () => {
|
||||
const annotationObject = await openmct.annotation.addSingleAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
|
||||
const originalAnnotationObject = await openmct.annotation.addAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
|
||||
const annotationObject = await openmct.annotation.addAnnotationTag(originalAnnotationObject, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'anotherTagToRemove');
|
||||
expect(annotationObject).toBeDefined();
|
||||
openmct.annotation.deleteAnnotations([annotationObject]);
|
||||
expect(annotationObject._deleted).toBeTrue();
|
||||
openmct.annotation.removeAnnotationTag(annotationObject, 'anotherTagToRemove');
|
||||
expect(annotationObject.tags).toEqual(['aWonderfulTag']);
|
||||
openmct.annotation.removeAnnotationTag(annotationObject, 'aWonderfulTag');
|
||||
expect(annotationObject.tags).toEqual([]);
|
||||
});
|
||||
it("throws an error if deleting non-existent tag", async () => {
|
||||
const annotationObject = await openmct.annotation.addSingleAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
|
||||
const annotationObject = await openmct.annotation.addAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
|
||||
expect(annotationObject).toBeDefined();
|
||||
expect(() => {
|
||||
openmct.annotation.removeAnnotationTag(annotationObject, 'ThisTagShouldNotExist');
|
||||
}).toThrow();
|
||||
});
|
||||
it("can remove all tags", async () => {
|
||||
const annotationObject = await openmct.annotation.addSingleAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
|
||||
const annotationObject = await openmct.annotation.addAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
|
||||
expect(annotationObject).toBeDefined();
|
||||
expect(() => {
|
||||
openmct.annotation.deleteAnnotations([annotationObject]);
|
||||
openmct.annotation.removeAnnotationTags(annotationObject);
|
||||
}).not.toThrow();
|
||||
expect(annotationObject._deleted).toBeTrue();
|
||||
});
|
||||
it("can add/delete/add a tag", async () => {
|
||||
let annotationObject = await openmct.annotation.addSingleAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
|
||||
expect(annotationObject).toBeDefined();
|
||||
expect(annotationObject.type).toEqual('annotation');
|
||||
expect(annotationObject.tags).toContain('aWonderfulTag');
|
||||
openmct.annotation.deleteAnnotations([annotationObject]);
|
||||
expect(annotationObject._deleted).toBeTrue();
|
||||
annotationObject = await openmct.annotation.addSingleAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
|
||||
expect(annotationObject).toBeDefined();
|
||||
expect(annotationObject.type).toEqual('annotation');
|
||||
expect(annotationObject.tags).toContain('aWonderfulTag');
|
||||
expect(annotationObject._deleted).toBeFalse();
|
||||
expect(annotationObject.tags).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -184,10 +175,16 @@ describe("The Annotation API", () => {
|
||||
expect(results).toBeDefined();
|
||||
expect(results.length).toEqual(1);
|
||||
});
|
||||
it("returns no tags for empty search", async () => {
|
||||
const results = await openmct.annotation.searchForTags('q');
|
||||
it("can get notebook annotations", async () => {
|
||||
const targetKeyString = openmct.objects.makeKeyString(mockDomainObject.identifier);
|
||||
const query = {
|
||||
targetKeyString,
|
||||
entryId: 'fooBarEntry'
|
||||
};
|
||||
|
||||
const results = await openmct.annotation.getAnnotation(query, openmct.objects.SEARCH_TYPES.NOTEBOOK_ANNOTATIONS);
|
||||
expect(results).toBeDefined();
|
||||
expect(results.length).toEqual(0);
|
||||
expect(results.tags.length).toEqual(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,9 +37,7 @@ define([
|
||||
'./types/TypeRegistry',
|
||||
'./user/UserAPI',
|
||||
'./annotation/AnnotationAPI'
|
||||
],
|
||||
|
||||
function (
|
||||
], function (
|
||||
ActionsAPI,
|
||||
CompositionAPI,
|
||||
EditorAPI,
|
||||
|
||||
@@ -20,41 +20,34 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import DefaultCompositionProvider from './DefaultCompositionProvider';
|
||||
import CompositionCollection from './CompositionCollection';
|
||||
|
||||
/**
|
||||
* @typedef {import('./CompositionProvider').default} CompositionProvider
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('../objects/ObjectAPI').DomainObject} DomainObject
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('../../../openmct').OpenMCT} OpenMCT
|
||||
*/
|
||||
|
||||
/**
|
||||
* An interface for interacting with the composition of domain objects.
|
||||
* The composition of a domain object is the list of other domain objects
|
||||
* it "contains" (for instance, that should be displayed beneath it
|
||||
* in the tree.)
|
||||
* @constructor
|
||||
*/
|
||||
export default class CompositionAPI {
|
||||
define([
|
||||
'lodash',
|
||||
'EventEmitter',
|
||||
'./DefaultCompositionProvider',
|
||||
'./CompositionCollection'
|
||||
], function (
|
||||
_,
|
||||
EventEmitter,
|
||||
DefaultCompositionProvider,
|
||||
CompositionCollection
|
||||
) {
|
||||
/**
|
||||
* @param {OpenMCT} publicAPI
|
||||
* An interface for interacting with the composition of domain objects.
|
||||
* The composition of a domain object is the list of other domain objects
|
||||
* it "contains" (for instance, that should be displayed beneath it
|
||||
* in the tree.)
|
||||
*
|
||||
* @interface CompositionAPI
|
||||
* @returns {module:openmct.CompositionCollection}
|
||||
* @memberof module:openmct
|
||||
*/
|
||||
constructor(publicAPI) {
|
||||
/** @type {CompositionProvider[]} */
|
||||
function CompositionAPI(publicAPI) {
|
||||
this.registry = [];
|
||||
/** @type {CompositionPolicy[]} */
|
||||
this.policies = [];
|
||||
this.addProvider(new DefaultCompositionProvider(publicAPI, this));
|
||||
/** @type {OpenMCT} */
|
||||
this.publicAPI = publicAPI;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a composition provider.
|
||||
*
|
||||
@@ -62,19 +55,21 @@ export default class CompositionAPI {
|
||||
* behavior for certain domain objects.
|
||||
*
|
||||
* @method addProvider
|
||||
* @param {CompositionProvider} provider the provider to add
|
||||
* @param {module:openmct.CompositionProvider} provider the provider to add
|
||||
* @memberof module:openmct.CompositionAPI#
|
||||
*/
|
||||
addProvider(provider) {
|
||||
CompositionAPI.prototype.addProvider = function (provider) {
|
||||
this.registry.unshift(provider);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the composition (if any) of this domain object.
|
||||
*
|
||||
* @method get
|
||||
* @param {DomainObject} domainObject
|
||||
* @returns {CompositionCollection}
|
||||
* @returns {module:openmct.CompositionCollection}
|
||||
* @memberof module:openmct.CompositionAPI#
|
||||
*/
|
||||
get(domainObject) {
|
||||
CompositionAPI.prototype.get = function (domainObject) {
|
||||
const provider = this.registry.find(p => {
|
||||
return p.appliesTo(domainObject);
|
||||
});
|
||||
@@ -84,7 +79,8 @@ export default class CompositionAPI {
|
||||
}
|
||||
|
||||
return new CompositionCollection(domainObject, provider, this.publicAPI);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A composition policy is a function which either allows or disallows
|
||||
* placing one object in another's composition.
|
||||
@@ -94,51 +90,52 @@ export default class CompositionAPI {
|
||||
* generally be written to return true in the default case.
|
||||
*
|
||||
* @callback CompositionPolicy
|
||||
* @param {DomainObject} containingObject the object which
|
||||
* @memberof module:openmct.CompositionAPI~
|
||||
* @param {module:openmct.DomainObject} containingObject the object which
|
||||
* would act as a container
|
||||
* @param {DomainObject} containedObject the object which
|
||||
* @param {module:openmct.DomainObject} containedObject the object which
|
||||
* would be contained
|
||||
* @returns {boolean} false if this composition should be disallowed
|
||||
*/
|
||||
|
||||
/**
|
||||
* Add a composition policy. Composition policies may disallow domain
|
||||
* objects from containing other domain objects.
|
||||
*
|
||||
* @method addPolicy
|
||||
* @param {CompositionPolicy} policy
|
||||
* @param {module:openmct.CompositionAPI~CompositionPolicy} policy
|
||||
* the policy to add
|
||||
* @memberof module:openmct.CompositionAPI#
|
||||
*/
|
||||
addPolicy(policy) {
|
||||
CompositionAPI.prototype.addPolicy = function (policy) {
|
||||
this.policies.push(policy);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check whether or not a domain object is allowed to contain another
|
||||
* domain object.
|
||||
*
|
||||
* @private
|
||||
* @method checkPolicy
|
||||
* @param {DomainObject} container the object which
|
||||
* @param {module:openmct.DomainObject} containingObject the object which
|
||||
* would act as a container
|
||||
* @param {DomainObject} containee the object which
|
||||
* @param {module:openmct.DomainObject} containedObject the object which
|
||||
* would be contained
|
||||
* @returns {boolean} false if this composition should be disallowed
|
||||
* @param {CompositionPolicy} policy
|
||||
|
||||
* @param {module:openmct.CompositionAPI~CompositionPolicy} policy
|
||||
* the policy to add
|
||||
* @memberof module:openmct.CompositionAPI#
|
||||
*/
|
||||
checkPolicy(container, containee) {
|
||||
CompositionAPI.prototype.checkPolicy = function (container, containee) {
|
||||
return this.policies.every(function (policy) {
|
||||
return policy(container, containee);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check whether or not a domainObject supports composition
|
||||
*
|
||||
* @param {DomainObject} domainObject
|
||||
* @returns {boolean} true if the domainObject supports composition
|
||||
*/
|
||||
supportsComposition(domainObject) {
|
||||
CompositionAPI.prototype.supportsComposition = function (domainObject) {
|
||||
return this.get(domainObject) !== undefined;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return CompositionAPI;
|
||||
});
|
||||
|
||||
@@ -1,319 +1,325 @@
|
||||
import CompositionAPI from './CompositionAPI';
|
||||
import CompositionCollection from './CompositionCollection';
|
||||
define([
|
||||
'./CompositionAPI',
|
||||
'./CompositionCollection'
|
||||
], function (
|
||||
CompositionAPI,
|
||||
CompositionCollection
|
||||
) {
|
||||
|
||||
describe('The Composition API', function () {
|
||||
let publicAPI;
|
||||
let compositionAPI;
|
||||
let topicService;
|
||||
let mutationTopic;
|
||||
|
||||
beforeEach(function () {
|
||||
|
||||
mutationTopic = jasmine.createSpyObj('mutationTopic', [
|
||||
'listen'
|
||||
]);
|
||||
topicService = jasmine.createSpy('topicService');
|
||||
topicService.and.returnValue(mutationTopic);
|
||||
publicAPI = {};
|
||||
publicAPI.objects = jasmine.createSpyObj('ObjectAPI', [
|
||||
'get',
|
||||
'mutate',
|
||||
'observe',
|
||||
'areIdsEqual'
|
||||
]);
|
||||
|
||||
publicAPI.objects.areIdsEqual.and.callFake(function (id1, id2) {
|
||||
return id1.namespace === id2.namespace && id1.key === id2.key;
|
||||
});
|
||||
|
||||
publicAPI.composition = jasmine.createSpyObj('CompositionAPI', [
|
||||
'checkPolicy'
|
||||
]);
|
||||
publicAPI.composition.checkPolicy.and.returnValue(true);
|
||||
|
||||
publicAPI.objects.eventEmitter = jasmine.createSpyObj('eventemitter', [
|
||||
'on'
|
||||
]);
|
||||
publicAPI.objects.get.and.callFake(function (identifier) {
|
||||
return Promise.resolve({identifier: identifier});
|
||||
});
|
||||
publicAPI.$injector = jasmine.createSpyObj('$injector', [
|
||||
'get'
|
||||
]);
|
||||
publicAPI.$injector.get.and.returnValue(topicService);
|
||||
compositionAPI = new CompositionAPI(publicAPI);
|
||||
});
|
||||
|
||||
it('returns falsy if an object does not support composition', function () {
|
||||
expect(compositionAPI.get({})).toBeFalsy();
|
||||
});
|
||||
|
||||
describe('default composition', function () {
|
||||
let domainObject;
|
||||
let composition;
|
||||
describe('The Composition API', function () {
|
||||
let publicAPI;
|
||||
let compositionAPI;
|
||||
let topicService;
|
||||
let mutationTopic;
|
||||
|
||||
beforeEach(function () {
|
||||
domainObject = {
|
||||
name: 'test folder',
|
||||
identifier: {
|
||||
namespace: 'test',
|
||||
key: '1'
|
||||
},
|
||||
composition: [
|
||||
{
|
||||
namespace: 'test',
|
||||
key: 'a'
|
||||
},
|
||||
{
|
||||
namespace: 'test',
|
||||
key: 'b'
|
||||
},
|
||||
{
|
||||
namespace: 'test',
|
||||
key: 'c'
|
||||
}
|
||||
]
|
||||
};
|
||||
composition = compositionAPI.get(domainObject);
|
||||
|
||||
mutationTopic = jasmine.createSpyObj('mutationTopic', [
|
||||
'listen'
|
||||
]);
|
||||
topicService = jasmine.createSpy('topicService');
|
||||
topicService.and.returnValue(mutationTopic);
|
||||
publicAPI = {};
|
||||
publicAPI.objects = jasmine.createSpyObj('ObjectAPI', [
|
||||
'get',
|
||||
'mutate',
|
||||
'observe',
|
||||
'areIdsEqual'
|
||||
]);
|
||||
|
||||
publicAPI.objects.areIdsEqual.and.callFake(function (id1, id2) {
|
||||
return id1.namespace === id2.namespace && id1.key === id2.key;
|
||||
});
|
||||
|
||||
publicAPI.composition = jasmine.createSpyObj('CompositionAPI', [
|
||||
'checkPolicy'
|
||||
]);
|
||||
publicAPI.composition.checkPolicy.and.returnValue(true);
|
||||
|
||||
publicAPI.objects.eventEmitter = jasmine.createSpyObj('eventemitter', [
|
||||
'on'
|
||||
]);
|
||||
publicAPI.objects.get.and.callFake(function (identifier) {
|
||||
return Promise.resolve({identifier: identifier});
|
||||
});
|
||||
publicAPI.$injector = jasmine.createSpyObj('$injector', [
|
||||
'get'
|
||||
]);
|
||||
publicAPI.$injector.get.and.returnValue(topicService);
|
||||
compositionAPI = new CompositionAPI(publicAPI);
|
||||
});
|
||||
|
||||
it('returns composition collection', function () {
|
||||
expect(composition).toBeDefined();
|
||||
expect(composition).toEqual(jasmine.any(CompositionCollection));
|
||||
it('returns falsy if an object does not support composition', function () {
|
||||
expect(compositionAPI.get({})).toBeFalsy();
|
||||
});
|
||||
|
||||
it('correctly reflects composability', function () {
|
||||
expect(compositionAPI.supportsComposition(domainObject)).toBe(true);
|
||||
delete domainObject.composition;
|
||||
expect(compositionAPI.supportsComposition(domainObject)).toBe(false);
|
||||
});
|
||||
describe('default composition', function () {
|
||||
let domainObject;
|
||||
let composition;
|
||||
|
||||
it('loads composition from domain object', function () {
|
||||
const listener = jasmine.createSpy('addListener');
|
||||
composition.on('add', listener);
|
||||
|
||||
return composition.load().then(function () {
|
||||
expect(listener.calls.count()).toBe(3);
|
||||
expect(listener).toHaveBeenCalledWith({
|
||||
beforeEach(function () {
|
||||
domainObject = {
|
||||
name: 'test folder',
|
||||
identifier: {
|
||||
namespace: 'test',
|
||||
key: 'a'
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('supports reordering of composition', function () {
|
||||
let listener;
|
||||
beforeEach(function () {
|
||||
listener = jasmine.createSpy('reorderListener');
|
||||
composition.on('reorder', listener);
|
||||
|
||||
return composition.load();
|
||||
});
|
||||
it('', function () {
|
||||
composition.reorder(1, 0);
|
||||
let newComposition =
|
||||
publicAPI.objects.mutate.calls.mostRecent().args[2];
|
||||
let reorderPlan = listener.calls.mostRecent().args[0][0];
|
||||
|
||||
expect(reorderPlan.oldIndex).toBe(1);
|
||||
expect(reorderPlan.newIndex).toBe(0);
|
||||
expect(newComposition[0].key).toEqual('b');
|
||||
expect(newComposition[1].key).toEqual('a');
|
||||
expect(newComposition[2].key).toEqual('c');
|
||||
});
|
||||
it('', function () {
|
||||
composition.reorder(0, 2);
|
||||
let newComposition =
|
||||
publicAPI.objects.mutate.calls.mostRecent().args[2];
|
||||
let reorderPlan = listener.calls.mostRecent().args[0][0];
|
||||
|
||||
expect(reorderPlan.oldIndex).toBe(0);
|
||||
expect(reorderPlan.newIndex).toBe(2);
|
||||
expect(newComposition[0].key).toEqual('b');
|
||||
expect(newComposition[1].key).toEqual('c');
|
||||
expect(newComposition[2].key).toEqual('a');
|
||||
});
|
||||
});
|
||||
it('supports adding an object to composition', function () {
|
||||
let addListener = jasmine.createSpy('addListener');
|
||||
let mockChildObject = {
|
||||
identifier: {
|
||||
key: 'mock-key',
|
||||
namespace: ''
|
||||
}
|
||||
};
|
||||
composition.on('add', addListener);
|
||||
composition.add(mockChildObject);
|
||||
|
||||
expect(domainObject.composition.length).toBe(4);
|
||||
expect(domainObject.composition[3]).toEqual(mockChildObject.identifier);
|
||||
});
|
||||
});
|
||||
|
||||
describe('static custom composition', function () {
|
||||
let customProvider;
|
||||
let domainObject;
|
||||
let composition;
|
||||
|
||||
beforeEach(function () {
|
||||
// A simple custom provider, returns the same composition for
|
||||
// all objects of a given type.
|
||||
customProvider = {
|
||||
appliesTo: function (object) {
|
||||
return object.type === 'custom-object-type';
|
||||
},
|
||||
load: function (object) {
|
||||
return Promise.resolve([
|
||||
key: '1'
|
||||
},
|
||||
composition: [
|
||||
{
|
||||
namespace: 'custom',
|
||||
key: 'thing'
|
||||
namespace: 'test',
|
||||
key: 'a'
|
||||
},
|
||||
{
|
||||
namespace: 'test',
|
||||
key: 'b'
|
||||
},
|
||||
{
|
||||
namespace: 'test',
|
||||
key: 'c'
|
||||
}
|
||||
]);
|
||||
},
|
||||
add: jasmine.createSpy('add'),
|
||||
remove: jasmine.createSpy('remove')
|
||||
};
|
||||
domainObject = {
|
||||
identifier: {
|
||||
namespace: 'test',
|
||||
key: '1'
|
||||
},
|
||||
type: 'custom-object-type'
|
||||
};
|
||||
compositionAPI.addProvider(customProvider);
|
||||
composition = compositionAPI.get(domainObject);
|
||||
});
|
||||
]
|
||||
};
|
||||
composition = compositionAPI.get(domainObject);
|
||||
});
|
||||
|
||||
it('supports listening and loading', function () {
|
||||
const addListener = jasmine.createSpy('addListener');
|
||||
composition.on('add', addListener);
|
||||
it('returns composition collection', function () {
|
||||
expect(composition).toBeDefined();
|
||||
expect(composition).toEqual(jasmine.any(CompositionCollection));
|
||||
});
|
||||
|
||||
return composition.load().then(function (children) {
|
||||
let listenObject;
|
||||
const loadedObject = children[0];
|
||||
it('correctly reflects composability', function () {
|
||||
expect(compositionAPI.supportsComposition(domainObject)).toBe(true);
|
||||
delete domainObject.composition;
|
||||
expect(compositionAPI.supportsComposition(domainObject)).toBe(false);
|
||||
});
|
||||
|
||||
expect(addListener).toHaveBeenCalled();
|
||||
it('loads composition from domain object', function () {
|
||||
const listener = jasmine.createSpy('addListener');
|
||||
composition.on('add', listener);
|
||||
|
||||
listenObject = addListener.calls.mostRecent().args[0];
|
||||
expect(listenObject).toEqual(loadedObject);
|
||||
expect(loadedObject).toEqual({
|
||||
identifier: {
|
||||
namespace: 'custom',
|
||||
key: 'thing'
|
||||
}
|
||||
return composition.load().then(function () {
|
||||
expect(listener.calls.count()).toBe(3);
|
||||
expect(listener).toHaveBeenCalledWith({
|
||||
identifier: {
|
||||
namespace: 'test',
|
||||
key: 'a'
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('Calling add or remove', function () {
|
||||
let mockChildObject;
|
||||
describe('supports reordering of composition', function () {
|
||||
let listener;
|
||||
beforeEach(function () {
|
||||
listener = jasmine.createSpy('reorderListener');
|
||||
composition.on('reorder', listener);
|
||||
|
||||
beforeEach(function () {
|
||||
mockChildObject = {
|
||||
return composition.load();
|
||||
});
|
||||
it('', function () {
|
||||
composition.reorder(1, 0);
|
||||
let newComposition =
|
||||
publicAPI.objects.mutate.calls.mostRecent().args[2];
|
||||
let reorderPlan = listener.calls.mostRecent().args[0][0];
|
||||
|
||||
expect(reorderPlan.oldIndex).toBe(1);
|
||||
expect(reorderPlan.newIndex).toBe(0);
|
||||
expect(newComposition[0].key).toEqual('b');
|
||||
expect(newComposition[1].key).toEqual('a');
|
||||
expect(newComposition[2].key).toEqual('c');
|
||||
});
|
||||
it('', function () {
|
||||
composition.reorder(0, 2);
|
||||
let newComposition =
|
||||
publicAPI.objects.mutate.calls.mostRecent().args[2];
|
||||
let reorderPlan = listener.calls.mostRecent().args[0][0];
|
||||
|
||||
expect(reorderPlan.oldIndex).toBe(0);
|
||||
expect(reorderPlan.newIndex).toBe(2);
|
||||
expect(newComposition[0].key).toEqual('b');
|
||||
expect(newComposition[1].key).toEqual('c');
|
||||
expect(newComposition[2].key).toEqual('a');
|
||||
});
|
||||
});
|
||||
it('supports adding an object to composition', function () {
|
||||
let addListener = jasmine.createSpy('addListener');
|
||||
let mockChildObject = {
|
||||
identifier: {
|
||||
key: 'mock-key',
|
||||
namespace: ''
|
||||
}
|
||||
};
|
||||
composition.on('add', addListener);
|
||||
composition.add(mockChildObject);
|
||||
});
|
||||
|
||||
it('calls add on the provider', function () {
|
||||
expect(customProvider.add).toHaveBeenCalledWith(domainObject, mockChildObject.identifier);
|
||||
});
|
||||
|
||||
it('calls remove on the provider', function () {
|
||||
composition.remove(mockChildObject);
|
||||
expect(customProvider.remove).toHaveBeenCalledWith(domainObject, mockChildObject.identifier);
|
||||
expect(domainObject.composition.length).toBe(4);
|
||||
expect(domainObject.composition[3]).toEqual(mockChildObject.identifier);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('dynamic custom composition', function () {
|
||||
let customProvider;
|
||||
let domainObject;
|
||||
let composition;
|
||||
describe('static custom composition', function () {
|
||||
let customProvider;
|
||||
let domainObject;
|
||||
let composition;
|
||||
|
||||
beforeEach(function () {
|
||||
// A dynamic provider, loads an empty composition and exposes
|
||||
// listener functions.
|
||||
customProvider = jasmine.createSpyObj('dynamicProvider', [
|
||||
'appliesTo',
|
||||
'load',
|
||||
'on',
|
||||
'off'
|
||||
]);
|
||||
|
||||
customProvider.appliesTo.and.returnValue('true');
|
||||
customProvider.load.and.returnValue(Promise.resolve([]));
|
||||
|
||||
domainObject = {
|
||||
identifier: {
|
||||
namespace: 'test',
|
||||
key: '1'
|
||||
},
|
||||
type: 'custom-object-type'
|
||||
};
|
||||
compositionAPI.addProvider(customProvider);
|
||||
composition = compositionAPI.get(domainObject);
|
||||
});
|
||||
|
||||
it('supports listening and loading', function () {
|
||||
const addListener = jasmine.createSpy('addListener');
|
||||
const removeListener = jasmine.createSpy('removeListener');
|
||||
const addPromise = new Promise(function (resolve) {
|
||||
addListener.and.callFake(resolve);
|
||||
});
|
||||
const removePromise = new Promise(function (resolve) {
|
||||
removeListener.and.callFake(resolve);
|
||||
beforeEach(function () {
|
||||
// A simple custom provider, returns the same composition for
|
||||
// all objects of a given type.
|
||||
customProvider = {
|
||||
appliesTo: function (object) {
|
||||
return object.type === 'custom-object-type';
|
||||
},
|
||||
load: function (object) {
|
||||
return Promise.resolve([
|
||||
{
|
||||
namespace: 'custom',
|
||||
key: 'thing'
|
||||
}
|
||||
]);
|
||||
},
|
||||
add: jasmine.createSpy('add'),
|
||||
remove: jasmine.createSpy('remove')
|
||||
};
|
||||
domainObject = {
|
||||
identifier: {
|
||||
namespace: 'test',
|
||||
key: '1'
|
||||
},
|
||||
type: 'custom-object-type'
|
||||
};
|
||||
compositionAPI.addProvider(customProvider);
|
||||
composition = compositionAPI.get(domainObject);
|
||||
});
|
||||
|
||||
composition.on('add', addListener);
|
||||
composition.on('remove', removeListener);
|
||||
it('supports listening and loading', function () {
|
||||
const addListener = jasmine.createSpy('addListener');
|
||||
composition.on('add', addListener);
|
||||
|
||||
expect(customProvider.on).toHaveBeenCalledWith(
|
||||
domainObject,
|
||||
'add',
|
||||
jasmine.any(Function),
|
||||
jasmine.any(CompositionCollection)
|
||||
);
|
||||
expect(customProvider.on).toHaveBeenCalledWith(
|
||||
domainObject,
|
||||
'remove',
|
||||
jasmine.any(Function),
|
||||
jasmine.any(CompositionCollection)
|
||||
);
|
||||
const add = customProvider.on.calls.all()[0].args[2];
|
||||
const remove = customProvider.on.calls.all()[1].args[2];
|
||||
return composition.load().then(function (children) {
|
||||
let listenObject;
|
||||
const loadedObject = children[0];
|
||||
|
||||
return composition.load()
|
||||
.then(function () {
|
||||
expect(addListener).not.toHaveBeenCalled();
|
||||
expect(removeListener).not.toHaveBeenCalled();
|
||||
add({
|
||||
namespace: 'custom',
|
||||
key: 'thing'
|
||||
});
|
||||
expect(addListener).toHaveBeenCalled();
|
||||
|
||||
return addPromise;
|
||||
}).then(function () {
|
||||
expect(addListener).toHaveBeenCalledWith({
|
||||
identifier: {
|
||||
namespace: 'custom',
|
||||
key: 'thing'
|
||||
}
|
||||
});
|
||||
remove(addListener.calls.mostRecent().args[0]);
|
||||
|
||||
return removePromise;
|
||||
}).then(function () {
|
||||
expect(removeListener).toHaveBeenCalledWith({
|
||||
listenObject = addListener.calls.mostRecent().args[0];
|
||||
expect(listenObject).toEqual(loadedObject);
|
||||
expect(loadedObject).toEqual({
|
||||
identifier: {
|
||||
namespace: 'custom',
|
||||
key: 'thing'
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('Calling add or remove', function () {
|
||||
let mockChildObject;
|
||||
|
||||
beforeEach(function () {
|
||||
mockChildObject = {
|
||||
identifier: {
|
||||
key: 'mock-key',
|
||||
namespace: ''
|
||||
}
|
||||
};
|
||||
composition.add(mockChildObject);
|
||||
});
|
||||
|
||||
it('calls add on the provider', function () {
|
||||
expect(customProvider.add).toHaveBeenCalledWith(domainObject, mockChildObject.identifier);
|
||||
});
|
||||
|
||||
it('calls remove on the provider', function () {
|
||||
composition.remove(mockChildObject);
|
||||
expect(customProvider.remove).toHaveBeenCalledWith(domainObject, mockChildObject.identifier);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('dynamic custom composition', function () {
|
||||
let customProvider;
|
||||
let domainObject;
|
||||
let composition;
|
||||
|
||||
beforeEach(function () {
|
||||
// A dynamic provider, loads an empty composition and exposes
|
||||
// listener functions.
|
||||
customProvider = jasmine.createSpyObj('dynamicProvider', [
|
||||
'appliesTo',
|
||||
'load',
|
||||
'on',
|
||||
'off'
|
||||
]);
|
||||
|
||||
customProvider.appliesTo.and.returnValue('true');
|
||||
customProvider.load.and.returnValue(Promise.resolve([]));
|
||||
|
||||
domainObject = {
|
||||
identifier: {
|
||||
namespace: 'test',
|
||||
key: '1'
|
||||
},
|
||||
type: 'custom-object-type'
|
||||
};
|
||||
compositionAPI.addProvider(customProvider);
|
||||
composition = compositionAPI.get(domainObject);
|
||||
});
|
||||
|
||||
it('supports listening and loading', function () {
|
||||
const addListener = jasmine.createSpy('addListener');
|
||||
const removeListener = jasmine.createSpy('removeListener');
|
||||
const addPromise = new Promise(function (resolve) {
|
||||
addListener.and.callFake(resolve);
|
||||
});
|
||||
const removePromise = new Promise(function (resolve) {
|
||||
removeListener.and.callFake(resolve);
|
||||
});
|
||||
|
||||
composition.on('add', addListener);
|
||||
composition.on('remove', removeListener);
|
||||
|
||||
expect(customProvider.on).toHaveBeenCalledWith(
|
||||
domainObject,
|
||||
'add',
|
||||
jasmine.any(Function),
|
||||
jasmine.any(CompositionCollection)
|
||||
);
|
||||
expect(customProvider.on).toHaveBeenCalledWith(
|
||||
domainObject,
|
||||
'remove',
|
||||
jasmine.any(Function),
|
||||
jasmine.any(CompositionCollection)
|
||||
);
|
||||
const add = customProvider.on.calls.all()[0].args[2];
|
||||
const remove = customProvider.on.calls.all()[1].args[2];
|
||||
|
||||
return composition.load()
|
||||
.then(function () {
|
||||
expect(addListener).not.toHaveBeenCalled();
|
||||
expect(removeListener).not.toHaveBeenCalled();
|
||||
add({
|
||||
namespace: 'custom',
|
||||
key: 'thing'
|
||||
});
|
||||
|
||||
return addPromise;
|
||||
}).then(function () {
|
||||
expect(addListener).toHaveBeenCalledWith({
|
||||
identifier: {
|
||||
namespace: 'custom',
|
||||
key: 'thing'
|
||||
}
|
||||
});
|
||||
remove(addListener.calls.mostRecent().args[0]);
|
||||
|
||||
return removePromise;
|
||||
}).then(function () {
|
||||
expect(removeListener).toHaveBeenCalledWith({
|
||||
identifier: {
|
||||
namespace: 'custom',
|
||||
key: 'thing'
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,98 +20,75 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* @typedef {import('../objects/ObjectAPI').DomainObject} DomainObject
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('./CompositionAPI').default} CompositionAPI
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('../../../openmct').OpenMCT} OpenMCT
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} ListenerMap
|
||||
* @property {Array.<any>} add
|
||||
* @property {Array.<any>} remove
|
||||
* @property {Array.<any>} load
|
||||
* @property {Array.<any>} reorder
|
||||
*/
|
||||
|
||||
/**
|
||||
* A CompositionCollection represents the list of domain objects contained
|
||||
* by another domain object. It provides methods for loading this
|
||||
* list asynchronously, modifying this list, and listening for changes to
|
||||
* this list.
|
||||
*
|
||||
* Usage:
|
||||
* ```javascript
|
||||
* var myViewComposition = MCT.composition.get(myViewObject);
|
||||
* myViewComposition.on('add', addObjectToView);
|
||||
* myViewComposition.on('remove', removeObjectFromView);
|
||||
* myViewComposition.load(); // will trigger `add` for all loaded objects.
|
||||
* ```
|
||||
*/
|
||||
export default class CompositionCollection {
|
||||
domainObject;
|
||||
#provider;
|
||||
#publicAPI;
|
||||
#listeners;
|
||||
#mutables;
|
||||
define([
|
||||
'lodash'
|
||||
], function (
|
||||
_
|
||||
) {
|
||||
/**
|
||||
* @constructor
|
||||
* @param {DomainObject} domainObject the domain object
|
||||
* A CompositionCollection represents the list of domain objects contained
|
||||
* by another domain object. It provides methods for loading this
|
||||
* list asynchronously, modifying this list, and listening for changes to
|
||||
* this list.
|
||||
*
|
||||
* Usage:
|
||||
* ```javascript
|
||||
* var myViewComposition = MCT.composition.get(myViewObject);
|
||||
* myViewComposition.on('add', addObjectToView);
|
||||
* myViewComposition.on('remove', removeObjectFromView);
|
||||
* myViewComposition.load(); // will trigger `add` for all loaded objects.
|
||||
* ```
|
||||
*
|
||||
* @interface CompositionCollection
|
||||
* @param {module:openmct.DomainObject} domainObject the domain object
|
||||
* whose composition will be contained
|
||||
* @param {import('./CompositionProvider').default} provider the provider
|
||||
* @param {module:openmct.CompositionProvider} provider the provider
|
||||
* to use to retrieve other domain objects
|
||||
* @param {OpenMCT} publicAPI the composition API, for
|
||||
* @param {module:openmct.CompositionAPI} api the composition API, for
|
||||
* policy checks
|
||||
* @memberof module:openmct
|
||||
*/
|
||||
constructor(domainObject, provider, publicAPI) {
|
||||
function CompositionCollection(domainObject, provider, publicAPI) {
|
||||
this.domainObject = domainObject;
|
||||
/** @type {import('./CompositionProvider').default} */
|
||||
this.#provider = provider;
|
||||
/** @type {OpenMCT} */
|
||||
this.#publicAPI = publicAPI;
|
||||
/** @type {ListenerMap} */
|
||||
this.#listeners = {
|
||||
this.provider = provider;
|
||||
this.publicAPI = publicAPI;
|
||||
this.listeners = {
|
||||
add: [],
|
||||
remove: [],
|
||||
load: [],
|
||||
reorder: []
|
||||
};
|
||||
this.onProviderAdd = this.#onProviderAdd.bind(this);
|
||||
this.onProviderRemove = this.#onProviderRemove.bind(this);
|
||||
this.#mutables = {};
|
||||
this.onProviderAdd = this.onProviderAdd.bind(this);
|
||||
this.onProviderRemove = this.onProviderRemove.bind(this);
|
||||
this.mutables = {};
|
||||
|
||||
if (this.domainObject.isMutable) {
|
||||
this.returnMutables = true;
|
||||
let unobserve = this.domainObject.$on('$_destroy', () => {
|
||||
Object.values(this.#mutables).forEach(mutable => {
|
||||
this.#publicAPI.objects.destroyMutable(mutable);
|
||||
Object.values(this.mutables).forEach(mutable => {
|
||||
this.publicAPI.objects.destroyMutable(mutable);
|
||||
});
|
||||
unobserve();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen for changes to this composition. Supports 'add', 'remove', and
|
||||
* 'load' events.
|
||||
*
|
||||
* @param {string} event event to listen for, either 'add', 'remove' or 'load'.
|
||||
* @param {(...args: any[]) => void} callback to trigger when event occurs.
|
||||
* @param {any} [context] to use when invoking callback, optional.
|
||||
* @param event event to listen for, either 'add', 'remove' or 'load'.
|
||||
* @param callback to trigger when event occurs.
|
||||
* @param [context] context to use when invoking callback, optional.
|
||||
*/
|
||||
on(event, callback, context) {
|
||||
if (!this.#listeners[event]) {
|
||||
CompositionCollection.prototype.on = function (event, callback, context) {
|
||||
if (!this.listeners[event]) {
|
||||
throw new Error('Event not supported by composition: ' + event);
|
||||
}
|
||||
|
||||
if (this.#provider.on && this.#provider.off) {
|
||||
if (this.provider.on && this.provider.off) {
|
||||
if (event === 'add') {
|
||||
this.#provider.on(
|
||||
this.provider.on(
|
||||
this.domainObject,
|
||||
'add',
|
||||
this.onProviderAdd,
|
||||
@@ -120,7 +97,7 @@ export default class CompositionCollection {
|
||||
}
|
||||
|
||||
if (event === 'remove') {
|
||||
this.#provider.on(
|
||||
this.provider.on(
|
||||
this.domainObject,
|
||||
'remove',
|
||||
this.onProviderRemove,
|
||||
@@ -129,34 +106,36 @@ export default class CompositionCollection {
|
||||
}
|
||||
|
||||
if (event === 'reorder') {
|
||||
this.#provider.on(
|
||||
this.provider.on(
|
||||
this.domainObject,
|
||||
'reorder',
|
||||
this.#onProviderReorder,
|
||||
this.onProviderReorder,
|
||||
this
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.#listeners[event].push({
|
||||
this.listeners[event].push({
|
||||
callback: callback,
|
||||
context: context
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove a listener. Must be called with same exact parameters as
|
||||
* `off`.
|
||||
*
|
||||
* @param {string} event
|
||||
* @param {(...args: any[]) => void} callback
|
||||
* @param {any} [context]
|
||||
* @param event
|
||||
* @param callback
|
||||
* @param [context]
|
||||
*/
|
||||
off(event, callback, context) {
|
||||
if (!this.#listeners[event]) {
|
||||
|
||||
CompositionCollection.prototype.off = function (event, callback, context) {
|
||||
if (!this.listeners[event]) {
|
||||
throw new Error('Event not supported by composition: ' + event);
|
||||
}
|
||||
|
||||
const index = this.#listeners[event].findIndex(l => {
|
||||
const index = this.listeners[event].findIndex(l => {
|
||||
return l.callback === callback && l.context === context;
|
||||
});
|
||||
|
||||
@@ -164,116 +143,125 @@ export default class CompositionCollection {
|
||||
throw new Error('Tried to remove a listener that does not exist');
|
||||
}
|
||||
|
||||
this.#listeners[event].splice(index, 1);
|
||||
if (this.#listeners[event].length === 0) {
|
||||
this.listeners[event].splice(index, 1);
|
||||
if (this.listeners[event].length === 0) {
|
||||
this._destroy();
|
||||
|
||||
// Remove provider listener if this is the last callback to
|
||||
// be removed.
|
||||
if (this.#provider.off && this.#provider.on) {
|
||||
if (this.provider.off && this.provider.on) {
|
||||
if (event === 'add') {
|
||||
this.#provider.off(
|
||||
this.provider.off(
|
||||
this.domainObject,
|
||||
'add',
|
||||
this.onProviderAdd,
|
||||
this
|
||||
);
|
||||
} else if (event === 'remove') {
|
||||
this.#provider.off(
|
||||
this.provider.off(
|
||||
this.domainObject,
|
||||
'remove',
|
||||
this.onProviderRemove,
|
||||
this
|
||||
);
|
||||
} else if (event === 'reorder') {
|
||||
this.#provider.off(
|
||||
this.provider.off(
|
||||
this.domainObject,
|
||||
'reorder',
|
||||
this.#onProviderReorder,
|
||||
this.onProviderReorder,
|
||||
this
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a domain object to this composition.
|
||||
*
|
||||
* A call to [load]{@link module:openmct.CompositionCollection#load}
|
||||
* must have resolved before using this method.
|
||||
*
|
||||
* **TODO:** Remove `skipMutate` parameter.
|
||||
*
|
||||
* @param {DomainObject} child the domain object to add
|
||||
* @param {boolean} skipMutate
|
||||
* **Intended for internal use ONLY.**
|
||||
* true if the underlying provider should not be updated.
|
||||
* @param {module:openmct.DomainObject} child the domain object to add
|
||||
* @param {boolean} skipMutate true if the underlying provider should
|
||||
* not be updated
|
||||
* @memberof module:openmct.CompositionCollection#
|
||||
* @name add
|
||||
*/
|
||||
add(child, skipMutate) {
|
||||
CompositionCollection.prototype.add = function (child, skipMutate) {
|
||||
if (!skipMutate) {
|
||||
if (!this.#publicAPI.composition.checkPolicy(this.domainObject, child)) {
|
||||
if (!this.publicAPI.composition.checkPolicy(this.domainObject, child)) {
|
||||
throw `Object of type ${child.type} cannot be added to object of type ${this.domainObject.type}`;
|
||||
}
|
||||
|
||||
this.#provider.add(this.domainObject, child.identifier);
|
||||
this.provider.add(this.domainObject, child.identifier);
|
||||
} else {
|
||||
if (this.returnMutables && this.#publicAPI.objects.supportsMutation(child.identifier)) {
|
||||
let keyString = this.#publicAPI.objects.makeKeyString(child.identifier);
|
||||
if (this.returnMutables && this.publicAPI.objects.supportsMutation(child.identifier)) {
|
||||
let keyString = this.publicAPI.objects.makeKeyString(child.identifier);
|
||||
|
||||
child = this.#publicAPI.objects.toMutable(child);
|
||||
this.#mutables[keyString] = child;
|
||||
child = this.publicAPI.objects._toMutable(child);
|
||||
this.mutables[keyString] = child;
|
||||
}
|
||||
|
||||
this.#emit('add', child);
|
||||
this.emit('add', child);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Load the domain objects in this composition.
|
||||
*
|
||||
* @param {AbortSignal} abortSignal
|
||||
* @returns {Promise.<Array.<DomainObject>>} a promise for
|
||||
* @returns {Promise.<Array.<module:openmct.DomainObject>>} a promise for
|
||||
* the domain objects in this composition
|
||||
* @memberof {module:openmct.CompositionCollection#}
|
||||
* @name load
|
||||
*/
|
||||
async load(abortSignal) {
|
||||
this.#cleanUpMutables();
|
||||
const children = await this.#provider.load(this.domainObject);
|
||||
const childObjects = await Promise.all(children.map((c) => this.#publicAPI.objects.get(c, abortSignal)));
|
||||
childObjects.forEach(c => this.add(c, true));
|
||||
this.#emit('load');
|
||||
CompositionCollection.prototype.load = function (abortSignal) {
|
||||
this.cleanUpMutables();
|
||||
|
||||
return this.provider.load(this.domainObject)
|
||||
.then(function (children) {
|
||||
return Promise.all(children.map((c) => this.publicAPI.objects.get(c, abortSignal)));
|
||||
}.bind(this))
|
||||
.then(function (childObjects) {
|
||||
childObjects.forEach(c => this.add(c, true));
|
||||
|
||||
return childObjects;
|
||||
}.bind(this))
|
||||
.then(function (children) {
|
||||
this.emit('load');
|
||||
|
||||
return children;
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
return childObjects;
|
||||
}
|
||||
/**
|
||||
* Remove a domain object from this composition.
|
||||
*
|
||||
* A call to [load]{@link module:openmct.CompositionCollection#load}
|
||||
* must have resolved before using this method.
|
||||
*
|
||||
* **TODO:** Remove `skipMutate` parameter.
|
||||
*
|
||||
* @param {DomainObject} child the domain object to remove
|
||||
* @param {boolean} skipMutate
|
||||
* **Intended for internal use ONLY.**
|
||||
* true if the underlying provider should not be updated.
|
||||
* @param {module:openmct.DomainObject} child the domain object to remove
|
||||
* @param {boolean} skipMutate true if the underlying provider should
|
||||
* not be updated
|
||||
* @memberof module:openmct.CompositionCollection#
|
||||
* @name remove
|
||||
*/
|
||||
remove(child, skipMutate) {
|
||||
CompositionCollection.prototype.remove = function (child, skipMutate) {
|
||||
if (!skipMutate) {
|
||||
this.#provider.remove(this.domainObject, child.identifier);
|
||||
this.provider.remove(this.domainObject, child.identifier);
|
||||
} else {
|
||||
if (this.returnMutables) {
|
||||
let keyString = this.#publicAPI.objects.makeKeyString(child);
|
||||
if (this.#mutables[keyString] !== undefined && this.#mutables[keyString].isMutable) {
|
||||
this.#publicAPI.objects.destroyMutable(this.#mutables[keyString]);
|
||||
delete this.#mutables[keyString];
|
||||
let keyString = this.publicAPI.objects.makeKeyString(child);
|
||||
if (this.mutables[keyString] !== undefined && this.mutables[keyString].isMutable) {
|
||||
this.publicAPI.objects.destroyMutable(this.mutables[keyString]);
|
||||
delete this.mutables[keyString];
|
||||
}
|
||||
}
|
||||
|
||||
this.#emit('remove', child);
|
||||
this.emit('remove', child);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Reorder the domain objects in this composition.
|
||||
*
|
||||
@@ -282,75 +270,67 @@ export default class CompositionCollection {
|
||||
*
|
||||
* @param {number} oldIndex
|
||||
* @param {number} newIndex
|
||||
* @memberof module:openmct.CompositionCollection#
|
||||
* @name remove
|
||||
*/
|
||||
reorder(oldIndex, newIndex, _skipMutate) {
|
||||
this.#provider.reorder(this.domainObject, oldIndex, newIndex);
|
||||
}
|
||||
/**
|
||||
* Destroy mutationListener
|
||||
*/
|
||||
_destroy() {
|
||||
if (this.mutationListener) {
|
||||
this.mutationListener();
|
||||
delete this.mutationListener;
|
||||
}
|
||||
}
|
||||
CompositionCollection.prototype.reorder = function (oldIndex, newIndex, skipMutate) {
|
||||
this.provider.reorder(this.domainObject, oldIndex, newIndex);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle reorder from provider.
|
||||
* @private
|
||||
* @param {object} reorderMap
|
||||
*/
|
||||
#onProviderReorder(reorderMap) {
|
||||
this.#emit('reorder', reorderMap);
|
||||
}
|
||||
CompositionCollection.prototype.onProviderReorder = function (reorderMap) {
|
||||
this.emit('reorder', reorderMap);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle adds from provider.
|
||||
* @private
|
||||
* @param {import('../objects/ObjectAPI').Identifier} childId
|
||||
* @returns {DomainObject}
|
||||
*/
|
||||
#onProviderAdd(childId) {
|
||||
return this.#publicAPI.objects.get(childId).then(function (child) {
|
||||
CompositionCollection.prototype.onProviderAdd = function (childId) {
|
||||
return this.publicAPI.objects.get(childId).then(function (child) {
|
||||
this.add(child, true);
|
||||
|
||||
return child;
|
||||
}.bind(this));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle removal from provider.
|
||||
* @param {DomainObject} child
|
||||
* @private
|
||||
*/
|
||||
#onProviderRemove(child) {
|
||||
CompositionCollection.prototype.onProviderRemove = function (child) {
|
||||
this.remove(child, true);
|
||||
}
|
||||
};
|
||||
|
||||
CompositionCollection.prototype._destroy = function () {
|
||||
if (this.mutationListener) {
|
||||
this.mutationListener();
|
||||
delete this.mutationListener;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Emit events.
|
||||
*
|
||||
* @private
|
||||
* @param {string} event
|
||||
* @param {...args.<any>} payload
|
||||
*/
|
||||
#emit(event, ...payload) {
|
||||
this.#listeners[event].forEach(function (l) {
|
||||
CompositionCollection.prototype.emit = function (event, ...payload) {
|
||||
this.listeners[event].forEach(function (l) {
|
||||
if (l.context) {
|
||||
l.callback.apply(l.context, payload);
|
||||
} else {
|
||||
l.callback(...payload);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Destroy all mutables.
|
||||
* @private
|
||||
*/
|
||||
#cleanUpMutables() {
|
||||
Object.values(this.#mutables).forEach(mutable => {
|
||||
this.#publicAPI.objects.destroyMutable(mutable);
|
||||
CompositionCollection.prototype.cleanUpMutables = function () {
|
||||
Object.values(this.mutables).forEach(mutable => {
|
||||
this.publicAPI.objects.destroyMutable(mutable);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return CompositionCollection;
|
||||
});
|
||||
|
||||
@@ -1,262 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import _ from 'lodash';
|
||||
import objectUtils from "../objects/object-utils";
|
||||
|
||||
/**
|
||||
* @typedef {import('../objects/ObjectAPI').DomainObject} DomainObject
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('../objects/ObjectAPI').Identifier} Identifier
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('./CompositionAPI').default} CompositionAPI
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('../../../openmct').OpenMCT} OpenMCT
|
||||
*/
|
||||
|
||||
/**
|
||||
* A CompositionProvider provides the underlying implementation of
|
||||
* composition-related behavior for certain types of domain object.
|
||||
*
|
||||
* By default, a composition provider will not support composition
|
||||
* modification. You can add support for mutation of composition by
|
||||
* defining `add` and/or `remove` methods.
|
||||
*
|
||||
* If the composition of an object can change over time-- perhaps via
|
||||
* server updates or mutation via the add/remove methods, then one must
|
||||
* trigger events as necessary.
|
||||
*
|
||||
*/
|
||||
export default class CompositionProvider {
|
||||
#publicAPI;
|
||||
#listeningTo;
|
||||
|
||||
/**
|
||||
* @param {OpenMCT} publicAPI
|
||||
* @param {CompositionAPI} compositionAPI
|
||||
*/
|
||||
constructor(publicAPI, compositionAPI) {
|
||||
this.#publicAPI = publicAPI;
|
||||
this.#listeningTo = {};
|
||||
|
||||
compositionAPI.addPolicy(this.#cannotContainItself.bind(this));
|
||||
compositionAPI.addPolicy(this.#supportsComposition.bind(this));
|
||||
}
|
||||
|
||||
get listeningTo() {
|
||||
return this.#listeningTo;
|
||||
}
|
||||
|
||||
get establishTopicListener() {
|
||||
return this.#establishTopicListener.bind(this);
|
||||
}
|
||||
|
||||
get publicAPI() {
|
||||
return this.#publicAPI;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this provider should be used to load composition for a
|
||||
* particular domain object.
|
||||
* @method appliesTo
|
||||
* @param {import('../objects/ObjectAPI').DomainObject} domainObject the domain object
|
||||
* to check
|
||||
* @returns {boolean} true if this provider can provide composition for a given domain object
|
||||
*/
|
||||
appliesTo(domainObject) {
|
||||
throw new Error("This method must be implemented by a subclass.");
|
||||
}
|
||||
/**
|
||||
* Load any domain objects contained in the composition of this domain
|
||||
* object.
|
||||
* @param {DomainObject} domainObject the domain object
|
||||
* for which to load composition
|
||||
* @returns {Promise<Identifier[]>} a promise for
|
||||
* the Identifiers in this composition
|
||||
* @method load
|
||||
*/
|
||||
load(domainObject) {
|
||||
throw new Error("This method must be implemented by a subclass.");
|
||||
}
|
||||
/**
|
||||
* Attach listeners for changes to the composition of a given domain object.
|
||||
* Supports `add` and `remove` events.
|
||||
*
|
||||
* @param {DomainObject} domainObject to listen to
|
||||
* @param {string} event the event to bind to, either `add` or `remove`.
|
||||
* @param {Function} callback callback to invoke when event is triggered.
|
||||
* @param {any} [context] to use when invoking callback.
|
||||
*/
|
||||
on(domainObject,
|
||||
event,
|
||||
callback,
|
||||
context) {
|
||||
throw new Error("This method must be implemented by a subclass.");
|
||||
}
|
||||
/**
|
||||
* Remove a listener that was previously added for a given domain object.
|
||||
* event name, callback, and context must be the same as when the listener
|
||||
* was originally attached.
|
||||
*
|
||||
* @param {DomainObject} domainObject to remove listener for
|
||||
* @param {string} event event to stop listening to: `add` or `remove`.
|
||||
* @param {Function} callback callback to remove.
|
||||
* @param {any} context of callback to remove.
|
||||
*/
|
||||
off(domainObject,
|
||||
event,
|
||||
callback,
|
||||
context) {
|
||||
throw new Error("This method must be implemented by a subclass.");
|
||||
}
|
||||
/**
|
||||
* Remove a domain object from another domain object's composition.
|
||||
*
|
||||
* This method is optional; if not present, adding to a domain object's
|
||||
* composition using this provider will be disallowed.
|
||||
*
|
||||
* @param {DomainObject} domainObject the domain object
|
||||
* which should have its composition modified
|
||||
* @param {Identifier} childId the domain object to remove
|
||||
* @method remove
|
||||
*/
|
||||
remove(domainObject, childId) {
|
||||
throw new Error("This method must be implemented by a subclass.");
|
||||
}
|
||||
/**
|
||||
* Add a domain object to another domain object's composition.
|
||||
*
|
||||
* This method is optional; if not present, adding to a domain object's
|
||||
* composition using this provider will be disallowed.
|
||||
*
|
||||
* @param {DomainObject} parent the domain object
|
||||
* which should have its composition modified
|
||||
* @param {Identifier} childId the domain object to add
|
||||
* @method add
|
||||
*/
|
||||
add(parent, childId) {
|
||||
throw new Error("This method must be implemented by a subclass.");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DomainObject} parent
|
||||
* @param {Identifier} childId
|
||||
* @returns {boolean}
|
||||
*/
|
||||
includes(parent, childId) {
|
||||
throw new Error("This method must be implemented by a subclass.");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DomainObject} domainObject
|
||||
* @param {number} oldIndex
|
||||
* @param {number} newIndex
|
||||
* @returns
|
||||
*/
|
||||
reorder(domainObject, oldIndex, newIndex) {
|
||||
throw new Error("This method must be implemented by a subclass.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Listens on general mutation topic, using injector to fetch to avoid
|
||||
* circular dependencies.
|
||||
* @private
|
||||
*/
|
||||
#establishTopicListener() {
|
||||
if (this.topicListener) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#publicAPI.objects.eventEmitter.on('mutation', this.#onMutation.bind(this));
|
||||
this.topicListener = () => {
|
||||
this.#publicAPI.objects.eventEmitter.off('mutation', this.#onMutation.bind(this));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {DomainObject} parent
|
||||
* @param {DomainObject} child
|
||||
* @returns {boolean}
|
||||
*/
|
||||
#cannotContainItself(parent, child) {
|
||||
return !(parent.identifier.namespace === child.identifier.namespace
|
||||
&& parent.identifier.key === child.identifier.key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {DomainObject} parent
|
||||
* @returns {boolean}
|
||||
*/
|
||||
#supportsComposition(parent, _child) {
|
||||
return this.#publicAPI.composition.supportsComposition(parent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles mutation events. If there are active listeners for the mutated
|
||||
* object, detects changes to composition and triggers necessary events.
|
||||
*
|
||||
* @private
|
||||
* @param {DomainObject} oldDomainObject
|
||||
*/
|
||||
#onMutation(oldDomainObject) {
|
||||
const id = objectUtils.makeKeyString(oldDomainObject.identifier);
|
||||
const listeners = this.#listeningTo[id];
|
||||
|
||||
if (!listeners) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldComposition = listeners.composition.map(objectUtils.makeKeyString);
|
||||
const newComposition = oldDomainObject.composition.map(objectUtils.makeKeyString);
|
||||
|
||||
const added = _.difference(newComposition, oldComposition).map(objectUtils.parseKeyString);
|
||||
const removed = _.difference(oldComposition, newComposition).map(objectUtils.parseKeyString);
|
||||
|
||||
function notify(value) {
|
||||
return function (listener) {
|
||||
if (listener.context) {
|
||||
listener.callback.call(listener.context, value);
|
||||
} else {
|
||||
listener.callback(value);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
listeners.composition = newComposition.map(objectUtils.parseKeyString);
|
||||
|
||||
added.forEach(function (addedChild) {
|
||||
listeners.add.forEach(notify(addedChild));
|
||||
});
|
||||
|
||||
removed.forEach(function (removedChild) {
|
||||
listeners.remove.forEach(notify(removedChild));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,79 +19,102 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import objectUtils from "../objects/object-utils";
|
||||
import CompositionProvider from './CompositionProvider';
|
||||
|
||||
/**
|
||||
* @typedef {import('../objects/ObjectAPI').DomainObject} DomainObject
|
||||
*/
|
||||
define([
|
||||
'lodash',
|
||||
'objectUtils'
|
||||
], function (
|
||||
_,
|
||||
objectUtils
|
||||
) {
|
||||
/**
|
||||
* A CompositionProvider provides the underlying implementation of
|
||||
* composition-related behavior for certain types of domain object.
|
||||
*
|
||||
* By default, a composition provider will not support composition
|
||||
* modification. You can add support for mutation of composition by
|
||||
* defining `add` and/or `remove` methods.
|
||||
*
|
||||
* If the composition of an object can change over time-- perhaps via
|
||||
* server updates or mutation via the add/remove methods, then one must
|
||||
* trigger events as necessary.
|
||||
*
|
||||
* @interface CompositionProvider
|
||||
* @memberof module:openmct
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('../objects/ObjectAPI').Identifier} Identifier
|
||||
*/
|
||||
function DefaultCompositionProvider(publicAPI, compositionAPI) {
|
||||
this.publicAPI = publicAPI;
|
||||
this.listeningTo = {};
|
||||
this.onMutation = this.onMutation.bind(this);
|
||||
|
||||
/**
|
||||
* @typedef {import('./CompositionAPI').default} CompositionAPI
|
||||
*/
|
||||
this.cannotContainItself = this.cannotContainItself.bind(this);
|
||||
this.supportsComposition = this.supportsComposition.bind(this);
|
||||
|
||||
/**
|
||||
* @typedef {import('../../../openmct').OpenMCT} OpenMCT
|
||||
*/
|
||||
compositionAPI.addPolicy(this.cannotContainItself);
|
||||
compositionAPI.addPolicy(this.supportsComposition);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
DefaultCompositionProvider.prototype.cannotContainItself = function (parent, child) {
|
||||
return !(parent.identifier.namespace === child.identifier.namespace
|
||||
&& parent.identifier.key === child.identifier.key);
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
DefaultCompositionProvider.prototype.supportsComposition = function (parent, child) {
|
||||
return this.publicAPI.composition.supportsComposition(parent);
|
||||
};
|
||||
|
||||
/**
|
||||
* A CompositionProvider provides the underlying implementation of
|
||||
* composition-related behavior for certain types of domain object.
|
||||
*
|
||||
* By default, a composition provider will not support composition
|
||||
* modification. You can add support for mutation of composition by
|
||||
* defining `add` and/or `remove` methods.
|
||||
*
|
||||
* If the composition of an object can change over time-- perhaps via
|
||||
* server updates or mutation via the add/remove methods, then one must
|
||||
* trigger events as necessary.
|
||||
* @extends CompositionProvider
|
||||
*/
|
||||
export default class DefaultCompositionProvider extends CompositionProvider {
|
||||
/**
|
||||
* Check if this provider should be used to load composition for a
|
||||
* particular domain object.
|
||||
* @override
|
||||
* @param {DomainObject} domainObject the domain object
|
||||
* @param {module:openmct.DomainObject} domainObject the domain object
|
||||
* to check
|
||||
* @returns {boolean} true if this provider can provide composition for a given domain object
|
||||
* @returns {boolean} true if this provider can provide
|
||||
* composition for a given domain object
|
||||
* @memberof module:openmct.CompositionProvider#
|
||||
* @method appliesTo
|
||||
*/
|
||||
appliesTo(domainObject) {
|
||||
DefaultCompositionProvider.prototype.appliesTo = function (domainObject) {
|
||||
return Boolean(domainObject.composition);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Load any domain objects contained in the composition of this domain
|
||||
* object.
|
||||
* @override
|
||||
* @param {DomainObject} domainObject the domain object
|
||||
* @param {module:openmct.DomainObject} domainObject the domain object
|
||||
* for which to load composition
|
||||
* @returns {Promise<Identifier[]>} a promise for
|
||||
* @returns {Promise.<Array.<module:openmct.Identifier>>} a promise for
|
||||
* the Identifiers in this composition
|
||||
* @memberof module:openmct.CompositionProvider#
|
||||
* @method load
|
||||
*/
|
||||
load(domainObject) {
|
||||
DefaultCompositionProvider.prototype.load = function (domainObject) {
|
||||
return Promise.all(domainObject.composition);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Attach listeners for changes to the composition of a given domain object.
|
||||
* Supports `add` and `remove` events.
|
||||
*
|
||||
* @override
|
||||
* @param {DomainObject} domainObject to listen to
|
||||
* @param {string} event the event to bind to, either `add` or `remove`.
|
||||
* @param {Function} callback callback to invoke when event is triggered.
|
||||
* @param {any} [context] to use when invoking callback.
|
||||
* @param {module:openmct.DomainObject} domainObject to listen to
|
||||
* @param String event the event to bind to, either `add` or `remove`.
|
||||
* @param Function callback callback to invoke when event is triggered.
|
||||
* @param [context] context to use when invoking callback.
|
||||
*/
|
||||
on(domainObject,
|
||||
DefaultCompositionProvider.prototype.on = function (
|
||||
domainObject,
|
||||
event,
|
||||
callback,
|
||||
context) {
|
||||
context
|
||||
) {
|
||||
this.establishTopicListener();
|
||||
|
||||
/** @type {string} */
|
||||
const keyString = objectUtils.makeKeyString(domainObject.identifier);
|
||||
let objectListeners = this.listeningTo[keyString];
|
||||
|
||||
@@ -108,24 +131,24 @@ export default class DefaultCompositionProvider extends CompositionProvider {
|
||||
callback: callback,
|
||||
context: context
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove a listener that was previously added for a given domain object.
|
||||
* event name, callback, and context must be the same as when the listener
|
||||
* was originally attached.
|
||||
*
|
||||
* @override
|
||||
* @param {DomainObject} domainObject to remove listener for
|
||||
* @param {string} event event to stop listening to: `add` or `remove`.
|
||||
* @param {Function} callback callback to remove.
|
||||
* @param {any} context of callback to remove.
|
||||
* @param {module:openmct.DomainObject} domainObject to remove listener for
|
||||
* @param String event event to stop listening to: `add` or `remove`.
|
||||
* @param Function callback callback to remove.
|
||||
* @param [context] context of callback to remove.
|
||||
*/
|
||||
off(domainObject,
|
||||
DefaultCompositionProvider.prototype.off = function (
|
||||
domainObject,
|
||||
event,
|
||||
callback,
|
||||
context) {
|
||||
|
||||
/** @type {string} */
|
||||
context
|
||||
) {
|
||||
const keyString = objectUtils.makeKeyString(domainObject.identifier);
|
||||
const objectListeners = this.listeningTo[keyString];
|
||||
|
||||
@@ -137,64 +160,57 @@ export default class DefaultCompositionProvider extends CompositionProvider {
|
||||
if (!objectListeners.add.length && !objectListeners.remove.length && !objectListeners.reorder.length) {
|
||||
delete this.listeningTo[keyString];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove a domain object from another domain object's composition.
|
||||
*
|
||||
* This method is optional; if not present, adding to a domain object's
|
||||
* composition using this provider will be disallowed.
|
||||
*
|
||||
* @override
|
||||
* @param {DomainObject} domainObject the domain object
|
||||
* @param {module:openmct.DomainObject} domainObject the domain object
|
||||
* which should have its composition modified
|
||||
* @param {Identifier} childId the domain object to remove
|
||||
* @param {module:openmct.DomainObject} child the domain object to remove
|
||||
* @memberof module:openmct.CompositionProvider#
|
||||
* @method remove
|
||||
*/
|
||||
remove(domainObject, childId) {
|
||||
DefaultCompositionProvider.prototype.remove = function (domainObject, childId) {
|
||||
let composition = domainObject.composition.filter(function (child) {
|
||||
return !(childId.namespace === child.namespace
|
||||
&& childId.key === child.key);
|
||||
&& childId.key === child.key);
|
||||
});
|
||||
|
||||
this.publicAPI.objects.mutate(domainObject, 'composition', composition);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a domain object to another domain object's composition.
|
||||
*
|
||||
* This method is optional; if not present, adding to a domain object's
|
||||
* composition using this provider will be disallowed.
|
||||
*
|
||||
* @override
|
||||
* @param {DomainObject} parent the domain object
|
||||
* @param {module:openmct.DomainObject} domainObject the domain object
|
||||
* which should have its composition modified
|
||||
* @param {Identifier} childId the domain object to add
|
||||
* @param {module:openmct.DomainObject} child the domain object to add
|
||||
* @memberof module:openmct.CompositionProvider#
|
||||
* @method add
|
||||
*/
|
||||
add(parent, childId) {
|
||||
DefaultCompositionProvider.prototype.add = function (parent, childId) {
|
||||
if (!this.includes(parent, childId)) {
|
||||
parent.composition.push(childId);
|
||||
this.publicAPI.objects.mutate(parent, 'composition', parent.composition);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @param {DomainObject} parent
|
||||
* @param {Identifier} childId
|
||||
* @returns {boolean}
|
||||
* @private
|
||||
*/
|
||||
includes(parent, childId) {
|
||||
return parent.composition.some(composee => this.publicAPI.objects.areIdsEqual(composee, childId));
|
||||
}
|
||||
DefaultCompositionProvider.prototype.includes = function (parent, childId) {
|
||||
return parent.composition.some(composee =>
|
||||
this.publicAPI.objects.areIdsEqual(composee, childId));
|
||||
};
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @param {DomainObject} domainObject
|
||||
* @param {number} oldIndex
|
||||
* @param {number} newIndex
|
||||
* @returns
|
||||
*/
|
||||
reorder(domainObject, oldIndex, newIndex) {
|
||||
DefaultCompositionProvider.prototype.reorder = function (domainObject, oldIndex, newIndex) {
|
||||
let newComposition = domainObject.composition.slice();
|
||||
let removeId = oldIndex > newIndex ? oldIndex + 1 : oldIndex;
|
||||
let insertPosition = oldIndex < newIndex ? newIndex + 1 : newIndex;
|
||||
@@ -225,7 +241,6 @@ export default class DefaultCompositionProvider extends CompositionProvider {
|
||||
|
||||
this.publicAPI.objects.mutate(domainObject, 'composition', newComposition);
|
||||
|
||||
/** @type {string} */
|
||||
let id = objectUtils.makeKeyString(domainObject.identifier);
|
||||
const listeners = this.listeningTo[id];
|
||||
|
||||
@@ -242,5 +257,66 @@ export default class DefaultCompositionProvider extends CompositionProvider {
|
||||
listener.callback(reorderPlan);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Listens on general mutation topic, using injector to fetch to avoid
|
||||
* circular dependencies.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
DefaultCompositionProvider.prototype.establishTopicListener = function () {
|
||||
if (this.topicListener) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.publicAPI.objects.eventEmitter.on('mutation', this.onMutation);
|
||||
this.topicListener = () => {
|
||||
this.publicAPI.objects.eventEmitter.off('mutation', this.onMutation);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles mutation events. If there are active listeners for the mutated
|
||||
* object, detects changes to composition and triggers necessary events.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
DefaultCompositionProvider.prototype.onMutation = function (oldDomainObject) {
|
||||
const id = objectUtils.makeKeyString(oldDomainObject.identifier);
|
||||
const listeners = this.listeningTo[id];
|
||||
|
||||
if (!listeners) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldComposition = listeners.composition.map(objectUtils.makeKeyString);
|
||||
const newComposition = oldDomainObject.composition.map(objectUtils.makeKeyString);
|
||||
|
||||
const added = _.difference(newComposition, oldComposition).map(objectUtils.parseKeyString);
|
||||
const removed = _.difference(oldComposition, newComposition).map(objectUtils.parseKeyString);
|
||||
|
||||
function notify(value) {
|
||||
return function (listener) {
|
||||
if (listener.context) {
|
||||
listener.callback.call(listener.context, value);
|
||||
} else {
|
||||
listener.callback(value);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
listeners.composition = newComposition.map(objectUtils.parseKeyString);
|
||||
|
||||
added.forEach(function (addedChild) {
|
||||
listeners.add.forEach(notify(addedChild));
|
||||
});
|
||||
|
||||
removed.forEach(function (removedChild) {
|
||||
listeners.remove.forEach(notify(removedChild));
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
return DefaultCompositionProvider;
|
||||
});
|
||||
|
||||
@@ -23,11 +23,13 @@
|
||||
import FormController from './FormController';
|
||||
import FormProperties from './components/FormProperties.vue';
|
||||
|
||||
import EventEmitter from 'EventEmitter';
|
||||
import Vue from 'vue';
|
||||
import _ from 'lodash';
|
||||
|
||||
export default class FormsAPI {
|
||||
export default class FormsAPI extends EventEmitter {
|
||||
constructor(openmct) {
|
||||
super();
|
||||
|
||||
this.openmct = openmct;
|
||||
this.formController = new FormController(openmct);
|
||||
}
|
||||
@@ -90,75 +92,29 @@ export default class FormsAPI {
|
||||
|
||||
/**
|
||||
* Show form inside an Overlay dialog with given form structure
|
||||
* @public
|
||||
* @param {Array<Section>} formStructure a form structure, array of section
|
||||
* @param {Object} options
|
||||
* @property {function} onChange a callback function when any changes detected
|
||||
*/
|
||||
showForm(formStructure, {
|
||||
onChange
|
||||
} = {}) {
|
||||
let overlay;
|
||||
|
||||
const self = this;
|
||||
|
||||
const overlayEl = document.createElement('div');
|
||||
overlayEl.classList.add('u-contents');
|
||||
|
||||
overlay = self.openmct.overlays.overlay({
|
||||
element: overlayEl,
|
||||
size: 'dialog'
|
||||
});
|
||||
|
||||
let formSave;
|
||||
let formCancel;
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
formSave = resolve;
|
||||
formCancel = reject;
|
||||
});
|
||||
|
||||
this.showCustomForm(formStructure, {
|
||||
element: overlayEl,
|
||||
onChange
|
||||
})
|
||||
.then((response) => {
|
||||
overlay.dismiss();
|
||||
formSave(response);
|
||||
})
|
||||
.catch((response) => {
|
||||
overlay.dismiss();
|
||||
formCancel(response);
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show form as a child of the element provided with given form structure
|
||||
*
|
||||
* @public
|
||||
* @param {Array<Section>} formStructure a form structure, array of section
|
||||
* @param {Object} options
|
||||
* @property {HTMLElement} element Parent Element to render a Form
|
||||
* @property {function} onChange a callback function when any changes detected
|
||||
* @property {function} onSave a callback function when form is submitted
|
||||
* @property {function} onDismiss a callback function when form is dismissed
|
||||
*/
|
||||
showCustomForm(formStructure, {
|
||||
showForm(formStructure, {
|
||||
element,
|
||||
onChange
|
||||
} = {}) {
|
||||
if (element === undefined) {
|
||||
throw Error('Required element parameter not provided');
|
||||
}
|
||||
const changes = {};
|
||||
let overlay;
|
||||
let onDismiss;
|
||||
let onSave;
|
||||
|
||||
const self = this;
|
||||
|
||||
const changes = {};
|
||||
let formSave;
|
||||
let formCancel;
|
||||
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
formSave = onFormAction(resolve);
|
||||
formCancel = onFormAction(reject);
|
||||
onSave = onFormAction(resolve);
|
||||
onDismiss = onFormAction(reject);
|
||||
});
|
||||
|
||||
const vm = new Vue({
|
||||
@@ -170,17 +126,26 @@ export default class FormsAPI {
|
||||
return {
|
||||
formStructure,
|
||||
onChange: onFormPropertyChange,
|
||||
onCancel: formCancel,
|
||||
onSave: formSave
|
||||
onDismiss,
|
||||
onSave
|
||||
};
|
||||
},
|
||||
template: '<FormProperties :model="formStructure" @onChange="onChange" @onCancel="onCancel" @onSave="onSave"></FormProperties>'
|
||||
template: '<FormProperties :model="formStructure" @onChange="onChange" @onDismiss="onDismiss" @onSave="onSave"></FormProperties>'
|
||||
}).$mount();
|
||||
|
||||
const formElement = vm.$el;
|
||||
element.append(formElement);
|
||||
if (element) {
|
||||
element.append(formElement);
|
||||
} else {
|
||||
overlay = self.openmct.overlays.overlay({
|
||||
element: vm.$el,
|
||||
size: 'dialog',
|
||||
onDestroy: () => vm.$destroy()
|
||||
});
|
||||
}
|
||||
|
||||
function onFormPropertyChange(data) {
|
||||
self.emit('onFormPropertyChange', data);
|
||||
if (onChange) {
|
||||
onChange(data);
|
||||
}
|
||||
@@ -193,14 +158,17 @@ export default class FormsAPI {
|
||||
key = property.join('.');
|
||||
}
|
||||
|
||||
_.set(changes, key, data.value);
|
||||
changes[key] = data.value;
|
||||
}
|
||||
}
|
||||
|
||||
function onFormAction(callback) {
|
||||
return () => {
|
||||
formElement.remove();
|
||||
vm.$destroy();
|
||||
if (element) {
|
||||
formElement.remove();
|
||||
} else {
|
||||
overlay.dismiss();
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
callback(changes);
|
||||
|
||||
@@ -133,7 +133,7 @@ describe('The Forms API', () => {
|
||||
});
|
||||
|
||||
it('when container element is provided', (done) => {
|
||||
openmct.forms.showCustomForm(formStructure, { element }).catch(() => {
|
||||
openmct.forms.showForm(formStructure, { element }).catch(() => {
|
||||
done();
|
||||
});
|
||||
const titleElement = element.querySelector('.c-overlay__dialog-title');
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
tabindex="0"
|
||||
class="c-button js-cancel-button"
|
||||
aria-label="Cancel"
|
||||
@click="onCancel"
|
||||
@click="onDismiss"
|
||||
>
|
||||
{{ cancelLabel }}
|
||||
</button>
|
||||
@@ -164,8 +164,8 @@ export default {
|
||||
|
||||
this.$emit('onChange', data);
|
||||
},
|
||||
onCancel() {
|
||||
this.$emit('onCancel');
|
||||
onDismiss() {
|
||||
this.$emit('onDismiss');
|
||||
},
|
||||
onSave() {
|
||||
this.$emit('onSave');
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
v-model="selected"
|
||||
required="model.required"
|
||||
name="mctControl"
|
||||
:aria-label="model.ariaLabel || model.name"
|
||||
@change="onChange($event)"
|
||||
>
|
||||
<option
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
:class="model.cssClass"
|
||||
>
|
||||
<textarea
|
||||
:id="`${model.key}-textarea`"
|
||||
v-model="field"
|
||||
type="text"
|
||||
:size="model.size"
|
||||
|
||||
@@ -3,52 +3,39 @@
|
||||
class="c-menu"
|
||||
:class="options.menuClass"
|
||||
>
|
||||
<ul
|
||||
v-if="options.actions.length && options.actions[0].length"
|
||||
role="menu"
|
||||
>
|
||||
<ul v-if="options.actions.length && options.actions[0].length">
|
||||
<template
|
||||
v-for="(actionGroups, index) in options.actions"
|
||||
>
|
||||
<div
|
||||
:key="index"
|
||||
role="group"
|
||||
<li
|
||||
v-for="action in actionGroups"
|
||||
:key="action.name"
|
||||
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
|
||||
:title="action.description"
|
||||
:data-testid="action.testId || false"
|
||||
@click="action.onItemClicked"
|
||||
>
|
||||
<li
|
||||
v-for="action in actionGroups"
|
||||
:key="action.name"
|
||||
role="menuitem"
|
||||
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
|
||||
:title="action.description"
|
||||
:data-testid="action.testId || false"
|
||||
@click="action.onItemClicked"
|
||||
>
|
||||
{{ action.name }}
|
||||
</li>
|
||||
<div
|
||||
v-if="index !== options.actions.length - 1"
|
||||
:key="index"
|
||||
role="separator"
|
||||
class="c-menu__section-separator"
|
||||
>
|
||||
</div>
|
||||
<li
|
||||
v-if="actionGroups.length === 0"
|
||||
:key="index"
|
||||
>
|
||||
No actions defined.
|
||||
</li>
|
||||
</div></template>
|
||||
{{ action.name }}
|
||||
</li>
|
||||
<div
|
||||
v-if="index !== options.actions.length - 1"
|
||||
:key="index"
|
||||
class="c-menu__section-separator"
|
||||
>
|
||||
</div>
|
||||
<li
|
||||
v-if="actionGroups.length === 0"
|
||||
:key="index"
|
||||
>
|
||||
No actions defined.
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
|
||||
<ul
|
||||
v-else
|
||||
role="menu"
|
||||
>
|
||||
<ul v-else>
|
||||
<li
|
||||
v-for="action in options.actions"
|
||||
:key="action.name"
|
||||
role="menuitem"
|
||||
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
|
||||
:title="action.description"
|
||||
:data-testid="action.testId || false"
|
||||
|
||||
@@ -5,54 +5,45 @@
|
||||
>
|
||||
<ul
|
||||
v-if="options.actions.length && options.actions[0].length"
|
||||
role="menu"
|
||||
class="c-super-menu__menu"
|
||||
>
|
||||
<template
|
||||
v-for="(actionGroups, index) in options.actions"
|
||||
>
|
||||
<div
|
||||
:key="index"
|
||||
role="group"
|
||||
<li
|
||||
v-for="action in actionGroups"
|
||||
:key="action.name"
|
||||
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
|
||||
:title="action.description"
|
||||
:data-testid="action.testId || false"
|
||||
@click="action.onItemClicked"
|
||||
@mouseover="toggleItemDescription(action)"
|
||||
@mouseleave="toggleItemDescription()"
|
||||
>
|
||||
<li
|
||||
v-for="action in actionGroups"
|
||||
:key="action.name"
|
||||
role="menuitem"
|
||||
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
|
||||
:title="action.description"
|
||||
:data-testid="action.testId || false"
|
||||
@click="action.onItemClicked"
|
||||
@mouseover="toggleItemDescription(action)"
|
||||
@mouseleave="toggleItemDescription()"
|
||||
>
|
||||
{{ action.name }}
|
||||
</li>
|
||||
<div
|
||||
v-if="index !== options.actions.length - 1"
|
||||
:key="index"
|
||||
role="separator"
|
||||
class="c-menu__section-separator"
|
||||
>
|
||||
</div>
|
||||
<li
|
||||
v-if="actionGroups.length === 0"
|
||||
:key="index"
|
||||
>
|
||||
No actions defined.
|
||||
</li>
|
||||
</div></template>
|
||||
{{ action.name }}
|
||||
</li>
|
||||
<div
|
||||
v-if="index !== options.actions.length - 1"
|
||||
:key="index"
|
||||
class="c-menu__section-separator"
|
||||
>
|
||||
</div>
|
||||
<li
|
||||
v-if="actionGroups.length === 0"
|
||||
:key="index"
|
||||
>
|
||||
No actions defined.
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
|
||||
<ul
|
||||
v-else
|
||||
class="c-super-menu__menu"
|
||||
role="menu"
|
||||
>
|
||||
<li
|
||||
v-for="action in options.actions"
|
||||
:key="action.name"
|
||||
role="menuitem"
|
||||
:class="action.cssClass"
|
||||
:title="action.description"
|
||||
:data-testid="action.testId || false"
|
||||
|
||||
@@ -42,6 +42,7 @@ class InMemorySearchProvider {
|
||||
this.openmct = openmct;
|
||||
this.indexedIds = {};
|
||||
this.indexedCompositions = {};
|
||||
this.indexedTags = {};
|
||||
this.idsToIndex = [];
|
||||
this.pendingIndex = {};
|
||||
this.pendingRequests = 0;
|
||||
@@ -60,6 +61,7 @@ class InMemorySearchProvider {
|
||||
this.localSearchForObjects = this.localSearchForObjects.bind(this);
|
||||
this.localSearchForAnnotations = this.localSearchForAnnotations.bind(this);
|
||||
this.localSearchForTags = this.localSearchForTags.bind(this);
|
||||
this.localSearchForNotebookAnnotations = this.localSearchForNotebookAnnotations.bind(this);
|
||||
this.onAnnotationCreation = this.onAnnotationCreation.bind(this);
|
||||
this.onCompositionAdded = this.onCompositionAdded.bind(this);
|
||||
this.onCompositionRemoved = this.onCompositionRemoved.bind(this);
|
||||
@@ -91,7 +93,7 @@ class InMemorySearchProvider {
|
||||
|
||||
this.searchTypes = this.openmct.objects.SEARCH_TYPES;
|
||||
|
||||
this.supportedSearchTypes = [this.searchTypes.OBJECTS, this.searchTypes.ANNOTATIONS, this.searchTypes.TAGS];
|
||||
this.supportedSearchTypes = [this.searchTypes.OBJECTS, this.searchTypes.ANNOTATIONS, this.searchTypes.NOTEBOOK_ANNOTATIONS, this.searchTypes.TAGS];
|
||||
|
||||
this.scheduleForIndexing(rootObject.identifier);
|
||||
|
||||
@@ -161,6 +163,8 @@ class InMemorySearchProvider {
|
||||
return this.localSearchForObjects(queryId, query, maxResults);
|
||||
} else if (searchType === this.searchTypes.ANNOTATIONS) {
|
||||
return this.localSearchForAnnotations(queryId, query, maxResults);
|
||||
} else if (searchType === this.searchTypes.NOTEBOOK_ANNOTATIONS) {
|
||||
return this.localSearchForNotebookAnnotations(queryId, query, maxResults);
|
||||
} else if (searchType === this.searchTypes.TAGS) {
|
||||
return this.localSearchForTags(queryId, query, maxResults);
|
||||
} else {
|
||||
@@ -277,6 +281,13 @@ class InMemorySearchProvider {
|
||||
provider.index(domainObject);
|
||||
}
|
||||
|
||||
onTagMutation(domainObject, newTags) {
|
||||
domainObject.tags = newTags;
|
||||
const provider = this;
|
||||
|
||||
provider.index(domainObject);
|
||||
}
|
||||
|
||||
onCompositionAdded(newDomainObjectToIndex) {
|
||||
const provider = this;
|
||||
// The object comes in as a mutable domain object, which has functions,
|
||||
@@ -331,6 +342,14 @@ class InMemorySearchProvider {
|
||||
composition.on('remove', this.onCompositionRemoved);
|
||||
this.indexedCompositions[keyString] = composition;
|
||||
}
|
||||
|
||||
if (domainObject.type === 'annotation') {
|
||||
this.indexedTags[keyString] = this.openmct.objects.observe(
|
||||
domainObject,
|
||||
'tags',
|
||||
this.onTagMutation.bind(this, domainObject)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ((keyString !== 'ROOT')) {
|
||||
@@ -562,6 +581,43 @@ class InMemorySearchProvider {
|
||||
this.onWorkerMessage(eventToReturn);
|
||||
}
|
||||
|
||||
/**
|
||||
* A local version of the same SharedWorker function
|
||||
* if we don't have SharedWorkers available (e.g., iOS)
|
||||
*/
|
||||
localSearchForNotebookAnnotations(queryId, {entryId, targetKeyString}, maxResults) {
|
||||
// This results dictionary will have domain object ID keys which
|
||||
// point to the value the domain object's score.
|
||||
let results = [];
|
||||
const message = {
|
||||
request: 'searchForNotebookAnnotations',
|
||||
results: [],
|
||||
total: 0,
|
||||
queryId
|
||||
};
|
||||
|
||||
const matchingAnnotations = this.localIndexedAnnotationsByDomainObject[targetKeyString];
|
||||
if (matchingAnnotations) {
|
||||
results = matchingAnnotations.filter(matchingAnnotation => {
|
||||
if (!matchingAnnotation.targets) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const target = matchingAnnotation.targets[targetKeyString];
|
||||
|
||||
return (target && target.entryId && (target.entryId === entryId));
|
||||
});
|
||||
}
|
||||
|
||||
message.total = results.length;
|
||||
message.results = results
|
||||
.slice(0, maxResults);
|
||||
const eventToReturn = {
|
||||
data: message
|
||||
};
|
||||
this.onWorkerMessage(eventToReturn);
|
||||
}
|
||||
|
||||
destroyObservers(observers) {
|
||||
Object.entries(observers).forEach(([keyString, unobserve]) => {
|
||||
if (typeof unobserve === 'function') {
|
||||
|
||||
@@ -43,6 +43,8 @@
|
||||
port.postMessage(searchForAnnotations(event.data));
|
||||
} else if (requestType === 'TAGS') {
|
||||
port.postMessage(searchForTags(event.data));
|
||||
} else if (requestType === 'NOTEBOOK_ANNOTATIONS') {
|
||||
port.postMessage(searchForNotebookAnnotations(event.data));
|
||||
} else {
|
||||
throw new Error(`Unknown request ${event.data.request}`);
|
||||
}
|
||||
@@ -202,4 +204,33 @@
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
function searchForNotebookAnnotations(data) {
|
||||
let results = [];
|
||||
const message = {
|
||||
request: 'searchForNotebookAnnotations',
|
||||
results: {},
|
||||
total: 0,
|
||||
queryId: data.queryId
|
||||
};
|
||||
|
||||
const matchingAnnotations = indexedAnnotationsByDomainObject[data.input.targetKeyString];
|
||||
if (matchingAnnotations) {
|
||||
results = matchingAnnotations.filter(matchingAnnotation => {
|
||||
if (!matchingAnnotation.targets) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const target = matchingAnnotation.targets[data.input.targetKeyString];
|
||||
|
||||
return (target && target.entryId && (target.entryId === data.input.entryId));
|
||||
});
|
||||
}
|
||||
|
||||
message.total = results.length;
|
||||
message.results = results
|
||||
.slice(0, data.maxResults);
|
||||
|
||||
return message;
|
||||
}
|
||||
}());
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
const DEFAULT_INTERCEPTOR_PRIORITY = 0;
|
||||
export default class InterceptorRegistry {
|
||||
/**
|
||||
* A InterceptorRegistry maintains the definitions for different interceptors that may be invoked on domain objects.
|
||||
@@ -46,6 +45,7 @@ export default class InterceptorRegistry {
|
||||
* @memberof module:openmct.InterceptorRegistry#
|
||||
*/
|
||||
addInterceptor(interceptorDef) {
|
||||
//TODO: sort by priority
|
||||
this.interceptors.push(interceptorDef);
|
||||
}
|
||||
|
||||
@@ -56,18 +56,10 @@ export default class InterceptorRegistry {
|
||||
* @memberof module:openmct.InterceptorRegistry#
|
||||
*/
|
||||
getInterceptors(identifier, object) {
|
||||
|
||||
function byPriority(interceptorA, interceptorB) {
|
||||
let priorityA = interceptorA.priority ?? DEFAULT_INTERCEPTOR_PRIORITY;
|
||||
let priorityB = interceptorB.priority ?? DEFAULT_INTERCEPTOR_PRIORITY;
|
||||
|
||||
return priorityB - priorityA;
|
||||
}
|
||||
|
||||
return this.interceptors.filter(interceptor => {
|
||||
return typeof interceptor.appliesTo === 'function'
|
||||
&& interceptor.appliesTo(identifier, object);
|
||||
}).sort(byPriority);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -75,7 +75,11 @@ class MutableDomainObject {
|
||||
return eventOff;
|
||||
}
|
||||
$set(path, value) {
|
||||
MutableDomainObject.mutateObject(this, path, value);
|
||||
_.set(this, path, value);
|
||||
|
||||
if (path !== 'persisted' && path !== 'modified') {
|
||||
_.set(this, 'modified', Date.now());
|
||||
}
|
||||
|
||||
//Emit secret synchronization event first, so that all objects are in sync before subsequent events fired.
|
||||
this._globalEventEmitter.emit(qualifiedEventName(this, '$_synchronize_model'), this);
|
||||
@@ -132,11 +136,8 @@ class MutableDomainObject {
|
||||
}
|
||||
|
||||
static mutateObject(object, path, value) {
|
||||
if (path !== 'persisted') {
|
||||
_.set(object, 'modified', Date.now());
|
||||
}
|
||||
|
||||
_.set(object, path, value);
|
||||
_.set(object, 'modified', Date.now());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ import InMemorySearchProvider from './InMemorySearchProvider';
|
||||
/**
|
||||
* Uniquely identifies a domain object.
|
||||
*
|
||||
* @typedef {object} Identifier
|
||||
* @typedef Identifier
|
||||
* @property {string} namespace the namespace to/from which this domain
|
||||
* object should be loaded/stored.
|
||||
* @property {string} key a unique identifier for the domain object
|
||||
@@ -50,8 +50,8 @@ import InMemorySearchProvider from './InMemorySearchProvider';
|
||||
* A few common properties are defined for domain objects. Beyond these,
|
||||
* individual types of domain objects may add more as they see fit.
|
||||
*
|
||||
* @typedef {object} DomainObject
|
||||
* @property {Identifier} identifier a key/namespace pair which
|
||||
* @typedef DomainObject
|
||||
* @property {module:openmct.ObjectAPI~Identifier} identifier a key/namespace pair which
|
||||
* uniquely identifies this domain object
|
||||
* @property {string} type the type of domain object
|
||||
* @property {string} name the human-readable name for this domain object
|
||||
@@ -59,20 +59,11 @@ import InMemorySearchProvider from './InMemorySearchProvider';
|
||||
* object
|
||||
* @property {number} [modified] the time, in milliseconds since the UNIX
|
||||
* epoch, at which this domain object was last modified
|
||||
* @property {Identifier[]} [composition] if
|
||||
* @property {module:openmct.ObjectAPI~Identifier[]} [composition] if
|
||||
* present, this will be used by the default composition provider
|
||||
* to load domain objects
|
||||
* @memberof module:openmct.ObjectAPI~
|
||||
* @memberof module:openmct
|
||||
*/
|
||||
|
||||
/**
|
||||
* @readonly
|
||||
* @enum {string} SEARCH_TYPES
|
||||
* @property {string} OBJECTS Search for objects
|
||||
* @property {string} ANNOTATIONS Search for annotations
|
||||
* @property {string} TAGS Search for tags
|
||||
*/
|
||||
|
||||
/**
|
||||
* Utilities for loading, saving, and manipulating domain objects.
|
||||
* @interface ObjectAPI
|
||||
@@ -85,6 +76,7 @@ export default class ObjectAPI {
|
||||
this.SEARCH_TYPES = Object.freeze({
|
||||
OBJECTS: 'OBJECTS',
|
||||
ANNOTATIONS: 'ANNOTATIONS',
|
||||
NOTEBOOK_ANNOTATIONS: 'NOTEBOOK_ANNOTATIONS',
|
||||
TAGS: 'TAGS'
|
||||
});
|
||||
this.eventEmitter = new EventEmitter();
|
||||
@@ -96,7 +88,7 @@ export default class ObjectAPI {
|
||||
this.cache = {};
|
||||
this.interceptorRegistry = new InterceptorRegistry();
|
||||
|
||||
this.SYNCHRONIZED_OBJECT_TYPES = ['notebook', 'restricted-notebook', 'plan', 'annotation'];
|
||||
this.SYNCHRONIZED_OBJECT_TYPES = ['notebook', 'plan', 'annotation'];
|
||||
|
||||
this.errors = {
|
||||
Conflict: ConflictError
|
||||
@@ -187,7 +179,7 @@ export default class ObjectAPI {
|
||||
*/
|
||||
|
||||
/**
|
||||
* Force remote get for a domain object. Don't return dirty objects.
|
||||
* Get a domain object.
|
||||
*
|
||||
* @method get
|
||||
* @memberof module:openmct.ObjectProvider#
|
||||
@@ -196,8 +188,24 @@ export default class ObjectAPI {
|
||||
* @returns {Promise} a promise which will resolve when the domain object
|
||||
* has been saved, or be rejected if it cannot be saved
|
||||
*/
|
||||
remoteGet(identifier, abortSignal) {
|
||||
const keystring = this.makeKeyString(identifier);
|
||||
|
||||
get(identifier, abortSignal) {
|
||||
let keystring = this.makeKeyString(identifier);
|
||||
|
||||
if (this.cache[keystring] !== undefined) {
|
||||
return this.cache[keystring];
|
||||
}
|
||||
|
||||
identifier = utils.parseKeyString(identifier);
|
||||
let dirtyObject;
|
||||
if (this.isTransactionActive()) {
|
||||
dirtyObject = this.transaction.getDirtyObject(identifier);
|
||||
}
|
||||
|
||||
if (dirtyObject) {
|
||||
return Promise.resolve(dirtyObject);
|
||||
}
|
||||
|
||||
const provider = this.getProvider(identifier);
|
||||
|
||||
if (!provider) {
|
||||
@@ -210,11 +218,12 @@ export default class ObjectAPI {
|
||||
|
||||
let objectPromise = provider.get(identifier, abortSignal).then(result => {
|
||||
delete this.cache[keystring];
|
||||
|
||||
result = this.applyGetInterceptors(identifier, result);
|
||||
if (result.isMutable) {
|
||||
result.$refresh(result);
|
||||
} else {
|
||||
let mutableDomainObject = this.toMutable(result);
|
||||
let mutableDomainObject = this._toMutable(result);
|
||||
mutableDomainObject.$refresh(result);
|
||||
}
|
||||
|
||||
@@ -234,36 +243,6 @@ export default class ObjectAPI {
|
||||
return objectPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a domain object.
|
||||
*
|
||||
* @method get
|
||||
* @memberof module:openmct.ObjectProvider#
|
||||
* @param {string} key the key for the domain object to load
|
||||
* @param {AbortController.signal} abortSignal (optional) signal to abort fetch requests
|
||||
* @returns {Promise} a promise which will resolve when the domain object
|
||||
* has been saved, or be rejected if it cannot be saved
|
||||
*/
|
||||
get(identifier, abortSignal) {
|
||||
const keystring = this.makeKeyString(identifier);
|
||||
|
||||
if (this.cache[keystring] !== undefined) {
|
||||
return this.cache[keystring];
|
||||
}
|
||||
|
||||
identifier = utils.parseKeyString(identifier);
|
||||
|
||||
if (this.isTransactionActive()) {
|
||||
let dirtyObject = this.transaction.getDirtyObject(identifier);
|
||||
|
||||
if (dirtyObject) {
|
||||
return Promise.resolve(dirtyObject);
|
||||
}
|
||||
}
|
||||
|
||||
return this.remoteGet(identifier, abortSignal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for domain objects.
|
||||
*
|
||||
@@ -321,7 +300,7 @@ export default class ObjectAPI {
|
||||
}
|
||||
|
||||
return this.get(identifier).then((object) => {
|
||||
return this.toMutable(object);
|
||||
return this._toMutable(object);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -368,67 +347,44 @@ export default class ObjectAPI {
|
||||
* @returns {Promise} a promise which will resolve when the domain object
|
||||
* has been saved, or be rejected if it cannot be saved
|
||||
*/
|
||||
async save(domainObject) {
|
||||
console.log('save', JSON.parse(JSON.stringify(domainObject)));
|
||||
const provider = this.getProvider(domainObject.identifier);
|
||||
save(domainObject) {
|
||||
let provider = this.getProvider(domainObject.identifier);
|
||||
let savedResolve;
|
||||
let savedReject;
|
||||
let result;
|
||||
let lastPersistedTime;
|
||||
|
||||
if (!this.isPersistable(domainObject.identifier)) {
|
||||
result = Promise.reject('Object provider does not support saving');
|
||||
} else if (this.#hasAlreadyBeenPersisted(domainObject)) {
|
||||
result = Promise.resolve(true);
|
||||
} else {
|
||||
const username = await this.#getCurrentUsername();
|
||||
const isNewObject = domainObject.persisted === undefined;
|
||||
let savedResolve;
|
||||
let savedReject;
|
||||
let savedObjectPromise;
|
||||
|
||||
result = new Promise((resolve, reject) => {
|
||||
savedResolve = resolve;
|
||||
savedReject = reject;
|
||||
});
|
||||
|
||||
this.#mutate(domainObject, 'modifiedBy', username);
|
||||
|
||||
if (isNewObject) {
|
||||
this.#mutate(domainObject, 'createdBy', username);
|
||||
|
||||
const createdTime = Date.now();
|
||||
this.#mutate(domainObject, 'created', createdTime);
|
||||
|
||||
const persistedTime = Date.now();
|
||||
this.#mutate(domainObject, 'persisted', persistedTime);
|
||||
|
||||
savedObjectPromise = provider.create(domainObject);
|
||||
} else {
|
||||
lastPersistedTime = domainObject.persisted;
|
||||
const persistedTime = Date.now();
|
||||
this.#mutate(domainObject, 'persisted', persistedTime);
|
||||
|
||||
savedObjectPromise = provider.update(domainObject);
|
||||
}
|
||||
|
||||
if (savedObjectPromise) {
|
||||
savedObjectPromise.then(response => {
|
||||
savedResolve(response);
|
||||
}).catch((error) => {
|
||||
if (!isNewObject) {
|
||||
this.#mutate(domainObject, 'persisted', lastPersistedTime);
|
||||
}
|
||||
|
||||
savedReject(error);
|
||||
const persistedTime = Date.now();
|
||||
if (domainObject.persisted === undefined) {
|
||||
result = new Promise((resolve, reject) => {
|
||||
savedResolve = resolve;
|
||||
savedReject = reject;
|
||||
});
|
||||
domainObject.persisted = persistedTime;
|
||||
const newObjectPromise = provider.create(domainObject);
|
||||
if (newObjectPromise) {
|
||||
newObjectPromise.then(response => {
|
||||
this.mutate(domainObject, 'persisted', persistedTime);
|
||||
savedResolve(response);
|
||||
}).catch((error) => {
|
||||
savedReject(error);
|
||||
});
|
||||
} else {
|
||||
result = Promise.reject(`[ObjectAPI][save] Object provider returned ${newObjectPromise} when creating new object.`);
|
||||
}
|
||||
} else {
|
||||
result = Promise.reject(`[ObjectAPI][save] Object provider returned ${savedObjectPromise} when ${isNewObject ? 'creating new' : 'updating'} object.`);
|
||||
domainObject.persisted = persistedTime;
|
||||
this.mutate(domainObject, 'persisted', persistedTime);
|
||||
result = provider.update(domainObject);
|
||||
}
|
||||
}
|
||||
|
||||
return result.catch((error) => {
|
||||
// suppress conflict error notifications for remotely synced items
|
||||
// (possibly just for notebook and restricted-notebook as they have conflic resolution)
|
||||
if (error instanceof this.errors.Conflict && !this.SYNCHRONIZED_OBJECT_TYPES.includes(domainObject.type)) {
|
||||
if (error instanceof this.errors.Conflict) {
|
||||
this.openmct.notifications.error(`Conflict detected while saving ${this.makeKeyString(domainObject.identifier)}`);
|
||||
}
|
||||
|
||||
@@ -436,21 +392,8 @@ export default class ObjectAPI {
|
||||
});
|
||||
}
|
||||
|
||||
async #getCurrentUsername() {
|
||||
const user = await this.openmct.user.getCurrentUser();
|
||||
let username;
|
||||
|
||||
if (user !== undefined) {
|
||||
username = user.getName();
|
||||
}
|
||||
|
||||
return username;
|
||||
}
|
||||
|
||||
/**
|
||||
* After entering into edit mode, creates a new instance of Transaction to keep track of changes in Objects
|
||||
*
|
||||
* @returns {Transaction} a new Transaction that was just created
|
||||
*/
|
||||
startTransaction() {
|
||||
if (this.isTransactionActive()) {
|
||||
@@ -458,8 +401,6 @@ export default class ObjectAPI {
|
||||
}
|
||||
|
||||
this.transaction = new Transaction(this);
|
||||
|
||||
return this.transaction;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -532,16 +473,14 @@ export default class ObjectAPI {
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify a domain object. Internal to ObjectAPI, won't call save after.
|
||||
* @private
|
||||
*
|
||||
* Modify a domain object.
|
||||
* @param {module:openmct.DomainObject} object the object to mutate
|
||||
* @param {string} path the property to modify
|
||||
* @param {*} value the new value for this property
|
||||
* @method mutate
|
||||
* @memberof module:openmct.ObjectAPI#
|
||||
*/
|
||||
#mutate(domainObject, path, value) {
|
||||
mutate(domainObject, path, value) {
|
||||
if (!this.supportsMutation(domainObject.identifier)) {
|
||||
throw `Error: Attempted to mutate immutable object ${domainObject.name}`;
|
||||
}
|
||||
@@ -551,7 +490,7 @@ export default class ObjectAPI {
|
||||
} else {
|
||||
//Creating a temporary mutable domain object allows other mutable instances of the
|
||||
//object to be kept in sync.
|
||||
let mutableDomainObject = this.toMutable(domainObject);
|
||||
let mutableDomainObject = this._toMutable(domainObject);
|
||||
|
||||
//Mutate original object
|
||||
MutableDomainObject.mutateObject(domainObject, path, value);
|
||||
@@ -562,21 +501,8 @@ export default class ObjectAPI {
|
||||
//Destroy temporary mutable object
|
||||
this.destroyMutable(mutableDomainObject);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify a domain object and save.
|
||||
* @param {module:openmct.DomainObject} object the object to mutate
|
||||
* @param {string} path the property to modify
|
||||
* @param {*} value the new value for this property
|
||||
* @method mutate
|
||||
* @memberof module:openmct.ObjectAPI#
|
||||
*/
|
||||
mutate(domainObject, path, value) {
|
||||
this.#mutate(domainObject, path, value);
|
||||
|
||||
if (this.isTransactionActive()) {
|
||||
console.log('objectAPI: mutate', JSON.parse(JSON.stringify(domainObject)), path, value);
|
||||
this.transaction.add(domainObject);
|
||||
} else {
|
||||
this.save(domainObject);
|
||||
@@ -584,19 +510,15 @@ export default class ObjectAPI {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a mutable domain object from an existing domain object
|
||||
* @param {module:openmct.DomainObject} domainObject the object to make mutable
|
||||
* @returns {MutableDomainObject} a mutable domain object that will automatically sync
|
||||
* @method toMutable
|
||||
* @memberof module:openmct.ObjectAPI#
|
||||
* @private
|
||||
*/
|
||||
toMutable(domainObject) {
|
||||
_toMutable(object) {
|
||||
let mutableObject;
|
||||
|
||||
if (domainObject.isMutable) {
|
||||
mutableObject = domainObject;
|
||||
if (object.isMutable) {
|
||||
mutableObject = object;
|
||||
} else {
|
||||
mutableObject = MutableDomainObject.createMutable(domainObject, this.eventEmitter);
|
||||
mutableObject = MutableDomainObject.createMutable(object, this.eventEmitter);
|
||||
|
||||
// Check if provider supports realtime updates
|
||||
let identifier = utils.parseKeyString(mutableObject.identifier);
|
||||
@@ -604,12 +526,9 @@ export default class ObjectAPI {
|
||||
|
||||
if (provider !== undefined
|
||||
&& provider.observe !== undefined
|
||||
&& this.SYNCHRONIZED_OBJECT_TYPES.includes(domainObject.type)) {
|
||||
&& this.SYNCHRONIZED_OBJECT_TYPES.includes(object.type)) {
|
||||
let unobserve = provider.observe(identifier, (updatedModel) => {
|
||||
// modified can sometimes be undefined, so make it 0 in this case
|
||||
const mutableObjectModification = mutableObject.modified ?? Number.MIN_SAFE_INTEGER;
|
||||
|
||||
if (updatedModel.persisted > mutableObjectModification) {
|
||||
if (updatedModel.persisted > mutableObject.modified) {
|
||||
//Don't replace with a stale model. This can happen on slow connections when multiple mutations happen
|
||||
//in rapid succession and intermediate persistence states are returned by the observe function.
|
||||
updatedModel = this.applyGetInterceptors(identifier, updatedModel);
|
||||
@@ -663,7 +582,7 @@ export default class ObjectAPI {
|
||||
if (domainObject.isMutable) {
|
||||
return domainObject.$observe(path, callback);
|
||||
} else {
|
||||
let mutable = this.toMutable(domainObject);
|
||||
let mutable = this._toMutable(domainObject);
|
||||
mutable.$observe(path, callback);
|
||||
|
||||
return () => mutable.$destroy();
|
||||
@@ -752,14 +671,12 @@ export default class ObjectAPI {
|
||||
}
|
||||
|
||||
isTransactionActive() {
|
||||
return this.transaction !== undefined && this.transaction !== null;
|
||||
return Boolean(this.transaction && this.openmct.editor.isEditing());
|
||||
}
|
||||
|
||||
#hasAlreadyBeenPersisted(domainObject) {
|
||||
// modified can sometimes be undefined, so make it 0 in this case
|
||||
const modified = domainObject.modified ?? Number.MIN_SAFE_INTEGER;
|
||||
const result = domainObject.persisted !== undefined
|
||||
&& domainObject.persisted >= modified;
|
||||
&& domainObject.persisted >= domainObject.modified;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -8,27 +8,13 @@ describe("The Object API", () => {
|
||||
let mockDomainObject;
|
||||
const TEST_NAMESPACE = "test-namespace";
|
||||
const TEST_KEY = "test-key";
|
||||
const USERNAME = 'Joan Q Public';
|
||||
const FIFTEEN_MINUTES = 15 * 60 * 1000;
|
||||
|
||||
beforeEach((done) => {
|
||||
typeRegistry = jasmine.createSpyObj('typeRegistry', [
|
||||
'get'
|
||||
]);
|
||||
const userProvider = {
|
||||
isLoggedIn() {
|
||||
return true;
|
||||
},
|
||||
getCurrentUser() {
|
||||
return Promise.resolve({
|
||||
getName() {
|
||||
return USERNAME;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
openmct = createOpenMct();
|
||||
openmct.user.setProvider(userProvider);
|
||||
objectAPI = openmct.objects;
|
||||
|
||||
openmct.editor = {};
|
||||
@@ -77,63 +63,19 @@ describe("The Object API", () => {
|
||||
mockProvider.update.and.returnValue(Promise.resolve(true));
|
||||
objectAPI.addProvider(TEST_NAMESPACE, mockProvider);
|
||||
});
|
||||
it("Adds a 'created' timestamp to new objects", async () => {
|
||||
await objectAPI.save(mockDomainObject);
|
||||
expect(mockDomainObject.created).not.toBeUndefined();
|
||||
});
|
||||
it("Calls 'create' on provider if object is new", async () => {
|
||||
await objectAPI.save(mockDomainObject);
|
||||
it("Calls 'create' on provider if object is new", () => {
|
||||
objectAPI.save(mockDomainObject);
|
||||
expect(mockProvider.create).toHaveBeenCalled();
|
||||
expect(mockProvider.update).not.toHaveBeenCalled();
|
||||
});
|
||||
it("Calls 'update' on provider if object is not new", async () => {
|
||||
it("Calls 'update' on provider if object is not new", () => {
|
||||
mockDomainObject.persisted = Date.now() - FIFTEEN_MINUTES;
|
||||
mockDomainObject.modified = Date.now();
|
||||
|
||||
await objectAPI.save(mockDomainObject);
|
||||
objectAPI.save(mockDomainObject);
|
||||
expect(mockProvider.create).not.toHaveBeenCalled();
|
||||
expect(mockProvider.update).toHaveBeenCalled();
|
||||
});
|
||||
describe("the persisted timestamp for existing objects", () => {
|
||||
let persistedTimestamp;
|
||||
beforeEach(() => {
|
||||
persistedTimestamp = Date.now() - FIFTEEN_MINUTES;
|
||||
mockDomainObject.persisted = persistedTimestamp;
|
||||
mockDomainObject.modified = Date.now();
|
||||
});
|
||||
|
||||
it("is updated", async () => {
|
||||
await objectAPI.save(mockDomainObject);
|
||||
expect(mockDomainObject.persisted).toBeDefined();
|
||||
expect(mockDomainObject.persisted > persistedTimestamp).toBe(true);
|
||||
});
|
||||
it("is >= modified timestamp", async () => {
|
||||
await objectAPI.save(mockDomainObject);
|
||||
expect(mockDomainObject.persisted >= mockDomainObject.modified).toBe(true);
|
||||
});
|
||||
});
|
||||
describe("the persisted timestamp for new objects", () => {
|
||||
it("is updated", async () => {
|
||||
await objectAPI.save(mockDomainObject);
|
||||
expect(mockDomainObject.persisted).toBeDefined();
|
||||
});
|
||||
it("is >= modified timestamp", async () => {
|
||||
await objectAPI.save(mockDomainObject);
|
||||
expect(mockDomainObject.persisted >= mockDomainObject.modified).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it("Sets the current user for 'createdBy' on new objects", async () => {
|
||||
await objectAPI.save(mockDomainObject);
|
||||
expect(mockDomainObject.createdBy).toBe(USERNAME);
|
||||
});
|
||||
it("Sets the current user for 'modifedBy' on existing objects", async () => {
|
||||
mockDomainObject.persisted = Date.now() - FIFTEEN_MINUTES;
|
||||
mockDomainObject.modified = Date.now();
|
||||
|
||||
await objectAPI.save(mockDomainObject);
|
||||
expect(mockDomainObject.modifiedBy).toBe(USERNAME);
|
||||
});
|
||||
|
||||
it("Does not persist if the object is unchanged", () => {
|
||||
mockDomainObject.persisted =
|
||||
@@ -378,7 +320,7 @@ describe("The Object API", () => {
|
||||
beforeEach(function () {
|
||||
// Duplicate object to guarantee we are not sharing object instance, which would invalidate test
|
||||
testObjectDuplicate = JSON.parse(JSON.stringify(testObject));
|
||||
mutableSecondInstance = objectAPI.toMutable(testObjectDuplicate);
|
||||
mutableSecondInstance = objectAPI._toMutable(testObjectDuplicate);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -41,7 +41,6 @@ export default class Transaction {
|
||||
const save = this.objectAPI.save.bind(this.objectAPI);
|
||||
|
||||
Object.values(this.dirtyObjects).forEach(object => {
|
||||
console.log('transaction: commit, objects', object);
|
||||
promiseArray.push(this.createDirtyObjectPromise(object, save));
|
||||
});
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ class Overlay extends EventEmitter {
|
||||
dismissable = true,
|
||||
element,
|
||||
onDestroy,
|
||||
onDismiss,
|
||||
size
|
||||
} = {}) {
|
||||
super();
|
||||
@@ -33,7 +32,7 @@ class Overlay extends EventEmitter {
|
||||
OverlayComponent: OverlayComponent
|
||||
},
|
||||
provide: {
|
||||
dismiss: this.notifyAndDismiss.bind(this),
|
||||
dismiss: this.dismiss.bind(this),
|
||||
element,
|
||||
buttons,
|
||||
dismissable: this.dismissable
|
||||
@@ -44,10 +43,6 @@ class Overlay extends EventEmitter {
|
||||
if (onDestroy) {
|
||||
this.once('destroy', onDestroy);
|
||||
}
|
||||
|
||||
if (onDismiss) {
|
||||
this.once('dismiss', onDismiss);
|
||||
}
|
||||
}
|
||||
|
||||
dismiss() {
|
||||
@@ -56,12 +51,6 @@ class Overlay extends EventEmitter {
|
||||
this.component.$destroy();
|
||||
}
|
||||
|
||||
//Ensures that any callers are notified that the overlay is dismissed
|
||||
notifyAndDismiss() {
|
||||
this.emit('dismiss');
|
||||
this.dismiss();
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
**/
|
||||
|
||||
@@ -55,7 +55,7 @@ class OverlayAPI {
|
||||
dismissLastOverlay() {
|
||||
let lastOverlay = this.activeOverlays[this.activeOverlays.length - 1];
|
||||
if (lastOverlay && lastOverlay.dismissable) {
|
||||
lastOverlay.notifyAndDismiss();
|
||||
lastOverlay.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import TelemetryMetadataManager from './TelemetryMetadataManager';
|
||||
import TelemetryValueFormatter from './TelemetryValueFormatter';
|
||||
import DefaultMetadataProvider from './DefaultMetadataProvider';
|
||||
import objectUtils from 'objectUtils';
|
||||
import _ from 'lodash';
|
||||
|
||||
export default class TelemetryAPI {
|
||||
|
||||
@@ -72,7 +73,7 @@ export default class TelemetryAPI {
|
||||
* @returns {boolean} true if the object is a telemetry object.
|
||||
*/
|
||||
isTelemetryObject(domainObject) {
|
||||
return Boolean(this.#findMetadataProvider(domainObject));
|
||||
return Boolean(this.findMetadataProvider(domainObject));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -86,7 +87,7 @@ export default class TelemetryAPI {
|
||||
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
|
||||
*/
|
||||
canProvideTelemetry(domainObject) {
|
||||
return Boolean(this.#findSubscriptionProvider(domainObject))
|
||||
return Boolean(this.findSubscriptionProvider(domainObject))
|
||||
|| Boolean(this.findRequestProvider(domainObject));
|
||||
}
|
||||
|
||||
@@ -119,7 +120,7 @@ export default class TelemetryAPI {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
#findSubscriptionProvider() {
|
||||
findSubscriptionProvider() {
|
||||
const args = Array.prototype.slice.apply(arguments);
|
||||
function supportsDomainObject(provider) {
|
||||
return provider.supportsSubscribe.apply(provider, args);
|
||||
@@ -129,10 +130,9 @@ export default class TelemetryAPI {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a telemetry request provider that supports
|
||||
* a given domain object and options.
|
||||
* @private
|
||||
*/
|
||||
findRequestProvider() {
|
||||
findRequestProvider(domainObject) {
|
||||
const args = Array.prototype.slice.apply(arguments);
|
||||
function supportsDomainObject(provider) {
|
||||
return provider.supportsRequest.apply(provider, args);
|
||||
@@ -144,7 +144,7 @@ export default class TelemetryAPI {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
#findMetadataProvider(domainObject) {
|
||||
findMetadataProvider(domainObject) {
|
||||
return this.metadataProviders.filter(function (p) {
|
||||
return p.supportsMetadata(domainObject);
|
||||
})[0];
|
||||
@@ -153,7 +153,7 @@ export default class TelemetryAPI {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
#findLimitEvaluator(domainObject) {
|
||||
findLimitEvaluator(domainObject) {
|
||||
return this.limitProviders.filter(function (p) {
|
||||
return p.supportsLimits(domainObject);
|
||||
})[0];
|
||||
@@ -161,7 +161,6 @@ export default class TelemetryAPI {
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Though used in TelemetryCollection as well
|
||||
*/
|
||||
standardizeRequestOptions(options) {
|
||||
if (!Object.prototype.hasOwnProperty.call(options, 'start')) {
|
||||
@@ -175,10 +174,6 @@ export default class TelemetryAPI {
|
||||
if (!Object.prototype.hasOwnProperty.call(options, 'domain')) {
|
||||
options.domain = this.openmct.time.timeSystem().key;
|
||||
}
|
||||
|
||||
if (!Object.prototype.hasOwnProperty.call(options, 'timeContext')) {
|
||||
options.timeContext = this.openmct.time;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -246,7 +241,7 @@ export default class TelemetryAPI {
|
||||
/**
|
||||
* Request historical telemetry for a domain object.
|
||||
* The `options` argument allows you to specify filters
|
||||
* (start, end, etc.), sort order, time context, and strategies for retrieving
|
||||
* (start, end, etc.), sort order, and strategies for retrieving
|
||||
* telemetry (aggregation, latest available, etc.).
|
||||
*
|
||||
* @method request
|
||||
@@ -260,7 +255,7 @@ export default class TelemetryAPI {
|
||||
*/
|
||||
async request(domainObject) {
|
||||
if (this.noRequestProviderForAllObjects) {
|
||||
return [];
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
if (arguments.length === 1) {
|
||||
@@ -278,24 +273,22 @@ export default class TelemetryAPI {
|
||||
if (!provider) {
|
||||
this.requestAbortControllers.delete(abortController);
|
||||
|
||||
return this.#handleMissingRequestProvider(domainObject);
|
||||
return this.handleMissingRequestProvider(domainObject);
|
||||
}
|
||||
|
||||
arguments[1] = await this.applyRequestInterceptors(domainObject, arguments[1]);
|
||||
try {
|
||||
const telemetry = await provider.request(...arguments);
|
||||
|
||||
return telemetry;
|
||||
} catch (error) {
|
||||
if (error.name !== 'AbortError') {
|
||||
this.openmct.notifications.error('Error requesting telemetry data, see console for details');
|
||||
console.error(error);
|
||||
}
|
||||
return provider.request.apply(provider, arguments)
|
||||
.catch((rejected) => {
|
||||
if (rejected.name !== 'AbortError') {
|
||||
this.openmct.notifications.error('Error requesting telemetry data, see console for details');
|
||||
console.error(rejected);
|
||||
}
|
||||
|
||||
throw new Error(error);
|
||||
} finally {
|
||||
this.requestAbortControllers.delete(abortController);
|
||||
}
|
||||
return Promise.reject(rejected);
|
||||
}).finally(() => {
|
||||
this.requestAbortControllers.delete(abortController);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -313,7 +306,7 @@ export default class TelemetryAPI {
|
||||
* the subscription
|
||||
*/
|
||||
subscribe(domainObject, callback, options) {
|
||||
const provider = this.#findSubscriptionProvider(domainObject);
|
||||
const provider = this.findSubscriptionProvider(domainObject);
|
||||
|
||||
if (!this.subscribeCache) {
|
||||
this.subscribeCache = {};
|
||||
@@ -360,7 +353,7 @@ export default class TelemetryAPI {
|
||||
*/
|
||||
getMetadata(domainObject) {
|
||||
if (!this.metadataCache.has(domainObject)) {
|
||||
const metadataProvider = this.#findMetadataProvider(domainObject);
|
||||
const metadataProvider = this.findMetadataProvider(domainObject);
|
||||
if (!metadataProvider) {
|
||||
return;
|
||||
}
|
||||
@@ -376,6 +369,33 @@ export default class TelemetryAPI {
|
||||
return this.metadataCache.get(domainObject);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array of valueMetadatas that are common to all supplied
|
||||
* telemetry objects and match the requested hints.
|
||||
*
|
||||
*/
|
||||
commonValuesForHints(metadatas, hints) {
|
||||
const options = metadatas.map(function (metadata) {
|
||||
const values = metadata.valuesForHints(hints);
|
||||
|
||||
return _.keyBy(values, 'key');
|
||||
}).reduce(function (a, b) {
|
||||
const results = {};
|
||||
Object.keys(a).forEach(function (key) {
|
||||
if (Object.prototype.hasOwnProperty.call(b, key)) {
|
||||
results[key] = a[key];
|
||||
}
|
||||
});
|
||||
|
||||
return results;
|
||||
});
|
||||
const sortKeys = hints.map(function (h) {
|
||||
return 'hints.' + h;
|
||||
});
|
||||
|
||||
return _.sortBy(options, sortKeys);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a value formatter for a given valueMetadata.
|
||||
*
|
||||
@@ -430,7 +450,7 @@ export default class TelemetryAPI {
|
||||
*
|
||||
* @returns Promise
|
||||
*/
|
||||
#handleMissingRequestProvider(domainObject) {
|
||||
handleMissingRequestProvider(domainObject) {
|
||||
this.noRequestProviderForAllObjects = this.requestProviders.every(requestProvider => {
|
||||
const supportsRequest = requestProvider.supportsRequest.apply(requestProvider, arguments);
|
||||
const hasRequestProvider = Object.prototype.hasOwnProperty.call(requestProvider, 'request') && typeof requestProvider.request === 'function';
|
||||
@@ -520,7 +540,7 @@ export default class TelemetryAPI {
|
||||
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
|
||||
*/
|
||||
getLimitEvaluator(domainObject) {
|
||||
const provider = this.#findLimitEvaluator(domainObject);
|
||||
const provider = this.findLimitEvaluator(domainObject);
|
||||
if (!provider) {
|
||||
return {
|
||||
evaluate: function () {}
|
||||
@@ -558,7 +578,7 @@ export default class TelemetryAPI {
|
||||
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
|
||||
*/
|
||||
getLimits(domainObject) {
|
||||
const provider = this.#findLimitEvaluator(domainObject);
|
||||
const provider = this.findLimitEvaluator(domainObject);
|
||||
if (!provider || !provider.getLimits) {
|
||||
return {
|
||||
limits: function () {
|
||||
|
||||
@@ -23,11 +23,11 @@ import { createOpenMct, resetApplicationState } from 'utils/testing';
|
||||
import TelemetryAPI from './TelemetryAPI';
|
||||
import TelemetryCollection from './TelemetryCollection';
|
||||
|
||||
describe('Telemetry API', () => {
|
||||
describe('Telemetry API', function () {
|
||||
let openmct;
|
||||
let telemetryAPI;
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(function () {
|
||||
openmct = {
|
||||
time: jasmine.createSpyObj('timeAPI', [
|
||||
'timeSystem',
|
||||
@@ -47,11 +47,11 @@ describe('Telemetry API', () => {
|
||||
|
||||
});
|
||||
|
||||
describe('telemetry providers', () => {
|
||||
describe('telemetry providers', function () {
|
||||
let telemetryProvider;
|
||||
let domainObject;
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(function () {
|
||||
telemetryProvider = jasmine.createSpyObj('telemetryProvider', [
|
||||
'supportsSubscribe',
|
||||
'subscribe',
|
||||
@@ -73,16 +73,19 @@ describe('Telemetry API', () => {
|
||||
};
|
||||
});
|
||||
|
||||
it('provides consistent results without providers', async () => {
|
||||
it('provides consistent results without providers', function (done) {
|
||||
const unsubscribe = telemetryAPI.subscribe(domainObject);
|
||||
|
||||
expect(unsubscribe).toEqual(jasmine.any(Function));
|
||||
|
||||
const data = await telemetryAPI.request(domainObject);
|
||||
expect(data).toEqual([]);
|
||||
telemetryAPI.request(domainObject)
|
||||
.then((data) => {
|
||||
expect(data).toEqual([]);
|
||||
})
|
||||
.finally(done);
|
||||
});
|
||||
|
||||
it('skips providers that do not match', async () => {
|
||||
it('skips providers that do not match', function (done) {
|
||||
telemetryProvider.supportsSubscribe.and.returnValue(false);
|
||||
telemetryProvider.supportsRequest.and.returnValue(false);
|
||||
telemetryProvider.request.and.returnValue(Promise.resolve([]));
|
||||
@@ -95,13 +98,14 @@ describe('Telemetry API', () => {
|
||||
expect(telemetryProvider.subscribe).not.toHaveBeenCalled();
|
||||
expect(unsubscribe).toEqual(jasmine.any(Function));
|
||||
|
||||
await telemetryAPI.request(domainObject);
|
||||
expect(telemetryProvider.supportsRequest)
|
||||
.toHaveBeenCalledWith(domainObject, jasmine.any(Object));
|
||||
expect(telemetryProvider.request).not.toHaveBeenCalled();
|
||||
telemetryAPI.request(domainObject).then((response) => {
|
||||
expect(telemetryProvider.supportsRequest)
|
||||
.toHaveBeenCalledWith(domainObject, jasmine.any(Object));
|
||||
expect(telemetryProvider.request).not.toHaveBeenCalled();
|
||||
}).finally(done);
|
||||
});
|
||||
|
||||
it('sends subscribe calls to matching providers', () => {
|
||||
it('sends subscribe calls to matching providers', function () {
|
||||
const unsubFunc = jasmine.createSpy('unsubscribe');
|
||||
telemetryProvider.subscribe.and.returnValue(unsubFunc);
|
||||
telemetryProvider.supportsSubscribe.and.returnValue(true);
|
||||
@@ -129,7 +133,7 @@ describe('Telemetry API', () => {
|
||||
expect(callback).not.toHaveBeenCalledWith('otherValue');
|
||||
});
|
||||
|
||||
it('subscribes once per object', () => {
|
||||
it('subscribes once per object', function () {
|
||||
const unsubFunc = jasmine.createSpy('unsubscribe');
|
||||
telemetryProvider.subscribe.and.returnValue(unsubFunc);
|
||||
telemetryProvider.supportsSubscribe.and.returnValue(true);
|
||||
@@ -160,7 +164,7 @@ describe('Telemetry API', () => {
|
||||
expect(callbacktwo).not.toHaveBeenCalledWith('anotherValue');
|
||||
});
|
||||
|
||||
it('only deletes subscription cache when there are no more subscribers', () => {
|
||||
it('only deletes subscription cache when there are no more subscribers', function () {
|
||||
const unsubFunc = jasmine.createSpy('unsubscribe');
|
||||
telemetryProvider.subscribe.and.returnValue(unsubFunc);
|
||||
telemetryProvider.supportsSubscribe.and.returnValue(true);
|
||||
@@ -183,7 +187,7 @@ describe('Telemetry API', () => {
|
||||
unsubscribeThree();
|
||||
});
|
||||
|
||||
it('does subscribe/unsubscribe', () => {
|
||||
it('does subscribe/unsubscribe', function () {
|
||||
const unsubFunc = jasmine.createSpy('unsubscribe');
|
||||
telemetryProvider.subscribe.and.returnValue(unsubFunc);
|
||||
telemetryProvider.supportsSubscribe.and.returnValue(true);
|
||||
@@ -199,7 +203,7 @@ describe('Telemetry API', () => {
|
||||
unsubscribe();
|
||||
});
|
||||
|
||||
it('subscribes for different object', () => {
|
||||
it('subscribes for different object', function () {
|
||||
const unsubFuncs = [];
|
||||
const notifiers = [];
|
||||
telemetryProvider.supportsSubscribe.and.returnValue(true);
|
||||
@@ -239,120 +243,120 @@ describe('Telemetry API', () => {
|
||||
expect(unsubFuncs[1]).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('sends requests to matching providers', async () => {
|
||||
it('sends requests to matching providers', function (done) {
|
||||
const telemPromise = Promise.resolve([]);
|
||||
telemetryProvider.supportsRequest.and.returnValue(true);
|
||||
telemetryProvider.request.and.returnValue(telemPromise);
|
||||
telemetryAPI.addProvider(telemetryProvider);
|
||||
|
||||
await telemetryAPI.request(domainObject);
|
||||
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(
|
||||
domainObject,
|
||||
jasmine.any(Object)
|
||||
);
|
||||
expect(telemetryProvider.request).toHaveBeenCalledWith(
|
||||
domainObject,
|
||||
jasmine.any(Object)
|
||||
);
|
||||
telemetryAPI.request(domainObject).then(() => {
|
||||
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(
|
||||
domainObject,
|
||||
jasmine.any(Object)
|
||||
);
|
||||
expect(telemetryProvider.request).toHaveBeenCalledWith(
|
||||
domainObject,
|
||||
jasmine.any(Object)
|
||||
);
|
||||
}).finally(done);
|
||||
});
|
||||
|
||||
it('generates default request options', async () => {
|
||||
it('generates default request options', function (done) {
|
||||
telemetryProvider.supportsRequest.and.returnValue(true);
|
||||
telemetryProvider.request.and.returnValue(Promise.resolve([]));
|
||||
telemetryAPI.addProvider(telemetryProvider);
|
||||
|
||||
await telemetryAPI.request(domainObject);
|
||||
const { signal } = new AbortController();
|
||||
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(
|
||||
jasmine.any(Object),
|
||||
{
|
||||
signal,
|
||||
start: 0,
|
||||
end: 1,
|
||||
domain: 'system',
|
||||
timeContext: jasmine.any(Object)
|
||||
}
|
||||
);
|
||||
telemetryAPI.request(domainObject).then(() => {
|
||||
const { signal } = new AbortController();
|
||||
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(
|
||||
jasmine.any(Object),
|
||||
{
|
||||
signal,
|
||||
start: 0,
|
||||
end: 1,
|
||||
domain: 'system'
|
||||
}
|
||||
);
|
||||
|
||||
expect(telemetryProvider.request).toHaveBeenCalledWith(
|
||||
jasmine.any(Object),
|
||||
{
|
||||
signal,
|
||||
start: 0,
|
||||
end: 1,
|
||||
domain: 'system',
|
||||
timeContext: jasmine.any(Object)
|
||||
}
|
||||
);
|
||||
expect(telemetryProvider.request).toHaveBeenCalledWith(
|
||||
jasmine.any(Object),
|
||||
{
|
||||
signal,
|
||||
start: 0,
|
||||
end: 1,
|
||||
domain: 'system'
|
||||
}
|
||||
);
|
||||
|
||||
telemetryProvider.supportsRequest.calls.reset();
|
||||
telemetryProvider.request.calls.reset();
|
||||
telemetryProvider.supportsRequest.calls.reset();
|
||||
telemetryProvider.request.calls.reset();
|
||||
|
||||
await telemetryAPI.request(domainObject, {});
|
||||
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(
|
||||
jasmine.any(Object),
|
||||
{
|
||||
signal,
|
||||
start: 0,
|
||||
end: 1,
|
||||
domain: 'system',
|
||||
timeContext: jasmine.any(Object)
|
||||
}
|
||||
);
|
||||
telemetryAPI.request(domainObject, {}).then(() => {
|
||||
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(
|
||||
jasmine.any(Object),
|
||||
{
|
||||
signal,
|
||||
start: 0,
|
||||
end: 1,
|
||||
domain: 'system'
|
||||
}
|
||||
);
|
||||
|
||||
expect(telemetryProvider.request).toHaveBeenCalledWith(
|
||||
jasmine.any(Object),
|
||||
{
|
||||
signal,
|
||||
start: 0,
|
||||
end: 1,
|
||||
domain: 'system'
|
||||
}
|
||||
);
|
||||
});
|
||||
}).finally(done);
|
||||
|
||||
expect(telemetryProvider.request).toHaveBeenCalledWith(
|
||||
jasmine.any(Object),
|
||||
{
|
||||
signal,
|
||||
start: 0,
|
||||
end: 1,
|
||||
domain: 'system',
|
||||
timeContext: jasmine.any(Object)
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('do not overwrite existing request options', async () => {
|
||||
it('do not overwrite existing request options', function (done) {
|
||||
telemetryProvider.supportsRequest.and.returnValue(true);
|
||||
telemetryProvider.request.and.returnValue(Promise.resolve([]));
|
||||
telemetryAPI.addProvider(telemetryProvider);
|
||||
|
||||
await telemetryAPI.request(domainObject, {
|
||||
telemetryAPI.request(domainObject, {
|
||||
start: 20,
|
||||
end: 30,
|
||||
domain: 'someDomain'
|
||||
});
|
||||
const { signal } = new AbortController();
|
||||
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(
|
||||
jasmine.any(Object),
|
||||
{
|
||||
start: 20,
|
||||
end: 30,
|
||||
domain: 'someDomain',
|
||||
signal,
|
||||
timeContext: jasmine.any(Object)
|
||||
}
|
||||
);
|
||||
}).then(() => {
|
||||
const { signal } = new AbortController();
|
||||
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(
|
||||
jasmine.any(Object),
|
||||
{
|
||||
start: 20,
|
||||
end: 30,
|
||||
domain: 'someDomain',
|
||||
signal
|
||||
}
|
||||
);
|
||||
|
||||
expect(telemetryProvider.request).toHaveBeenCalledWith(
|
||||
jasmine.any(Object),
|
||||
{
|
||||
start: 20,
|
||||
end: 30,
|
||||
domain: 'someDomain',
|
||||
signal,
|
||||
timeContext: jasmine.any(Object)
|
||||
}
|
||||
);
|
||||
expect(telemetryProvider.request).toHaveBeenCalledWith(
|
||||
jasmine.any(Object),
|
||||
{
|
||||
start: 20,
|
||||
end: 30,
|
||||
domain: 'someDomain',
|
||||
signal
|
||||
}
|
||||
);
|
||||
|
||||
}).finally(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('metadata', () => {
|
||||
describe('metadata', function () {
|
||||
let mockMetadata = {};
|
||||
let mockObjectType = {
|
||||
definition: {}
|
||||
};
|
||||
beforeEach(() => {
|
||||
beforeEach(function () {
|
||||
telemetryAPI.addProvider({
|
||||
key: 'mockMetadataProvider',
|
||||
supportsMetadata() {
|
||||
@@ -365,7 +369,7 @@ describe('Telemetry API', () => {
|
||||
openmct.types.get.and.returnValue(mockObjectType);
|
||||
});
|
||||
|
||||
it('respects explicit priority', () => {
|
||||
it('respects explicit priority', function () {
|
||||
mockMetadata.values = [
|
||||
{
|
||||
key: "name",
|
||||
@@ -404,7 +408,7 @@ describe('Telemetry API', () => {
|
||||
expect(value.hints.priority).toBe(index + 1);
|
||||
});
|
||||
});
|
||||
it('if no explicit priority, defaults to order defined', () => {
|
||||
it('if no explicit priority, defaults to order defined', function () {
|
||||
mockMetadata.values = [
|
||||
{
|
||||
key: "name",
|
||||
@@ -431,7 +435,7 @@ describe('Telemetry API', () => {
|
||||
expect(value.key).toBe(mockMetadata.values[index].key);
|
||||
});
|
||||
});
|
||||
it('respects domain priority', () => {
|
||||
it('respects domain priority', function () {
|
||||
mockMetadata.values = [
|
||||
{
|
||||
key: "name",
|
||||
@@ -473,7 +477,7 @@ describe('Telemetry API', () => {
|
||||
expect(values[0].key).toBe('timestamp-local');
|
||||
expect(values[1].key).toBe('timestamp-utc');
|
||||
});
|
||||
it('respects range priority', () => {
|
||||
it('respects range priority', function () {
|
||||
mockMetadata.values = [
|
||||
{
|
||||
key: "name",
|
||||
@@ -515,7 +519,7 @@ describe('Telemetry API', () => {
|
||||
expect(values[0].key).toBe('cos');
|
||||
expect(values[1].key).toBe('sin');
|
||||
});
|
||||
it('respects priority and domain ordering', () => {
|
||||
it('respects priority and domain ordering', function () {
|
||||
mockMetadata.values = [
|
||||
{
|
||||
key: "id",
|
||||
@@ -584,7 +588,7 @@ describe('Telemetry API', () => {
|
||||
definition: {}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(function () {
|
||||
openmct.telemetry = telemetryAPI;
|
||||
telemetryAPI.addProvider({
|
||||
key: 'mockMetadataProvider',
|
||||
@@ -640,14 +644,16 @@ describe('Telemetery', () => {
|
||||
return resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
it('should not abort request without navigation', async () => {
|
||||
it('should not abort request without navigation', function (done) {
|
||||
telemetryAPI.addProvider(telemetryProvider);
|
||||
|
||||
await telemetryAPI.request({});
|
||||
expect(watchedSignal.aborted).toBe(false);
|
||||
telemetryAPI.request({}).finally(() => {
|
||||
expect(watchedSignal.aborted).toBe(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should abort request on navigation', (done) => {
|
||||
it('should abort request on navigation', function (done) {
|
||||
telemetryAPI.addProvider(telemetryProvider);
|
||||
|
||||
telemetryAPI.request({}).finally(() => {
|
||||
|
||||
@@ -229,25 +229,6 @@ describe("The Time API", function () {
|
||||
expect(api.clock()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('Provides a default time context', () => {
|
||||
const timeContext = api.getContextForView([]);
|
||||
expect(timeContext).not.toBe(null);
|
||||
});
|
||||
|
||||
it("Without a clock, is in fixed time mode", () => {
|
||||
const timeContext = api.getContextForView([]);
|
||||
expect(timeContext.isRealTime()).toBe(false);
|
||||
});
|
||||
|
||||
it("Provided a clock, is in real-time mode", () => {
|
||||
const timeContext = api.getContextForView([]);
|
||||
timeContext.clock('mts', {
|
||||
start: 0,
|
||||
end: 1
|
||||
});
|
||||
expect(timeContext.isRealTime()).toBe(true);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it("on tick, observes offsets, and indicates tick in bounds callback", function () {
|
||||
|
||||
@@ -362,18 +362,6 @@ class TimeContext extends EventEmitter {
|
||||
this.boundsVal = newBounds;
|
||||
this.emit('bounds', this.boundsVal, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this time context is in real-time mode or not.
|
||||
* @returns {boolean} true if this context is in real-time mode, false if not
|
||||
*/
|
||||
isRealTime() {
|
||||
if (this.clock()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export default TimeContext;
|
||||
|
||||
@@ -114,8 +114,6 @@ export default {
|
||||
this.formats = this.openmct.telemetry.getFormatMap(this.metadata);
|
||||
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
|
||||
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
|
||||
|
||||
this.limitEvaluator = this.openmct
|
||||
.telemetry
|
||||
.limitEvaluator(this.domainObject);
|
||||
@@ -136,8 +134,7 @@ export default {
|
||||
|
||||
this.telemetryCollection = this.openmct.telemetry.requestCollection(this.domainObject, {
|
||||
size: 1,
|
||||
strategy: 'latest',
|
||||
timeContext: this.timeContext
|
||||
strategy: 'latest'
|
||||
});
|
||||
this.telemetryCollection.on('add', this.setLatestValues);
|
||||
this.telemetryCollection.on('clear', this.resetValues);
|
||||
|
||||
@@ -112,7 +112,11 @@ export default {
|
||||
}
|
||||
},
|
||||
removeFromComposition(telemetryObject) {
|
||||
this.composition.remove(telemetryObject);
|
||||
let composition = this.domainObject.composition.filter(id =>
|
||||
!this.openmct.objects.areIdsEqual(id, telemetryObject.identifier)
|
||||
);
|
||||
|
||||
this.openmct.objects.mutate(this.domainObject, 'composition', composition);
|
||||
},
|
||||
addTelemetryObject(telemetryObject) {
|
||||
// grab information we need from the added telmetry object
|
||||
|
||||
@@ -104,14 +104,10 @@ export default {
|
||||
this.$set(this.plotSeries, this.plotSeries.length, series);
|
||||
this.setAxesLabels();
|
||||
},
|
||||
removeSeries(seriesKey) {
|
||||
const seriesIndex = this.plotSeries.findIndex(
|
||||
plotSeries => this.openmct.objects.areIdsEqual(seriesKey, plotSeries.identifier)
|
||||
);
|
||||
|
||||
const foundSeries = seriesIndex > -1;
|
||||
if (foundSeries) {
|
||||
this.$delete(this.plotSeries, seriesIndex);
|
||||
removeSeries(series) {
|
||||
const index = this.plotSeries.findIndex(plotSeries => this.openmct.objects.areIdsEqual(series.identifier, plotSeries.identifier));
|
||||
if (index !== undefined) {
|
||||
this.$delete(this.plotSeries, index);
|
||||
this.setAxesLabels();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -68,7 +68,6 @@ export default function ClockPlugin(options) {
|
||||
]
|
||||
},
|
||||
{
|
||||
ariaLabel: "12 or 24 hour clock",
|
||||
control: 'select',
|
||||
options: [
|
||||
{
|
||||
|
||||