Compare commits
133 Commits
5211-tests
...
fix-gauge
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a73719e809 | ||
|
|
798e2d4337 | ||
|
|
e3770dc701 | ||
|
|
fcfa95c2df | ||
|
|
0f12aa1eae | ||
|
|
d673cc6bcc | ||
|
|
c17efcc157 | ||
|
|
90662ce4a7 | ||
|
|
84c1526f5e | ||
|
|
b6d8d794be | ||
|
|
b53cc810f5 | ||
|
|
d73cb2ef70 | ||
|
|
5386ceb94c | ||
|
|
affb7a5311 | ||
|
|
07bdbe5108 | ||
|
|
f92bc0122c | ||
|
|
291e62687e | ||
|
|
313390f1fe | ||
|
|
e7804dd25e | ||
|
|
ec1de92d4b | ||
|
|
06b321588e | ||
|
|
909901b0f3 | ||
|
|
865f95c0b6 | ||
|
|
cb1e04b0d6 | ||
|
|
b0c2e66613 | ||
|
|
d162b5dbd8 | ||
|
|
efadf9036f | ||
|
|
ca928370a4 | ||
|
|
6820e0d044 | ||
|
|
64565b1bbb | ||
|
|
f721980bf0 | ||
|
|
b47712a0f4 | ||
|
|
b8fa89af6e | ||
|
|
57f3d4eba0 | ||
|
|
bbb84c695d | ||
|
|
f979e170ee | ||
|
|
1d875cb8ca | ||
|
|
064a865c9b | ||
|
|
a584766618 | ||
|
|
a671be726b | ||
|
|
61bf60783c | ||
|
|
5dc718b78d | ||
|
|
115912da31 | ||
|
|
10ff4e1781 | ||
|
|
41f8cb404d | ||
|
|
c6c58af12c | ||
|
|
15a0a87251 | ||
|
|
59a8614f1c | ||
|
|
7cf11e177c | ||
|
|
1a44652470 | ||
|
|
51d16f812a | ||
|
|
25de5653e8 | ||
|
|
cb6014d69f | ||
|
|
36736eb8a0 | ||
|
|
a13a6002c5 | ||
|
|
f1c85933c3 | ||
|
|
f2ed518300 | ||
|
|
bf8d1561ae | ||
|
|
2e1ede1427 | ||
|
|
fc3614dfbd | ||
|
|
22924f18fc | ||
|
|
60f20c64d5 | ||
|
|
8dc8a1c0a9 | ||
|
|
710259b5f0 | ||
|
|
e774eb01f3 | ||
|
|
01aac89be0 | ||
|
|
3c7ecc8561 | ||
|
|
dee92d893c | ||
|
|
0e707150e0 | ||
|
|
2540d96617 | ||
|
|
8ca0f13cd9 | ||
|
|
8ec7fbb74c | ||
|
|
143fb2dcdc | ||
|
|
1c8784fec5 | ||
|
|
a692245644 | ||
|
|
2943d2b6ec | ||
|
|
4246a597a9 | ||
|
|
0af7965021 | ||
|
|
d6bd2793d7 | ||
|
|
e9c0909415 | ||
|
|
0f0a3dc48f | ||
|
|
4c82680b87 | ||
|
|
450d3a5575 | ||
|
|
4f28e3bdf1 | ||
|
|
e6d25b22c1 | ||
|
|
acc60f5e4e | ||
|
|
9911d9ed6a | ||
|
|
c4734b8ad6 | ||
|
|
9786ff5de4 | ||
|
|
437154a5c0 | ||
|
|
2bd38dab9f | ||
|
|
063df721ae | ||
|
|
a09db30b32 | ||
|
|
9d89bdd6d3 | ||
|
|
ed9ca2829b | ||
|
|
eacbac6aad | ||
|
|
69153fe8f0 | ||
|
|
51196530fd | ||
|
|
fefa46ce7e | ||
|
|
82e685d4df | ||
|
|
e08ab8ef24 | ||
|
|
0060a6e20b | ||
|
|
7011877e64 | ||
|
|
8890cd9b22 | ||
|
|
34ecc08238 | ||
|
|
a07c043a29 | ||
|
|
2999a5135e | ||
|
|
2766452b38 | ||
|
|
f3cdf69288 | ||
|
|
a040bb30c2 | ||
|
|
0a2e0a4e65 | ||
|
|
e8df2bd437 | ||
|
|
ccd2a8b64c | ||
|
|
2bd35bb2a5 | ||
|
|
28dbd724d6 | ||
|
|
5a1c329c66 | ||
|
|
00a5cbd2fd | ||
|
|
a2d698d5c1 | ||
|
|
5685a5b393 | ||
|
|
164f39695e | ||
|
|
c384cf67da | ||
|
|
417b225505 | ||
|
|
e5e93f311c | ||
|
|
39e6d9c90c | ||
|
|
60d021ef82 | ||
|
|
59880955a2 | ||
|
|
b51ed7e844 | ||
|
|
7bbaec4006 | ||
|
|
c0f24b3925 | ||
|
|
4e79725897 | ||
|
|
0674c9fc33 | ||
|
|
de1b877954 | ||
|
|
4db2f547d9 |
@@ -5,6 +5,7 @@ executors:
|
||||
- image: mcr.microsoft.com/playwright:v1.23.0-focal
|
||||
environment:
|
||||
NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed
|
||||
PERCY_POSTINSTALL_BROWSER: 'true' # Needed to store the percy browser in cache deps
|
||||
parameters:
|
||||
BUST_CACHE:
|
||||
description: "Set this with the CircleCI UI Trigger Workflow button (boolean = true) to bust the cache!"
|
||||
@@ -64,8 +65,8 @@ commands:
|
||||
suite:
|
||||
type: string
|
||||
steps:
|
||||
- run: npm run cov:e2e:report
|
||||
- run: npm run cov:e2e:<<parameters.suite>>:publish
|
||||
- run: npm run cov:e2e:report || true
|
||||
- run: npm run cov:e2e:<<parameters.suite>>:publish
|
||||
orbs:
|
||||
node: circleci/node@4.9.0
|
||||
browser-tools: circleci/browser-tools@1.3.0
|
||||
@@ -153,6 +154,22 @@ jobs:
|
||||
- store_artifacts:
|
||||
path: html-test-results
|
||||
- generate_and_store_version_and_filesystem_artifacts
|
||||
visual-test:
|
||||
parameters:
|
||||
node-version:
|
||||
type: string
|
||||
executor: pw-focal-development
|
||||
steps:
|
||||
- build_and_install:
|
||||
node-version: <<parameters.node-version>>
|
||||
- run: npm run test:e2e:visual
|
||||
- store_test_results:
|
||||
path: test-results/results.xml
|
||||
- store_artifacts:
|
||||
path: test-results
|
||||
- store_artifacts:
|
||||
path: html-test-results
|
||||
- generate_and_store_version_and_filesystem_artifacts
|
||||
workflows:
|
||||
overall-circleci-commit-status: #These jobs run on every commit
|
||||
jobs:
|
||||
@@ -168,6 +185,9 @@ workflows:
|
||||
suite: stable
|
||||
- perf-test:
|
||||
node-version: lts/gallium
|
||||
- visual-test:
|
||||
node-version: lts/gallium
|
||||
|
||||
the-nightly: #These jobs do not run on PRs, but against master at night
|
||||
jobs:
|
||||
- unit-test:
|
||||
@@ -185,6 +205,10 @@ workflows:
|
||||
name: e2e-full-nightly
|
||||
node-version: lts/gallium
|
||||
suite: full
|
||||
- perf-test:
|
||||
node-version: lts/gallium
|
||||
- visual-test:
|
||||
node-version: lts/gallium
|
||||
triggers:
|
||||
- schedule:
|
||||
cron: "0 0 * * *"
|
||||
|
||||
5
.github/dependabot.yml
vendored
@@ -7,12 +7,13 @@ updates:
|
||||
interval: "daily"
|
||||
open-pull-requests-limit: 10
|
||||
labels:
|
||||
- "pr:e2e"
|
||||
- "type:maintenance"
|
||||
- "dependencies"
|
||||
- "pr:e2e"
|
||||
- "pr:daveit"
|
||||
- "pr:visual"
|
||||
- "pr:platform"
|
||||
ignore:
|
||||
- dependency-name: "@playwright/test" #we source the container instead of the dependency in CI
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
|
||||
37
.github/workflows/e2e-couchdb.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
name: "e2e-couchdb"
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
types:
|
||||
- labeled
|
||||
- opened
|
||||
env:
|
||||
OPENMCT_DATABASE_NAME: openmct
|
||||
COUCH_ADMIN_USER: admin
|
||||
COUCH_ADMIN_PASSWORD: password
|
||||
COUCH_BASE_LOCAL: http://localhost:5984
|
||||
COUCH_NODE_NAME: nonode@nohost
|
||||
jobs:
|
||||
e2e-couchdb:
|
||||
if: ${{ github.event.label.name == 'pr:e2e:couchdb' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- run : docker-compose up -d -f src/plugins/persistence/couch/couchdb-compose.yaml
|
||||
- run : sh src/plugins/persistence/couch/setup-couchdb.sh
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
- run: npx playwright@1.23.0 install
|
||||
- run: npm install
|
||||
- run: sh src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh
|
||||
- run: npm run test:e2e:couchdb
|
||||
- run: ls -latr
|
||||
- name: Archive test results
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: test-results
|
||||
- name: Archive html test results
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: html-test-results
|
||||
25
.github/workflows/e2e-visual.yml
vendored
@@ -1,25 +0,0 @@
|
||||
name: "e2e-visual"
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
types:
|
||||
- labeled
|
||||
- opened
|
||||
schedule:
|
||||
- cron: '28 21 * * 1-5'
|
||||
|
||||
jobs:
|
||||
e2e-visual:
|
||||
if: ${{ github.event.label.name == 'pr:visual' }} || ${{ github.event.workflow_dispatch }} || ${{ github.event.schedule }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
- run: npx playwright@1.23.0 install
|
||||
- run: npm install
|
||||
- name: Run the e2e visual tests
|
||||
run: npm run test:e2e:visual
|
||||
env:
|
||||
PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}
|
||||
6
API.md
@@ -390,7 +390,7 @@ A telemetry object is a domain object with a telemetry property. To take an exa
|
||||
{
|
||||
"key": "value",
|
||||
"name": "Value",
|
||||
"units": "kilograms",
|
||||
"unit": "kilograms",
|
||||
"format": "float",
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
@@ -425,7 +425,7 @@ attribute | type | flags | notes
|
||||
`name` | string | optional | a human readable label for this field. If omitted, defaults to `key`.
|
||||
`source` | string | optional | identifies the property of a datum where this value is stored. If omitted, defaults to `key`.
|
||||
`format` | string | optional | a specific format identifier, mapping to a formatter. If omitted, uses a default formatter. For enumerations, use `enum`. For timestamps, use `utc` if you are using utc dates, otherwise use a key mapping to your custom date format.
|
||||
`units` | string | optional | the units of this value, e.g. `km`, `seconds`, `parsecs`
|
||||
`unit` | string | optional | the unit of this value, e.g. `km`, `seconds`, `parsecs`
|
||||
`min` | number | optional | the minimum possible value of this measurement. Will be used by plots, gauges, etc to automatically set a min value.
|
||||
`max` | number | optional | the maximum possible value of this measurement. Will be used by plots, gauges, etc to automatically set a max value.
|
||||
`enumerations` | array | optional | for objects where `format` is `"enum"`, this array tracks all possible enumerations of the value. Each entry in this array is an object, with a `value` property that is the numerical value of the enumeration, and a `string` property that is the text value of the enumeration. ex: `{"value": 0, "string": "OFF"}`. If you use an enumerations array, `min` and `max` will be set automatically for you.
|
||||
@@ -1082,4 +1082,4 @@ View provider Example:
|
||||
return openmct.priority.HIGH;
|
||||
}
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
// This is a Babel config that webpack.coverage.js uses in order to instrument
|
||||
// code with coverage instrumentation.
|
||||
const babelConfig = {
|
||||
plugins: [['babel-plugin-istanbul', {
|
||||
extension: ['.js', '.vue']
|
||||
}]]
|
||||
};
|
||||
|
||||
module.exports = babelConfig;
|
||||
@@ -23,21 +23,23 @@ If this is your first time ever using the Playwright framework, we recommend goi
|
||||
Once you've got an understanding of Playwright, you'll need a baseline understanding of Open MCT:
|
||||
|
||||
1. Follow the steps [Building and Running Open MCT Locally](../README.md#building-and-running-open-mct-locally)
|
||||
2. Once you're serving Open MCT locally, create an Example Telemetry Object (e.g.: 'Sine Wave Generator')
|
||||
2. Once you're serving Open MCT locally, create a 'Display Layout' object. Save it.
|
||||
3. Create a 'Plot' Object (e.g.: 'Stacked Plot')
|
||||
4. Expand the Tree on the left-hand nav and drag and drop the Example Telemetry Object into the Plot Object
|
||||
5. Create a 'Display Layout' object
|
||||
6. From the Tree, Drag the Plot object into the Display Layout
|
||||
4. Create an Example Telemetry Object (e.g.: 'Sine Wave Generator')
|
||||
5. Expand the Tree and note the hierarchy of objects which were created.
|
||||
6. Navigate to the Demo Display Layout Object to edit and modify the embedded plot.
|
||||
7. Modify the embedded plot with Telemetry Data.
|
||||
|
||||
What you've created is a display which mimics the display that a mission control operator might use to understand and model telemetry data.
|
||||
|
||||
Recreate the steps above with Playwright's codegen tool:
|
||||
|
||||
1. `npm run start` in a terminal window
|
||||
2. Open another terminal window and start the Playwright codegen application `npx playwright codegen`
|
||||
3. Navigate the browser to `http://localhost:8080`
|
||||
4. Click the Create button and notice how your actions in the browser are being recorded in the Playwright Inspector
|
||||
5. Continue through the steps 2-6 above
|
||||
1. `npm run start` in a terminal window to serve Open MCT locally
|
||||
2. `npx @playwright/test install` to install playwright and dependencies
|
||||
3. Open another terminal window and start the Playwright codegen application `npx playwright codegen`
|
||||
4. Navigate the browser to `http://localhost:8080`
|
||||
5. Click the Create button and notice how your actions in the browser are being recorded in the Playwright Inspector
|
||||
6. Continue through the steps 2-6 above
|
||||
|
||||
What you've created is an automated test which mimics the creation of a mission control display.
|
||||
|
||||
@@ -148,6 +150,7 @@ Current list of test tags:
|
||||
- `@localStorage` - Captures or generates session storage to manipulate browser state. Useful for excluding in tests which require a persistent backend (i.e. CouchDB).
|
||||
- `@snapshot` - Uses Playwright's snapshot functionality to record a copy of the DOM for direct comparison. Must be run inside of the playwright container.
|
||||
- `@unstable` - A new test or test which is known to be flaky.
|
||||
- `@2p` - Indicates that multiple users are involved, or multiple tabs/pages are used. Useful for testing multi-user interactivity.
|
||||
|
||||
### Continuous Integration
|
||||
|
||||
|
||||
@@ -30,54 +30,239 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* This common function creates a `domainObject` with default options. It is the preferred way of creating objects
|
||||
* in the e2e suite when uninterested in properties of the objects themselves.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} type
|
||||
* @param {string | undefined} name
|
||||
* Defines parameters to be used in the creation of a domain object.
|
||||
* @typedef {Object} CreateObjectOptions
|
||||
* @property {string} type the type of domain object to create (e.g.: "Sine Wave Generator").
|
||||
* @property {string} [name] the desired name of the created domain object.
|
||||
* @property {string | import('../src/api/objects/ObjectAPI').Identifier} [parent] the Identifier or uuid of the parent object.
|
||||
*/
|
||||
async function createDomainObjectWithDefaults(page, type, name) {
|
||||
|
||||
/**
|
||||
* Contains information about the newly created domain object.
|
||||
* @typedef {Object} CreatedObjectInfo
|
||||
* @property {string} name the name of the created object
|
||||
* @property {string} uuid the uuid of the created object
|
||||
* @property {string} url the relative url to the object (for use with `page.goto()`)
|
||||
*/
|
||||
|
||||
/**
|
||||
* This common function creates a domain object with the default options. It is the preferred way of creating objects
|
||||
* in the e2e suite when uninterested in properties of the objects themselves.
|
||||
*
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {CreateObjectOptions} options
|
||||
* @returns {Promise<CreatedObjectInfo>} An object containing information about the newly created domain object.
|
||||
*/
|
||||
async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine' }) {
|
||||
const parentUrl = await getHashUrlToDomainObject(page, parent);
|
||||
|
||||
// Navigate to the parent object. This is necessary to create the object
|
||||
// in the correct location, such as a folder, layout, or plot.
|
||||
await page.goto(`${parentUrl}?hideTree=true`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
//Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
// Click the object specified by 'type'
|
||||
await page.click(`text=${type}`);
|
||||
await page.click(`li:text("${type}")`);
|
||||
|
||||
// Modify the name input field of the domain object to accept 'name'
|
||||
if (name) {
|
||||
const nameInput = page.locator('input[type="text"]').nth(2);
|
||||
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([
|
||||
page.waitForNavigation({waitUntil: 'networkidle'}),
|
||||
page.click('text=OK')
|
||||
page.waitForLoadState(),
|
||||
page.click('[aria-label="Save"]'),
|
||||
// Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
|
||||
// Wait until the URL is updated
|
||||
await page.waitForURL(`**/${parent}/*`);
|
||||
const uuid = await getFocusedObjectUuid(page);
|
||||
const objectUrl = await getHashUrlToDomainObject(page, uuid);
|
||||
|
||||
if (await _isInEditMode(page, uuid)) {
|
||||
// Save (exit edit mode)
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.locator('li[title="Save and Finish Editing"]').click();
|
||||
}
|
||||
|
||||
return {
|
||||
name: name || `Unnamed ${type}`,
|
||||
uuid: uuid,
|
||||
url: objectUrl
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the given `domainObject`'s context menu from the object tree.
|
||||
* Expands the 'My Items' folder if it is not already expanded.
|
||||
* Expands the path to the object and scrolls to it if necessary.
|
||||
*
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} myItemsFolderName the name of the "My Items" folder
|
||||
* @param {string} domainObjectName the display name of the `domainObject`
|
||||
* @param {string} url the url to the object
|
||||
*/
|
||||
async function openObjectTreeContextMenu(page, myItemsFolderName, domainObjectName) {
|
||||
const myItemsFolder = page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3);
|
||||
const className = await myItemsFolder.getAttribute('class');
|
||||
if (!className.includes('c-disclosure-triangle--expanded')) {
|
||||
await myItemsFolder.click();
|
||||
}
|
||||
|
||||
await page.locator(`a:has-text("${domainObjectName}")`).click({
|
||||
async function openObjectTreeContextMenu(page, url) {
|
||||
await page.goto(url);
|
||||
await page.click('button[title="Show selected item in tree"]');
|
||||
await page.locator('.is-navigated-object').click({
|
||||
button: 'right'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the UUID of the currently focused object by parsing the current URL
|
||||
* and returning the last UUID in the path.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @returns {Promise<string>} the uuid of the focused object
|
||||
*/
|
||||
async function getFocusedObjectUuid(page) {
|
||||
const UUIDv4Regexp = /[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/gi;
|
||||
const focusedObjectUuid = await page.evaluate((regexp) => {
|
||||
return window.location.href.split('?')[0].match(regexp).at(-1);
|
||||
}, UUIDv4Regexp);
|
||||
|
||||
return focusedObjectUuid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the hashUrl to the domainObject given its uuid.
|
||||
* Useful for directly navigating to the given domainObject.
|
||||
*
|
||||
* URLs returned will be of the form `'./browse/#/mine/<uuid0>/<uuid1>/...'`
|
||||
*
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} uuid the uuid of the object to get the url for
|
||||
* @returns {Promise<string>} the url of the object
|
||||
*/
|
||||
async function getHashUrlToDomainObject(page, uuid) {
|
||||
const hashUrl = await page.evaluate(async (objectUuid) => {
|
||||
const path = await window.openmct.objects.getOriginalPath(objectUuid);
|
||||
let url = './#/browse/' + [...path].reverse()
|
||||
.map((object) => window.openmct.objects.makeKeyString(object.identifier))
|
||||
.join('/');
|
||||
|
||||
// Drop the vestigial '/ROOT' if it exists
|
||||
if (url.includes('/ROOT')) {
|
||||
url = url.split('/ROOT').join('');
|
||||
}
|
||||
|
||||
return url;
|
||||
}, uuid);
|
||||
|
||||
return hashUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @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((objectIdentifier) => window.openmct.objects.isTransactionActive(objectIdentifier), identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the time conductor mode to either fixed timespan or realtime mode.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {boolean} [isFixedTimespan=true] true for fixed timespan mode, false for realtime mode; default is true
|
||||
*/
|
||||
async function setTimeConductorMode(page, isFixedTimespan = true) {
|
||||
// Click 'mode' button
|
||||
await page.locator('.c-mode-button').click();
|
||||
|
||||
// Switch time conductor mode
|
||||
if (isFixedTimespan) {
|
||||
await page.locator('data-testid=conductor-modeOption-fixed').click();
|
||||
} else {
|
||||
await page.locator('data-testid=conductor-modeOption-realtime').click();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the time conductor to fixed timespan mode
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function setFixedTimeMode(page) {
|
||||
await setTimeConductorMode(page, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the time conductor to realtime mode
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function setRealTimeMode(page) {
|
||||
await setTimeConductorMode(page, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} OffsetValues
|
||||
* @property {string | undefined} hours
|
||||
* @property {string | undefined} mins
|
||||
* @property {string | undefined} secs
|
||||
*/
|
||||
|
||||
/**
|
||||
* Set the values (hours, mins, secs) for the TimeConductor offsets when in realtime mode
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {OffsetValues} offset
|
||||
* @param {import('@playwright/test').Locator} offsetButton
|
||||
*/
|
||||
async function setTimeConductorOffset(page, {hours, mins, secs}, offsetButton) {
|
||||
await offsetButton.click();
|
||||
|
||||
if (hours) {
|
||||
await page.fill('.pr-time-controls__hrs', hours);
|
||||
}
|
||||
|
||||
if (mins) {
|
||||
await page.fill('.pr-time-controls__mins', mins);
|
||||
}
|
||||
|
||||
if (secs) {
|
||||
await page.fill('.pr-time-controls__secs', secs);
|
||||
}
|
||||
|
||||
// Click the check button
|
||||
await page.locator('.pr-time__buttons .icon-check').click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the values (hours, mins, secs) for the start time offset when in realtime mode
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {OffsetValues} offset
|
||||
*/
|
||||
async function setStartOffset(page, offset) {
|
||||
const startOffsetButton = page.locator('data-testid=conductor-start-offset-button');
|
||||
await setTimeConductorOffset(page, offset, startOffsetButton);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the values (hours, mins, secs) for the end time offset when in realtime mode
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {OffsetValues} offset
|
||||
*/
|
||||
async function setEndOffset(page, offset) {
|
||||
const endOffsetButton = page.locator('data-testid=conductor-end-offset-button');
|
||||
await setTimeConductorOffset(page, offset, endOffsetButton);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
module.exports = {
|
||||
createDomainObjectWithDefaults,
|
||||
openObjectTreeContextMenu
|
||||
openObjectTreeContextMenu,
|
||||
getHashUrlToDomainObject,
|
||||
getFocusedObjectUuid,
|
||||
setFixedTimeMode,
|
||||
setRealTimeMode,
|
||||
setStartOffset,
|
||||
setEndOffset
|
||||
};
|
||||
|
||||
28
e2e/helper/addInitExampleFaultProvider.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/*****************************************************************************
|
||||
* 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 should be used to install the Example Fault Provider, this will also install the FaultManagementPlugin (neither of which are installed by default).
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const openmct = window.openmct;
|
||||
openmct.install(openmct.plugins.example.ExampleFaultSource());
|
||||
});
|
||||
30
e2e/helper/addInitExampleFaultProviderStatic.js
Normal file
@@ -0,0 +1,30 @@
|
||||
/*****************************************************************************
|
||||
* 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 should be used to install the Example Fault Provider, this will also install the FaultManagementPlugin (neither of which are installed by default).
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const openmct = window.openmct;
|
||||
const staticFaults = true;
|
||||
|
||||
openmct.install(openmct.plugins.example.ExampleFaultSource(staticFaults));
|
||||
});
|
||||
28
e2e/helper/addInitFaultManagementPlugin.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/*****************************************************************************
|
||||
* 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 should be used to install the Example Fault Provider, this will also install the FaultManagementPlugin (neither of which are installed by default).
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const openmct = window.openmct;
|
||||
openmct.install(openmct.plugins.FaultManagement());
|
||||
});
|
||||
277
e2e/helper/faultUtils.js
Normal file
@@ -0,0 +1,277 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
const path = require('path');
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function navigateToFaultManagementWithExample(page) {
|
||||
// eslint-disable-next-line no-undef
|
||||
await page.addInitScript({ path: path.join(__dirname, './', 'addInitExampleFaultProvider.js') });
|
||||
|
||||
await navigateToFaultItemInTree(page);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function navigateToFaultManagementWithStaticExample(page) {
|
||||
// eslint-disable-next-line no-undef
|
||||
await page.addInitScript({ path: path.join(__dirname, './', 'addInitExampleFaultProviderStatic.js') });
|
||||
|
||||
await navigateToFaultItemInTree(page);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function navigateToFaultManagementWithoutExample(page) {
|
||||
// eslint-disable-next-line no-undef
|
||||
await page.addInitScript({ path: path.join(__dirname, './', 'addInitFaultManagementPlugin.js') });
|
||||
|
||||
await navigateToFaultItemInTree(page);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function navigateToFaultItemInTree(page) {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
// Click text=Fault Management
|
||||
await page.click('text=Fault Management'); // this verifies the plugin has been added
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function acknowledgeFault(page, rowNumber) {
|
||||
await openFaultRowMenu(page, rowNumber);
|
||||
await page.locator('.c-menu >> text="Acknowledge"').click();
|
||||
// Click [aria-label="Save"]
|
||||
await page.locator('[aria-label="Save"]').click();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function shelveMultipleFaults(page, ...nums) {
|
||||
const selectRows = nums.map((num) => {
|
||||
return selectFaultItem(page, num);
|
||||
});
|
||||
await Promise.all(selectRows);
|
||||
|
||||
await page.locator('button:has-text("Shelve")').click();
|
||||
await page.locator('[aria-label="Save"]').click();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function acknowledgeMultipleFaults(page, ...nums) {
|
||||
const selectRows = nums.map((num) => {
|
||||
return selectFaultItem(page, num);
|
||||
});
|
||||
await Promise.all(selectRows);
|
||||
|
||||
await page.locator('button:has-text("Acknowledge")').click();
|
||||
await page.locator('[aria-label="Save"]').click();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function shelveFault(page, rowNumber) {
|
||||
await openFaultRowMenu(page, rowNumber);
|
||||
await page.locator('.c-menu >> text="Shelve"').click();
|
||||
// Click [aria-label="Save"]
|
||||
await page.locator('[aria-label="Save"]').click();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function changeViewTo(page, view) {
|
||||
await page.locator('.c-fault-mgmt__search-row select').first().selectOption(view);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function sortFaultsBy(page, sort) {
|
||||
await page.locator('.c-fault-mgmt__list-header-sortButton select').selectOption(sort);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function enterSearchTerm(page, term) {
|
||||
await page.locator('.c-fault-mgmt-search [aria-label="Search Input"]').fill(term);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function clearSearch(page) {
|
||||
await enterSearchTerm(page, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function selectFaultItem(page, rowNumber) {
|
||||
// eslint-disable-next-line playwright/no-force-option
|
||||
await page.check(`.c-fault-mgmt-item > input >> nth=${rowNumber - 1}`, { force: true }); // this will not work without force true, saw this may be a pw bug
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function getHighestSeverity(page) {
|
||||
const criticalCount = await page.locator('[title=CRITICAL]').count();
|
||||
const warningCount = await page.locator('[title=WARNING]').count();
|
||||
|
||||
if (criticalCount > 0) {
|
||||
return 'CRITICAL';
|
||||
} else if (warningCount > 0) {
|
||||
return 'WARNING';
|
||||
}
|
||||
|
||||
return 'WATCH';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function getLowestSeverity(page) {
|
||||
const warningCount = await page.locator('[title=WARNING]').count();
|
||||
const watchCount = await page.locator('[title=WATCH]').count();
|
||||
|
||||
if (watchCount > 0) {
|
||||
return 'WATCH';
|
||||
} else if (warningCount > 0) {
|
||||
return 'WARNING';
|
||||
}
|
||||
|
||||
return 'CRITICAL';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function getFaultResultCount(page) {
|
||||
const count = await page.locator('.c-faults-list-view-item-body > .c-fault-mgmt__list').count();
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
function getFault(page, rowNumber) {
|
||||
const fault = page.locator(`.c-faults-list-view-item-body > .c-fault-mgmt__list >> nth=${rowNumber - 1}`);
|
||||
|
||||
return fault;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
function getFaultByName(page, name) {
|
||||
const fault = page.locator(`.c-fault-mgmt__list-faultname:has-text("${name}")`);
|
||||
|
||||
return fault;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function getFaultName(page, rowNumber) {
|
||||
const faultName = await page.locator(`.c-fault-mgmt__list-faultname >> nth=${rowNumber - 1}`).textContent();
|
||||
|
||||
return faultName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function getFaultSeverity(page, rowNumber) {
|
||||
const faultSeverity = await page.locator(`.c-faults-list-view-item-body .c-fault-mgmt__list-severity >> nth=${rowNumber - 1}`).getAttribute('title');
|
||||
|
||||
return faultSeverity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function getFaultNamespace(page, rowNumber) {
|
||||
const faultNamespace = await page.locator(`.c-fault-mgmt__list-path >> nth=${rowNumber - 1}`).textContent();
|
||||
|
||||
return faultNamespace;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function getFaultTriggerTime(page, rowNumber) {
|
||||
const faultTriggerTime = await page.locator(`.c-fault-mgmt__list-trigTime >> nth=${rowNumber - 1} >> .c-fault-mgmt-item__value`).textContent();
|
||||
|
||||
return faultTriggerTime.toString().trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function openFaultRowMenu(page, rowNumber) {
|
||||
// select
|
||||
await page.locator(`.c-fault-mgmt-item > .c-fault-mgmt__list-action-button >> nth=${rowNumber - 1}`).click();
|
||||
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
module.exports = {
|
||||
navigateToFaultManagementWithExample,
|
||||
navigateToFaultManagementWithStaticExample,
|
||||
navigateToFaultManagementWithoutExample,
|
||||
navigateToFaultItemInTree,
|
||||
acknowledgeFault,
|
||||
shelveMultipleFaults,
|
||||
acknowledgeMultipleFaults,
|
||||
shelveFault,
|
||||
changeViewTo,
|
||||
sortFaultsBy,
|
||||
enterSearchTerm,
|
||||
clearSearch,
|
||||
selectFaultItem,
|
||||
getHighestSeverity,
|
||||
getLowestSeverity,
|
||||
getFaultResultCount,
|
||||
getFault,
|
||||
getFaultByName,
|
||||
getFaultName,
|
||||
getFaultSeverity,
|
||||
getFaultNamespace,
|
||||
getFaultTriggerTime,
|
||||
openFaultRowMenu
|
||||
};
|
||||
30
e2e/helper/useSnowTheme.js
Normal file
@@ -0,0 +1,30 @@
|
||||
/*****************************************************************************
|
||||
* 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 should be used to install the Snow theme for Open MCT. Espresso is the default
|
||||
// e.g.
|
||||
// await page.addInitScript({ path: path.join(__dirname, 'useSnowTheme.js') });
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const openmct = window.openmct;
|
||||
openmct.install(openmct.plugins.Snow());
|
||||
});
|
||||
@@ -32,6 +32,7 @@ const config = {
|
||||
projects: [
|
||||
{
|
||||
name: 'chrome',
|
||||
testMatch: '**/*.e2e.spec.js', // only run e2e tests
|
||||
use: {
|
||||
browserName: 'chromium'
|
||||
}
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
// playwright.config.js
|
||||
// @ts-check
|
||||
|
||||
/** @type {import('@playwright/test').PlaywrightTestConfig} */
|
||||
/** @type {import('@playwright/test').PlaywrightTestConfig<{ theme: string }>} */
|
||||
const config = {
|
||||
retries: 0, // visual tests should never retry due to snapshot comparison errors
|
||||
testDir: 'tests/visual',
|
||||
timeout: 90 * 1000,
|
||||
workers: 1, // visual tests should never run in parallel due to test pollution
|
||||
testMatch: '**/*.visual.spec.js', // only run visual tests
|
||||
timeout: 60 * 1000,
|
||||
workers: 2, //Limit to 2 for CircleCI Agent
|
||||
webServer: {
|
||||
command: 'cross-env NODE_ENV=test npm run start',
|
||||
url: 'http://localhost:8080/#',
|
||||
@@ -15,17 +16,35 @@ const config = {
|
||||
reuseExistingServer: !process.env.CI
|
||||
},
|
||||
use: {
|
||||
browserName: "chromium",
|
||||
baseURL: 'http://localhost:8080/',
|
||||
headless: true, // this needs to remain headless to avoid visual changes due to GPU
|
||||
headless: true, // this needs to remain headless to avoid visual changes due to GPU rendering in headed browsers
|
||||
ignoreHTTPSErrors: true,
|
||||
screenshot: 'on',
|
||||
trace: 'off',
|
||||
trace: 'on',
|
||||
video: 'off'
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chrome',
|
||||
use: {
|
||||
browserName: 'chromium'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'chrome-snow-theme',
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
theme: 'snow'
|
||||
}
|
||||
}
|
||||
],
|
||||
reporter: [
|
||||
['list'],
|
||||
['junit', { outputFile: 'test-results/results.xml' }]
|
||||
['junit', { outputFile: 'test-results/results.xml' }],
|
||||
['html', {
|
||||
open: 'on-failure',
|
||||
outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840
|
||||
}]
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
@@ -27,7 +27,8 @@
|
||||
*/
|
||||
|
||||
const { test, expect } = require('./baseFixtures');
|
||||
const { createDomainObjectWithDefaults } = require('./appActions');
|
||||
// const { createDomainObjectWithDefaults } = require('./appActions');
|
||||
const path = require('path');
|
||||
|
||||
/**
|
||||
* @typedef {Object} ObjectCreateOptions
|
||||
@@ -36,12 +37,16 @@ const { createDomainObjectWithDefaults } = require('./appActions');
|
||||
*/
|
||||
|
||||
/**
|
||||
* **NOTE: This feature is a work-in-progress and should not currently be used.**
|
||||
*
|
||||
* Used to create a new domain object as a part of getOrCreateDomainObject.
|
||||
* @type {Map<string, string>}
|
||||
*/
|
||||
const createdObjects = new Map();
|
||||
// const createdObjects = new Map();
|
||||
|
||||
/**
|
||||
* **NOTE: This feature is a work-in-progress and should not currently be used.**
|
||||
*
|
||||
* This action will create a domain object for the test to reference and return the uuid. If an object
|
||||
* of a given name already exists, it will return the uuid of that object to the test instead of creating
|
||||
* a new file. The intent is to move object creation out of test suites which are not explicitly worried
|
||||
@@ -50,27 +55,29 @@ const createdObjects = new Map();
|
||||
* @param {ObjectCreateOptions} options
|
||||
* @returns {Promise<string>} uuid of the domain object
|
||||
*/
|
||||
async function getOrCreateDomainObject(page, options) {
|
||||
const { type, name } = options;
|
||||
const objectName = name ? `${type}:${name}` : type;
|
||||
// async function getOrCreateDomainObject(page, options) {
|
||||
// const { type, name } = options;
|
||||
// const objectName = name ? `${type}:${name}` : type;
|
||||
|
||||
if (createdObjects.has(objectName)) {
|
||||
return createdObjects.get(objectName);
|
||||
}
|
||||
// if (createdObjects.has(objectName)) {
|
||||
// return createdObjects.get(objectName);
|
||||
// }
|
||||
|
||||
await createDomainObjectWithDefaults(page, type, name);
|
||||
// await createDomainObjectWithDefaults(page, type, name);
|
||||
|
||||
// Once object is created, get the uuid from the url
|
||||
const uuid = await page.evaluate(() => {
|
||||
return window.location.href.match(/[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/)[0];
|
||||
});
|
||||
// // Once object is created, get the uuid from the url
|
||||
// const uuid = await page.evaluate(() => {
|
||||
// return window.location.href.match(/[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/)[0];
|
||||
// });
|
||||
|
||||
createdObjects.set(objectName, uuid);
|
||||
// createdObjects.set(objectName, uuid);
|
||||
|
||||
return uuid;
|
||||
}
|
||||
// return uuid;
|
||||
// }
|
||||
|
||||
/**
|
||||
* **NOTE: This feature is a work-in-progress and should not currently be used.**
|
||||
*
|
||||
* If provided, these options will be used to get or create the desired domain object before
|
||||
* any tests or test hooks have run.
|
||||
* The `uuid` of the `domainObject` will then be available to use within the scoped tests.
|
||||
@@ -87,7 +94,24 @@ async function getOrCreateDomainObject(page, options) {
|
||||
* ```
|
||||
* @type {ObjectCreateOptions}
|
||||
*/
|
||||
const objectCreateOptions = null;
|
||||
// const objectCreateOptions = null;
|
||||
|
||||
/**
|
||||
* The default theme for VIPER and Open MCT is the 'espresso' theme. Overriding this value with 'snow' in our playwright config.js
|
||||
* will override the default theme by injecting the 'snow' theme on launch.
|
||||
*
|
||||
* ### Example:
|
||||
* ```js
|
||||
* projects: [
|
||||
* {
|
||||
* name: 'chrome-snow-theme',
|
||||
* use: {
|
||||
* browserName: 'chromium',
|
||||
* theme: 'snow'
|
||||
* ```
|
||||
* @type {'snow' | 'espresso'}
|
||||
*/
|
||||
const theme = 'espresso';
|
||||
|
||||
/**
|
||||
* The name of the "My Items" folder in the domain object tree.
|
||||
@@ -99,27 +123,39 @@ const objectCreateOptions = null;
|
||||
const myItemsFolderName = "My Items";
|
||||
|
||||
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) => {
|
||||
// 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') });
|
||||
}
|
||||
|
||||
await use(page);
|
||||
},
|
||||
myItemsFolderName: [myItemsFolderName, { option: true }],
|
||||
// eslint-disable-next-line no-shadow
|
||||
openmctConfig: async ({ myItemsFolderName }, use) => {
|
||||
await use({ myItemsFolderName });
|
||||
},
|
||||
objectCreateOptions: [objectCreateOptions, {option: true}],
|
||||
}
|
||||
// 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);
|
||||
// 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;
|
||||
}
|
||||
// return;
|
||||
// }
|
||||
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
// //Go to baseURL
|
||||
// await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
const uuid = await getOrCreateDomainObject(page, objectCreateOptions);
|
||||
await use({ uuid });
|
||||
}, { auto: true }]
|
||||
// const uuid = await getOrCreateDomainObject(page, objectCreateOptions);
|
||||
// await use({ uuid });
|
||||
// }, { auto: true }]
|
||||
});
|
||||
exports.expect = expect;
|
||||
|
||||
88
e2e/tests/framework/appActions.e2e.spec.js
Normal file
@@ -0,0 +1,88 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
const { test, expect } = require('../../baseFixtures.js');
|
||||
const { createDomainObjectWithDefaults } = require('../../appActions.js');
|
||||
|
||||
test.describe('AppActions', () => {
|
||||
test('createDomainObjectsWithDefaults', async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
const e2eFolder = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder',
|
||||
name: 'e2e folder'
|
||||
});
|
||||
|
||||
await test.step('Create multiple flat objects in a row', async () => {
|
||||
const timer1 = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Timer',
|
||||
name: 'Timer Foo',
|
||||
parent: e2eFolder.uuid
|
||||
});
|
||||
const timer2 = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Timer',
|
||||
name: 'Timer Bar',
|
||||
parent: e2eFolder.uuid
|
||||
});
|
||||
const timer3 = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Timer',
|
||||
name: 'Timer Baz',
|
||||
parent: e2eFolder.uuid
|
||||
});
|
||||
|
||||
await page.goto(timer1.url, { waitUntil: 'networkidle' });
|
||||
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('Timer Bar');
|
||||
await page.goto(timer3.url, { waitUntil: 'networkidle' });
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toHaveText('Timer Baz');
|
||||
});
|
||||
|
||||
await test.step('Create multiple nested objects in a row', async () => {
|
||||
const folder1 = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder',
|
||||
name: 'Folder Foo',
|
||||
parent: e2eFolder.uuid
|
||||
});
|
||||
const folder2 = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder',
|
||||
name: 'Folder Bar',
|
||||
parent: folder1.uuid
|
||||
});
|
||||
const folder3 = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder',
|
||||
name: 'Folder Baz',
|
||||
parent: folder2.uuid
|
||||
});
|
||||
await page.goto(folder1.url, { waitUntil: 'networkidle' });
|
||||
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('Folder Bar');
|
||||
await page.goto(folder3.url, { waitUntil: 'networkidle' });
|
||||
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}`);
|
||||
expect(folder3.url).toBe(`${e2eFolder.url}/${folder1.uuid}/${folder2.uuid}/${folder3.uuid}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -29,7 +29,7 @@ relates to how we've extended it (i.e. ./e2e/baseFixtures.js) and assumptions ma
|
||||
const { test } = require('../../baseFixtures.js');
|
||||
|
||||
test.describe('baseFixtures tests', () => {
|
||||
test('Verify that tests fail if console.error is thrown @framework', async ({ page }) => {
|
||||
test('Verify that tests fail if console.error is thrown', async ({ page }) => {
|
||||
test.fail();
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
@@ -41,7 +41,7 @@ test.describe('baseFixtures tests', () => {
|
||||
]);
|
||||
|
||||
});
|
||||
test('Verify that tests pass if console.warn is thrown @framework', async ({ page }) => {
|
||||
test('Verify that tests pass if console.warn is thrown', async ({ page }) => {
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
|
||||
@@ -52,13 +52,13 @@ const { createDomainObjectWithDefaults } = require('../../appActions');
|
||||
|
||||
// Structure: Try to keep a single describe block per logical groups of tests. If your test runtime exceeds 5 minutes or 500 lines, it's likely that it will need to be split.
|
||||
// Annotations: Please use the @unstable tag so that our automation can pick it up as a part of our test promotion pipeline.
|
||||
test.describe('Renaming Timer Object @unstable', () => {
|
||||
test.describe('Renaming Timer Object', () => {
|
||||
//Create a testcase name which will be obvious when it fails in CI
|
||||
test('Can create a new Timer object and rename it from actions Menu', async ({ page }) => {
|
||||
//Open a browser, navigate to the main page, and wait until all networkevents to resolve
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
//We provide some helper functions in appActions like createDomainObjectWithDefaults. This example will create a Timer object
|
||||
await createDomainObjectWithDefaults(page, 'Timer');
|
||||
await createDomainObjectWithDefaults(page, { type: 'Timer' });
|
||||
//Assert the object to be created and check it's name in the title
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Timer');
|
||||
|
||||
@@ -68,13 +68,12 @@ test.describe('Renaming Timer Object @unstable', () => {
|
||||
|
||||
//Assert that the name has changed in the browser bar to the value we assigned above
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText(newObjectName);
|
||||
|
||||
});
|
||||
test('An existing Timer object can be renamed twice', async ({ page }) => {
|
||||
//Open a browser, navigate to the main page, and wait until all networkevents to resolve
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
//We provide some helper functions in appActions like createDomainObjectWithDefaults. This example will create a Timer object
|
||||
await createDomainObjectWithDefaults(page, 'Timer');
|
||||
await createDomainObjectWithDefaults(page, { type: 'Timer' });
|
||||
//Expect the object to be created and check it's name in the title
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Timer');
|
||||
|
||||
|
||||
@@ -31,29 +31,13 @@ TODO: Provide additional validation of object properties as it grows.
|
||||
|
||||
*/
|
||||
|
||||
const { createDomainObjectWithDefaults } = require('../../appActions.js');
|
||||
const { test, expect } = require('../../pluginFixtures.js');
|
||||
|
||||
test('Generate Visual Test Data @localStorage', async ({ page, context, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
|
||||
test('Generate Visual Test Data @localStorage', async ({ page, context }) => {
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
|
||||
// add overlay plot with defaults
|
||||
await page.locator('li:has-text("Overlay Plot")').click();
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=OK').click(),
|
||||
//Wait for Save Banner to appear1
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
|
||||
// save (exit edit mode)
|
||||
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
const overlayPlot = await createDomainObjectWithDefaults(page, { type: 'Overlay Plot' });
|
||||
|
||||
// click create button
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
@@ -67,16 +51,12 @@ test('Generate Visual Test Data @localStorage', async ({ page, context, openmctC
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=OK').click(),
|
||||
//Wait for Save Banner to appear1
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
|
||||
// focus the overlay plot
|
||||
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=Unnamed Overlay Plot').first().click()
|
||||
]);
|
||||
await page.goto(overlayPlot.url);
|
||||
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Overlay Plot');
|
||||
//Save localStorage for future test execution
|
||||
|
||||
@@ -25,21 +25,22 @@ This test suite is dedicated to testing our use of our custom fixtures to verify
|
||||
that they are working as expected.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../pluginFixtures.js');
|
||||
const { test } = require('../../pluginFixtures.js');
|
||||
|
||||
test.describe('pluginFixtures tests', () => {
|
||||
test.use({ domainObjectName: 'Timer' });
|
||||
let timerUUID;
|
||||
// eslint-disable-next-line playwright/no-skipped-test
|
||||
test.describe.skip('pluginFixtures tests', () => {
|
||||
// test.use({ domainObjectName: 'Timer' });
|
||||
// let timerUUID;
|
||||
|
||||
test('Creates a timer object @framework @unstable', ({ domainObject }) => {
|
||||
const { uuid } = domainObject;
|
||||
const uuidRegexp = /[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/;
|
||||
expect(uuid).toMatch(uuidRegexp);
|
||||
timerUUID = uuid;
|
||||
});
|
||||
// test('Creates a timer object @framework @unstable', ({ domainObject }) => {
|
||||
// const { uuid } = domainObject;
|
||||
// const uuidRegexp = /[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/;
|
||||
// expect(uuid).toMatch(uuidRegexp);
|
||||
// timerUUID = uuid;
|
||||
// });
|
||||
|
||||
test('Provides same uuid for subsequent uses of the same object @framework', ({ domainObject }) => {
|
||||
const { uuid } = domainObject;
|
||||
expect(uuid).toEqual(timerUUID);
|
||||
});
|
||||
// test('Provides same uuid for subsequent uses of the same object @framework', ({ domainObject }) => {
|
||||
// const { uuid } = domainObject;
|
||||
// expect(uuid).toEqual(timerUUID);
|
||||
// });
|
||||
});
|
||||
|
||||
@@ -38,14 +38,14 @@ test.describe('Branding tests', () => {
|
||||
await expect(page.locator('.c-about__image')).toBeVisible();
|
||||
|
||||
// Modify the Build information in 'about' Modal
|
||||
const versionInformationLocator = page.locator('ul.t-info.l-info.s-info');
|
||||
const versionInformationLocator = page.locator('ul.t-info.l-info.s-info').first();
|
||||
await expect(versionInformationLocator).toBeEnabled();
|
||||
await expect.soft(versionInformationLocator).toContainText(/Version: \d/);
|
||||
await expect.soft(versionInformationLocator).toContainText(/Build Date: ((?:Mon|Tue|Wed|Thu|Fri|Sat|Sun))/);
|
||||
await expect.soft(versionInformationLocator).toContainText(/Revision: \b[0-9a-f]{5,40}\b/);
|
||||
await expect.soft(versionInformationLocator).toContainText(/Branch: ./);
|
||||
});
|
||||
test('Verify Links in About Modal', async ({ page }) => {
|
||||
test('Verify Links in About Modal @2p', async ({ page }) => {
|
||||
// Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
|
||||
108
e2e/tests/functional/couchdb.e2e.spec.js
Normal file
@@ -0,0 +1,108 @@
|
||||
/*****************************************************************************
|
||||
* 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 meant to be executed against a couchdb container. More doc to come
|
||||
*
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../baseFixtures');
|
||||
|
||||
test.describe("CouchDB Status Indicator @couchdb", () => {
|
||||
test.use({ failOnConsoleError: false });
|
||||
//TODO BeforeAll Verify CouchDB Connectivity with APIContext
|
||||
test('Shows green if connected', async ({ page }) => {
|
||||
await page.route('**/openmct/mine', route => {
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({})
|
||||
});
|
||||
});
|
||||
|
||||
//Go to baseURL
|
||||
await page.goto('./#/browse/mine?hideTree=true&hideInspector=true', { waitUntil: 'networkidle' });
|
||||
await expect(page.locator('div:has-text("CouchDB is connected")').nth(3)).toBeVisible();
|
||||
});
|
||||
test('Shows red if not connected', async ({ page }) => {
|
||||
await page.route('**/openmct/**', route => {
|
||||
route.fulfill({
|
||||
status: 503,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({})
|
||||
});
|
||||
});
|
||||
|
||||
//Go to baseURL
|
||||
await page.goto('./#/browse/mine?hideTree=true&hideInspector=true', { waitUntil: 'networkidle' });
|
||||
await expect(page.locator('div:has-text("CouchDB is offline")').nth(3)).toBeVisible();
|
||||
});
|
||||
test('Shows unknown if it receives an unexpected response code', async ({ page }) => {
|
||||
await page.route('**/openmct/mine', route => {
|
||||
route.fulfill({
|
||||
status: 418,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({})
|
||||
});
|
||||
});
|
||||
|
||||
//Go to baseURL
|
||||
await page.goto('./#/browse/mine?hideTree=true&hideInspector=true', { waitUntil: 'networkidle' });
|
||||
await expect(page.locator('div:has-text("CouchDB connectivity unknown")').nth(3)).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("CouchDB initialization @couchdb", () => {
|
||||
test.use({ failOnConsoleError: false });
|
||||
test("'My Items' folder is created if it doesn't exist", async ({ page }) => {
|
||||
// Store any relevant PUT requests that happen on the page
|
||||
const createMineFolderRequests = [];
|
||||
page.on('request', req => {
|
||||
// eslint-disable-next-line playwright/no-conditional-in-test
|
||||
if (req.method() === 'PUT' && req.url().endsWith('openmct/mine')) {
|
||||
createMineFolderRequests.push(req);
|
||||
}
|
||||
});
|
||||
|
||||
// Override the first request to GET openmct/mine to return a 404
|
||||
await page.route('**/openmct/mine', route => {
|
||||
route.fulfill({
|
||||
status: 404,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({})
|
||||
});
|
||||
}, { times: 1 });
|
||||
|
||||
// Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
// Verify that error banner is displayed
|
||||
const bannerMessage = await page.locator('.c-message-banner__message').innerText();
|
||||
expect(bannerMessage).toEqual('Failed to retrieve object mine');
|
||||
|
||||
// Verify that a PUT request to create "My Items" folder was made
|
||||
expect.poll(() => createMineFolderRequests.length, {
|
||||
message: 'Verify that PUT request to create "mine" folder was made',
|
||||
timeout: 1000
|
||||
}).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
});
|
||||
@@ -35,7 +35,10 @@ test.describe('Example Event Generator CRUD Operations', () => {
|
||||
//Create a name for the object
|
||||
const newObjectName = 'Test Event Generator';
|
||||
|
||||
await createDomainObjectWithDefaults(page, 'Event Message Generator', newObjectName);
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Event Message Generator',
|
||||
name: newObjectName
|
||||
});
|
||||
|
||||
//Assertions against newly created object which define standard behavior
|
||||
await expect(page.waitForURL(/.*&view=table/)).toBeTruthy();
|
||||
|
||||
@@ -28,7 +28,8 @@ const { test, expect } = require('../../../../baseFixtures');
|
||||
|
||||
test.describe('Sine Wave Generator', () => {
|
||||
test('Create new Sine Wave Generator Object and validate create Form Logic', async ({ page, browserName }) => {
|
||||
test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox');
|
||||
// eslint-disable-next-line playwright/no-skipped-test
|
||||
test.skip(browserName === 'firefox', 'This test needs to be updated to work with firefox');
|
||||
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
212
e2e/tests/functional/moveAndLinkObjects.e2e.spec.js
Normal file
@@ -0,0 +1,212 @@
|
||||
/*****************************************************************************
|
||||
* 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 moving & linking objects.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults } = require('../../appActions');
|
||||
|
||||
test.describe('Move & link item tests', () => {
|
||||
test('Create a basic object and verify that it can be moved to another folder', async ({ page, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
|
||||
// Go to Open MCT
|
||||
await page.goto('./');
|
||||
|
||||
const parentFolder = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder',
|
||||
name: 'Parent Folder'
|
||||
});
|
||||
const childFolder = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder',
|
||||
name: 'Child Folder',
|
||||
parent: parentFolder.uuid
|
||||
});
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder',
|
||||
name: 'Grandchild Folder',
|
||||
parent: childFolder.uuid
|
||||
});
|
||||
|
||||
// Attempt to move parent to its own grandparent
|
||||
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
|
||||
await page.locator('.c-disclosure-triangle >> nth=0').click();
|
||||
|
||||
await page.locator(`a:has-text("Parent Folder") >> nth=0`).click({
|
||||
button: 'right'
|
||||
});
|
||||
|
||||
await page.locator('li.icon-move').click();
|
||||
await page.locator('form[name="mctForm"] >> .c-disclosure-triangle >> nth=0').click();
|
||||
await page.locator('form[name="mctForm"] >> text=Parent Folder').click();
|
||||
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
|
||||
await page.locator('form[name="mctForm"] >> .c-disclosure-triangle >> nth=1').click();
|
||||
await page.locator('form[name="mctForm"] >> text=Child Folder').click();
|
||||
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
|
||||
await page.locator('form[name="mctForm"] >> .c-disclosure-triangle >> nth=2').click();
|
||||
await page.locator('form[name="mctForm"] >> text=Grandchild Folder').click();
|
||||
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
|
||||
await page.locator('form[name="mctForm"] >> text=Parent Folder').click();
|
||||
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
|
||||
await page.locator('[aria-label="Cancel"]').click();
|
||||
|
||||
// Move Child Folder from Parent Folder to My Items
|
||||
await page.locator('.c-disclosure-triangle >> nth=0').click();
|
||||
await page.locator('.c-disclosure-triangle >> nth=1').click();
|
||||
|
||||
await page.locator(`a:has-text("Child Folder") >> nth=0`).click({
|
||||
button: 'right'
|
||||
});
|
||||
await page.locator('li.icon-move').click();
|
||||
await page.locator(`form[name="mctForm"] >> text=${myItemsFolderName}`).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();
|
||||
});
|
||||
test('Create a basic object and verify that it cannot be moved to telemetry object without Composition Provider', async ({ page, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
|
||||
// Go to Open MCT
|
||||
await page.goto('./');
|
||||
|
||||
// Create Telemetry Table
|
||||
let telemetryTable = 'Test Telemetry Table';
|
||||
await page.locator('button:has-text("Create")').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('text=OK').click();
|
||||
|
||||
// Finish editing and save Telemetry Table
|
||||
await page.locator('.c-button--menu.c-button--major.icon-save').click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
// Create New Folder Basic Domain Object
|
||||
let folder = 'Test Folder';
|
||||
await page.locator('button:has-text("Create")').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);
|
||||
|
||||
// See if it's possible to put the folder in the Telemetry object during creation (Soft Assert)
|
||||
await page.locator(`form[name="mctForm"] >> text=${telemetryTable}`).click();
|
||||
let okButton = await page.locator('button.c-button.c-button--major:has-text("OK")');
|
||||
let okButtonStateDisabled = await okButton.isDisabled();
|
||||
expect.soft(okButtonStateDisabled).toBeTruthy();
|
||||
|
||||
// Continue test regardless of assertion and create it in My Items
|
||||
await page.locator(`form[name="mctForm"] >> text=${myItemsFolderName}`).click();
|
||||
await page.locator('text=OK').click();
|
||||
|
||||
// Open My Items
|
||||
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
|
||||
|
||||
// Select Folder Object and select Move from context menu
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator(`a:has-text("${folder}")`).click()
|
||||
]);
|
||||
await page.locator('.c-tree__item.is-navigated-object .c-tree__item__label .c-tree__item__type-icon').click({
|
||||
button: 'right'
|
||||
});
|
||||
await page.locator('li.icon-move').click();
|
||||
|
||||
// See if it's possible to put the folder in the Telemetry object after creation
|
||||
await page.locator(`text=Location Open MCT ${myItemsFolderName} >> span`).nth(3).click();
|
||||
await page.locator(`form[name="mctForm"] >> text=${telemetryTable}`).click();
|
||||
let okButton2 = await page.locator('button.c-button.c-button--major:has-text("OK")');
|
||||
let okButtonStateDisabled2 = await okButton2.isDisabled();
|
||||
expect(okButtonStateDisabled2).toBeTruthy();
|
||||
});
|
||||
|
||||
test('Create a basic object and verify that it can be linked to another folder', async ({ page, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
|
||||
// Go to Open MCT
|
||||
await page.goto('./');
|
||||
|
||||
const parentFolder = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder',
|
||||
name: 'Parent Folder'
|
||||
});
|
||||
const childFolder = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder',
|
||||
name: 'Child Folder',
|
||||
parent: parentFolder.uuid
|
||||
});
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder',
|
||||
name: 'Grandchild Folder',
|
||||
parent: childFolder.uuid
|
||||
});
|
||||
|
||||
// Attempt to link parent to its own grandparent
|
||||
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
|
||||
await page.locator('.c-disclosure-triangle >> nth=0').click();
|
||||
|
||||
await page.locator(`a:has-text("Parent Folder") >> nth=0`).click({
|
||||
button: 'right'
|
||||
});
|
||||
|
||||
await page.locator('li.icon-link').click();
|
||||
await page.locator('form[name="mctForm"] >> .c-disclosure-triangle >> nth=0').click();
|
||||
await page.locator('form[name="mctForm"] >> text=Parent Folder').click();
|
||||
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
|
||||
await page.locator('form[name="mctForm"] >> .c-disclosure-triangle >> nth=1').click();
|
||||
await page.locator('form[name="mctForm"] >> text=Child Folder').click();
|
||||
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
|
||||
await page.locator('form[name="mctForm"] >> .c-disclosure-triangle >> nth=2').click();
|
||||
await page.locator('form[name="mctForm"] >> text=Grandchild Folder').click();
|
||||
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
|
||||
await page.locator('form[name="mctForm"] >> text=Parent Folder').click();
|
||||
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
|
||||
await page.locator('[aria-label="Cancel"]').click();
|
||||
|
||||
// Link Child Folder from Parent Folder to My Items
|
||||
await page.locator('.c-disclosure-triangle >> nth=0').click();
|
||||
await page.locator('.c-disclosure-triangle >> nth=1').click();
|
||||
|
||||
await page.locator(`a:has-text("Child Folder") >> nth=0`).click({
|
||||
button: 'right'
|
||||
});
|
||||
await page.locator('li.icon-link').click();
|
||||
await page.locator(`form[name="mctForm"] >> text=${myItemsFolderName}`).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();
|
||||
});
|
||||
});
|
||||
|
||||
test.fixme('Cannot move a previously created domain object to non-peristable object in Move Modal', async ({ page }) => {
|
||||
//Create a domain object
|
||||
//Save Domain object
|
||||
//Move Object and verify that cannot select non-persistable object
|
||||
//Move Object to My Items
|
||||
//Verify successful move
|
||||
});
|
||||
@@ -1,148 +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 moving objects.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../pluginFixtures');
|
||||
|
||||
test.describe('Move item tests', () => {
|
||||
test('Create a basic object and verify that it can be moved to another folder', async ({ page, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
|
||||
// Go to Open MCT
|
||||
await page.goto('./');
|
||||
|
||||
// Create a new folder in the root my items folder
|
||||
let folder1 = "Folder1";
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
await page.locator('li.icon-folder').click();
|
||||
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"]').click();
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder1);
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=OK').click(),
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
//Wait until Save Banner is gone
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
||||
|
||||
// Create another folder with a new name at default location, which is currently inside Folder 1
|
||||
let folder2 = "Folder2";
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
await page.locator('li.icon-folder').click();
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"]').click();
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder2);
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=OK').click(),
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
//Wait until Save Banner is gone
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
||||
|
||||
// Move Folder 2 from Folder 1 to My Items
|
||||
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
|
||||
await page.locator('.c-tree__scrollable div div:nth-child(2) .c-tree__item .c-tree__item__view-control').click();
|
||||
|
||||
await page.locator(`a:has-text("${folder2}")`).click({
|
||||
button: 'right'
|
||||
});
|
||||
await page.locator('li.icon-move').click();
|
||||
await page.locator(`form[name="mctForm"] >> text=${myItemsFolderName}`).click();
|
||||
|
||||
await page.locator('text=OK').click();
|
||||
|
||||
// Expect that Folder 2 is in My Items, the root folder
|
||||
expect(page.locator(`text=${myItemsFolderName} >> nth=0:has(text=${folder2})`)).toBeTruthy();
|
||||
});
|
||||
test('Create a basic object and verify that it cannot be moved to telemetry object without Composition Provider', async ({ page, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
|
||||
// Go to Open MCT
|
||||
await page.goto('./');
|
||||
|
||||
// Create Telemetry Table
|
||||
let telemetryTable = 'Test Telemetry Table';
|
||||
await page.locator('button:has-text("Create")').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('text=OK').click();
|
||||
|
||||
// Finish editing and save Telemetry Table
|
||||
await page.locator('.c-button--menu.c-button--major.icon-save').click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
// Create New Folder Basic Domain Object
|
||||
let folder = 'Test Folder';
|
||||
await page.locator('button:has-text("Create")').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);
|
||||
|
||||
// See if it's possible to put the folder in the Telemetry object during creation (Soft Assert)
|
||||
await page.locator(`form[name="mctForm"] >> text=${telemetryTable}`).click();
|
||||
let okButton = await page.locator('button.c-button.c-button--major:has-text("OK")');
|
||||
let okButtonStateDisabled = await okButton.isDisabled();
|
||||
expect.soft(okButtonStateDisabled).toBeTruthy();
|
||||
|
||||
// Continue test regardless of assertion and create it in My Items
|
||||
await page.locator(`form[name="mctForm"] >> text=${myItemsFolderName}`).click();
|
||||
await page.locator('text=OK').click();
|
||||
|
||||
// Open My Items
|
||||
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
|
||||
|
||||
// Select Folder Object and select Move from context menu
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator(`a:has-text("${folder}")`).click()
|
||||
]);
|
||||
await page.locator('.c-tree__item.is-navigated-object .c-tree__item__label .c-tree__item__type-icon').click({
|
||||
button: 'right'
|
||||
});
|
||||
await page.locator('li.icon-move').click();
|
||||
|
||||
// See if it's possible to put the folder in the Telemetry object after creation
|
||||
await page.locator(`text=Location Open MCT ${myItemsFolderName} >> span`).nth(3).click();
|
||||
await page.locator(`form[name="mctForm"] >> text=${telemetryTable}`).click();
|
||||
let okButton2 = await page.locator('button.c-button.c-button--major:has-text("OK")');
|
||||
let okButtonStateDisabled2 = await okButton2.isDisabled();
|
||||
expect(okButtonStateDisabled2).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
test.fixme('Cannot move a previously created domain object to non-peristable object in Move Modal', async ({ page }) => {
|
||||
//Create a domain object
|
||||
//Save Domain object
|
||||
//Move Object and verify that cannot select non-persistable object
|
||||
//Move Object to My Items
|
||||
//Verify successful move
|
||||
});
|
||||
@@ -50,7 +50,7 @@ test.describe('Clock Generator CRUD Operations', () => {
|
||||
await page.locator('.icon-arrow-down').click();
|
||||
|
||||
// Verify clicking on the autocomplete arrow collapses the dropdown
|
||||
await expect(page.locator(".c-input--autocomplete__options")).not.toBeVisible();
|
||||
await expect(page.locator(".c-input--autocomplete__options")).toBeHidden();
|
||||
|
||||
// Click timezone input to open dropdown
|
||||
await page.locator('.c-input--autocomplete__input').click();
|
||||
@@ -60,7 +60,7 @@ test.describe('Clock Generator CRUD Operations', () => {
|
||||
// Verify clicking outside the autocomplete dropdown collapses it
|
||||
await page.locator('text=Timezone').click();
|
||||
// Verify clicking on the autocomplete arrow collapses the dropdown
|
||||
await expect(page.locator(".c-input--autocomplete__options")).not.toBeVisible();
|
||||
await expect(page.locator(".c-input--autocomplete__options")).toBeHidden();
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,6 +27,7 @@ demonstrate some playwright for test developers. This pattern should not be re-u
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../../../pluginFixtures.js');
|
||||
const { createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||
|
||||
let conditionSetUrl;
|
||||
let getConditionSetIdentifierFromUrl;
|
||||
@@ -178,3 +179,24 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Basic Condition Set Use', () => {
|
||||
test('Can add a condition', async ({ page }) => {
|
||||
//Navigate to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
// Create a new condition set
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Condition Set',
|
||||
name: "Test Condition Set"
|
||||
});
|
||||
// Change the object to edit mode
|
||||
await page.locator('[title="Edit"]').click();
|
||||
|
||||
// Click Add Condition button
|
||||
await page.locator('#addCondition').click();
|
||||
// Check that the new Unnamed Condition section appears
|
||||
const numOfUnnamedConditions = await page.locator('text=Unnamed Condition').count();
|
||||
expect(numOfUnnamedConditions).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults, setStartOffset, setFixedTimeMode, setRealTimeMode } = require('../../../../appActions');
|
||||
|
||||
test.describe('Testing Display Layout @unstable', () => {
|
||||
let sineWaveObject;
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await setRealTimeMode(page);
|
||||
|
||||
// Create Sine Wave Generator
|
||||
sineWaveObject = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
name: "Test Sine Wave Generator"
|
||||
});
|
||||
});
|
||||
test('alpha-numeric widget telemetry value exactly matches latest telemetry value received in real time', async ({ page }) => {
|
||||
// Create a Display Layout
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Display Layout',
|
||||
name: "Test Display Layout"
|
||||
});
|
||||
// Edit Display 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 Display Layout and save changes
|
||||
await page.dragAndDrop('text=Test Sine Wave Generator', '.l-layout__grid-holder');
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
// Subscribe to the Sine Wave Generator data
|
||||
// On getting data, check if the value found in the Display Layout is the most recent value
|
||||
// from the Sine Wave Generator
|
||||
const getTelemValuePromise = await subscribeToTelemetry(page, sineWaveObject.uuid);
|
||||
const formattedTelemetryValue = await getTelemValuePromise;
|
||||
const displayLayoutValuePromise = await page.waitForSelector(`text="${formattedTelemetryValue}"`);
|
||||
const displayLayoutValue = await displayLayoutValuePromise.textContent();
|
||||
const trimmedDisplayValue = displayLayoutValue.trim();
|
||||
|
||||
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
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Display Layout',
|
||||
name: "Test Display Layout"
|
||||
});
|
||||
// Edit Display 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 Display Layout and save changes
|
||||
await page.dragAndDrop('text=Test Sine Wave Generator', '.l-layout__grid-holder');
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
// Subscribe to the Sine Wave Generator data
|
||||
const getTelemValuePromise = await subscribeToTelemetry(page, sineWaveObject.uuid);
|
||||
// Set an offset of 1 minute and then change the time mode to fixed to set a 1 minute historical window
|
||||
await setStartOffset(page, { mins: '1' });
|
||||
await setFixedTimeMode(page);
|
||||
|
||||
// On getting data, check if the value found in the Display Layout is the most recent value
|
||||
// from the Sine Wave Generator
|
||||
const formattedTelemetryValue = await getTelemValuePromise;
|
||||
const displayLayoutValuePromise = await page.waitForSelector(`text="${formattedTelemetryValue}"`);
|
||||
const displayLayoutValue = await displayLayoutValuePromise.textContent();
|
||||
const trimmedDisplayValue = displayLayoutValue.trim();
|
||||
|
||||
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
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Display Layout',
|
||||
name: "Test Display Layout"
|
||||
});
|
||||
// Edit Display 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 Display Layout and save changes
|
||||
await page.dragAndDrop('text=Test Sine Wave Generator', '.l-layout__grid-holder');
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
expect.soft(await page.locator('.l-layout .l-layout__frame').count()).toEqual(1);
|
||||
|
||||
// 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();
|
||||
|
||||
// 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('text=Remove').click();
|
||||
await page.locator('text=OK').click();
|
||||
|
||||
// delete
|
||||
|
||||
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 }) => {
|
||||
// Create a Display Layout
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Display Layout',
|
||||
name: "Test Display Layout"
|
||||
});
|
||||
// Edit Display 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 Display Layout and save changes
|
||||
await page.dragAndDrop('text=Test Sine Wave Generator', '.l-layout__grid-holder');
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
expect.soft(await page.locator('.l-layout .l-layout__frame').count()).toEqual(1);
|
||||
|
||||
// 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();
|
||||
|
||||
// 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('text=Remove').click();
|
||||
await page.locator('text=OK').click();
|
||||
|
||||
// navigate back to the display layout to confirm it has been removed
|
||||
await page.locator('.c-tree__item .c-tree__item__name:text("Test Display Layout")').click();
|
||||
|
||||
expect.soft(await page.locator('.l-layout .l-layout__frame').count()).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Util for subscribing to a telemetry object by object identifier
|
||||
* Limitations: Currently only works to return telemetry once to the node scope
|
||||
* To Do: See if there's a way to await this multiple times to allow for multiple
|
||||
* values to be returned over time
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} objectIdentifier identifier for object
|
||||
* @returns {Promise<string>} the formatted sin telemetry value
|
||||
*/
|
||||
async function subscribeToTelemetry(page, objectIdentifier) {
|
||||
const getTelemValuePromise = new Promise(resolve => page.exposeFunction('getTelemValue', resolve));
|
||||
|
||||
await page.evaluate(async (telemetryIdentifier) => {
|
||||
const telemetryObject = await window.openmct.objects.get(telemetryIdentifier);
|
||||
const metadata = window.openmct.telemetry.getMetadata(telemetryObject);
|
||||
const formats = await window.openmct.telemetry.getFormatMap(metadata);
|
||||
window.openmct.telemetry.subscribe(telemetryObject, (obj) => {
|
||||
const sinVal = obj.sin;
|
||||
const formattedSinVal = formats.sin.format(sinVal);
|
||||
window.getTelemValue(formattedSinVal);
|
||||
});
|
||||
}, objectIdentifier);
|
||||
|
||||
return getTelemValuePromise;
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const utils = require('../../../../helper/faultUtils');
|
||||
|
||||
test.describe('The Fault Management Plugin using example faults', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await utils.navigateToFaultManagementWithExample(page);
|
||||
});
|
||||
|
||||
test('Shows a criticality icon for every fault', async ({ page }) => {
|
||||
const faultCount = await page.locator('c-fault-mgmt__list').count();
|
||||
const criticalityIconCount = await page.locator('c-fault-mgmt__list-severity').count();
|
||||
|
||||
expect.soft(faultCount).toEqual(criticalityIconCount);
|
||||
});
|
||||
|
||||
test('When selecting a fault, it has an "is-selected" class and it\'s information shows in the inspector', async ({ page }) => {
|
||||
await utils.selectFaultItem(page, 1);
|
||||
|
||||
const selectedFaultName = await page.locator('.c-fault-mgmt__list.is-selected .c-fault-mgmt__list-faultname').textContent();
|
||||
const inspectorFaultNameCount = await page.locator(`.c-inspector__properties >> :text("${selectedFaultName}")`).count();
|
||||
|
||||
await expect.soft(page.locator('.c-faults-list-view-item-body > .c-fault-mgmt__list').first()).toHaveClass(/is-selected/);
|
||||
expect.soft(inspectorFaultNameCount).toEqual(1);
|
||||
});
|
||||
|
||||
test('When selecting multiple faults, no specific fault information is shown in the inspector', async ({ page }) => {
|
||||
await utils.selectFaultItem(page, 1);
|
||||
await utils.selectFaultItem(page, 2);
|
||||
|
||||
const selectedRows = page.locator('.c-fault-mgmt__list.is-selected .c-fault-mgmt__list-faultname');
|
||||
expect.soft(await selectedRows.count()).toEqual(2);
|
||||
|
||||
const firstSelectedFaultName = await selectedRows.nth(0).textContent();
|
||||
const secondSelectedFaultName = await selectedRows.nth(1).textContent();
|
||||
const firstNameInInspectorCount = await page.locator(`.c-inspector__properties >> :text("${firstSelectedFaultName}")`).count();
|
||||
const secondNameInInspectorCount = await page.locator(`.c-inspector__properties >> :text("${secondSelectedFaultName}")`).count();
|
||||
|
||||
expect.soft(firstNameInInspectorCount).toEqual(0);
|
||||
expect.soft(secondNameInInspectorCount).toEqual(0);
|
||||
});
|
||||
|
||||
test('Allows you to shelve a fault', async ({ page }) => {
|
||||
const shelvedFaultName = await utils.getFaultName(page, 2);
|
||||
const beforeShelvedFault = utils.getFaultByName(page, shelvedFaultName);
|
||||
|
||||
expect.soft(await beforeShelvedFault.count()).toBe(1);
|
||||
|
||||
await utils.shelveFault(page, 2);
|
||||
|
||||
// check it is removed from standard view
|
||||
const afterShelvedFault = utils.getFaultByName(page, shelvedFaultName);
|
||||
expect.soft(await afterShelvedFault.count()).toBe(0);
|
||||
|
||||
await utils.changeViewTo(page, 'shelved');
|
||||
|
||||
const shelvedViewFault = utils.getFaultByName(page, shelvedFaultName);
|
||||
|
||||
expect.soft(await shelvedViewFault.count()).toBe(1);
|
||||
});
|
||||
|
||||
test('Allows you to acknowledge a fault', async ({ page }) => {
|
||||
const acknowledgedFaultName = await utils.getFaultName(page, 3);
|
||||
|
||||
await utils.acknowledgeFault(page, 3);
|
||||
|
||||
const fault = utils.getFault(page, 3);
|
||||
await expect.soft(fault).toHaveClass(/is-acknowledged/);
|
||||
|
||||
await utils.changeViewTo(page, 'acknowledged');
|
||||
|
||||
const acknowledgedViewFaultName = await utils.getFaultName(page, 1);
|
||||
expect.soft(acknowledgedFaultName).toEqual(acknowledgedViewFaultName);
|
||||
});
|
||||
|
||||
test('Allows you to shelve multiple faults', async ({ page }) => {
|
||||
const shelvedFaultNameOne = await utils.getFaultName(page, 1);
|
||||
const shelvedFaultNameFour = await utils.getFaultName(page, 4);
|
||||
|
||||
const beforeShelvedFaultOne = utils.getFaultByName(page, shelvedFaultNameOne);
|
||||
const beforeShelvedFaultFour = utils.getFaultByName(page, shelvedFaultNameFour);
|
||||
|
||||
expect.soft(await beforeShelvedFaultOne.count()).toBe(1);
|
||||
expect.soft(await beforeShelvedFaultFour.count()).toBe(1);
|
||||
|
||||
await utils.shelveMultipleFaults(page, 1, 4);
|
||||
|
||||
// check it is removed from standard view
|
||||
const afterShelvedFaultOne = utils.getFaultByName(page, shelvedFaultNameOne);
|
||||
const afterShelvedFaultFour = utils.getFaultByName(page, shelvedFaultNameFour);
|
||||
expect.soft(await afterShelvedFaultOne.count()).toBe(0);
|
||||
expect.soft(await afterShelvedFaultFour.count()).toBe(0);
|
||||
|
||||
await utils.changeViewTo(page, 'shelved');
|
||||
|
||||
const shelvedViewFaultOne = utils.getFaultByName(page, shelvedFaultNameOne);
|
||||
const shelvedViewFaultFour = utils.getFaultByName(page, shelvedFaultNameFour);
|
||||
|
||||
expect.soft(await shelvedViewFaultOne.count()).toBe(1);
|
||||
expect.soft(await shelvedViewFaultFour.count()).toBe(1);
|
||||
});
|
||||
|
||||
test('Allows you to acknowledge multiple faults', async ({ page }) => {
|
||||
const acknowledgedFaultNameTwo = await utils.getFaultName(page, 2);
|
||||
const acknowledgedFaultNameFive = await utils.getFaultName(page, 5);
|
||||
|
||||
await utils.acknowledgeMultipleFaults(page, 2, 5);
|
||||
|
||||
const faultTwo = utils.getFault(page, 2);
|
||||
const faultFive = utils.getFault(page, 5);
|
||||
|
||||
// check they have been acknowledged
|
||||
await expect.soft(faultTwo).toHaveClass(/is-acknowledged/);
|
||||
await expect.soft(faultFive).toHaveClass(/is-acknowledged/);
|
||||
|
||||
await utils.changeViewTo(page, 'acknowledged');
|
||||
|
||||
const acknowledgedViewFaultTwo = utils.getFaultByName(page, acknowledgedFaultNameTwo);
|
||||
const acknowledgedViewFaultFive = utils.getFaultByName(page, acknowledgedFaultNameFive);
|
||||
|
||||
expect.soft(await acknowledgedViewFaultTwo.count()).toBe(1);
|
||||
expect.soft(await acknowledgedViewFaultFive.count()).toBe(1);
|
||||
});
|
||||
|
||||
test('Allows you to search faults', async ({ page }) => {
|
||||
const faultThreeNamespace = await utils.getFaultNamespace(page, 3);
|
||||
const faultTwoName = await utils.getFaultName(page, 2);
|
||||
const faultFiveTriggerTime = await utils.getFaultTriggerTime(page, 5);
|
||||
|
||||
// should be all faults (5)
|
||||
let faultResultCount = await utils.getFaultResultCount(page);
|
||||
expect.soft(faultResultCount).toEqual(5);
|
||||
|
||||
// search namespace
|
||||
await utils.enterSearchTerm(page, faultThreeNamespace);
|
||||
|
||||
faultResultCount = await utils.getFaultResultCount(page);
|
||||
expect.soft(faultResultCount).toEqual(1);
|
||||
expect.soft(await utils.getFaultNamespace(page, 1)).toEqual(faultThreeNamespace);
|
||||
|
||||
// all faults
|
||||
await utils.clearSearch(page);
|
||||
faultResultCount = await utils.getFaultResultCount(page);
|
||||
expect.soft(faultResultCount).toEqual(5);
|
||||
|
||||
// search name
|
||||
await utils.enterSearchTerm(page, faultTwoName);
|
||||
|
||||
faultResultCount = await utils.getFaultResultCount(page);
|
||||
expect.soft(faultResultCount).toEqual(1);
|
||||
expect.soft(await utils.getFaultName(page, 1)).toEqual(faultTwoName);
|
||||
|
||||
// all faults
|
||||
await utils.clearSearch(page);
|
||||
faultResultCount = await utils.getFaultResultCount(page);
|
||||
expect.soft(faultResultCount).toEqual(5);
|
||||
|
||||
// search triggerTime
|
||||
await utils.enterSearchTerm(page, faultFiveTriggerTime);
|
||||
|
||||
faultResultCount = await utils.getFaultResultCount(page);
|
||||
expect.soft(faultResultCount).toEqual(1);
|
||||
expect.soft(await utils.getFaultTriggerTime(page, 1)).toEqual(faultFiveTriggerTime);
|
||||
});
|
||||
|
||||
test('Allows you to sort faults', async ({ page }) => {
|
||||
const highestSeverity = await utils.getHighestSeverity(page);
|
||||
const lowestSeverity = await utils.getLowestSeverity(page);
|
||||
const faultOneName = 'Example Fault 1';
|
||||
const faultFiveName = 'Example Fault 5';
|
||||
let firstFaultName = await utils.getFaultName(page, 1);
|
||||
|
||||
expect.soft(firstFaultName).toEqual(faultOneName);
|
||||
|
||||
await utils.sortFaultsBy(page, 'oldest-first');
|
||||
|
||||
firstFaultName = await utils.getFaultName(page, 1);
|
||||
expect.soft(firstFaultName).toEqual(faultFiveName);
|
||||
|
||||
await utils.sortFaultsBy(page, 'severity');
|
||||
|
||||
const sortedHighestSeverity = await utils.getFaultSeverity(page, 1);
|
||||
const sortedLowestSeverity = await utils.getFaultSeverity(page, 5);
|
||||
expect.soft(sortedHighestSeverity).toEqual(highestSeverity);
|
||||
expect.soft(sortedLowestSeverity).toEqual(lowestSeverity);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
test.describe('The Fault Management Plugin without using example faults', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await utils.navigateToFaultManagementWithoutExample(page);
|
||||
});
|
||||
|
||||
test('Shows no faults when no faults are provided', async ({ page }) => {
|
||||
const faultCount = await page.locator('c-fault-mgmt__list').count();
|
||||
|
||||
expect.soft(faultCount).toEqual(0);
|
||||
|
||||
await utils.changeViewTo(page, 'acknowledged');
|
||||
const acknowledgedCount = await page.locator('c-fault-mgmt__list').count();
|
||||
expect.soft(acknowledgedCount).toEqual(0);
|
||||
|
||||
await utils.changeViewTo(page, 'shelved');
|
||||
const shelvedCount = await page.locator('c-fault-mgmt__list').count();
|
||||
expect.soft(shelvedCount).toEqual(0);
|
||||
});
|
||||
|
||||
test('Will return no faults when searching', async ({ page }) => {
|
||||
await utils.enterSearchTerm(page, 'fault');
|
||||
|
||||
const faultCount = await page.locator('c-fault-mgmt__list').count();
|
||||
|
||||
expect.soft(faultCount).toEqual(0);
|
||||
});
|
||||
});
|
||||
@@ -25,9 +25,10 @@ This test suite is dedicated to tests which verify the basic operations surround
|
||||
but only assume that example imagery is present.
|
||||
*/
|
||||
/* globals process */
|
||||
|
||||
const { v4: uuid } = require('uuid');
|
||||
const { waitForAnimations } = require('../../../../baseFixtures');
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||
const backgroundImageSelector = '.c-imagery__main-image__background-image';
|
||||
const panHotkey = process.platform === 'linux' ? ['Control', 'Alt'] : ['Alt'];
|
||||
const expectedAltText = process.platform === 'linux' ? 'Ctrl+Alt drag to pan' : 'Alt drag to pan';
|
||||
@@ -39,26 +40,17 @@ test.describe('Example Imagery Object', () => {
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
//Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
// Create a default 'Example Imagery' object
|
||||
createDomainObjectWithDefaults(page, { type: 'Example Imagery' });
|
||||
|
||||
// Click text=Example Imagery
|
||||
await page.click('text=Example Imagery');
|
||||
|
||||
// Click text=OK
|
||||
await Promise.all([
|
||||
page.waitForNavigation({waitUntil: 'networkidle'}),
|
||||
page.click('text=OK'),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
page.waitForNavigation(),
|
||||
page.locator(backgroundImageSelector).hover({trial: true}),
|
||||
// eslint-disable-next-line playwright/missing-playwright-await
|
||||
expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery')
|
||||
]);
|
||||
// Close Banner
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
|
||||
//Wait until Save Banner is gone
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
// Verify that the created object is focused
|
||||
});
|
||||
|
||||
test('Can use Mouse Wheel to zoom in and out of latest image', async ({ page }) => {
|
||||
@@ -85,7 +77,8 @@ test.describe('Example Imagery Object', () => {
|
||||
});
|
||||
|
||||
test('Can adjust image brightness/contrast by dragging the sliders', async ({ page, browserName }) => {
|
||||
test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox');
|
||||
// eslint-disable-next-line playwright/no-skipped-test
|
||||
test.skip(browserName === 'firefox', 'This test needs to be updated to work with firefox');
|
||||
// Open the image filter menu
|
||||
await page.locator('[role=toolbar] button[title="Brightness and contrast"]').click();
|
||||
|
||||
@@ -208,7 +201,7 @@ test.describe('Example Imagery Object', () => {
|
||||
const pausePlayButton = page.locator('.c-button.pause-play');
|
||||
|
||||
// open the time conductor drop down
|
||||
await page.locator('button:has-text("Fixed Timespan")').click();
|
||||
await page.locator('.c-mode-button').click();
|
||||
|
||||
// Click local clock
|
||||
await page.locator('[data-testid="conductor-modeOption-realtime"]').click();
|
||||
@@ -430,16 +423,12 @@ test.describe('Example imagery thumbnails resize in display layouts', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// test.fixme('Can use Mouse Wheel to zoom in and out of previous image');
|
||||
// test.fixme('Can use alt+drag to move around image once zoomed in');
|
||||
// test.fixme('Clicking on the left arrow should pause the imagery and go to previous image');
|
||||
// test.fixme('If the imagery view is in pause mode, images still come in');
|
||||
// test.fixme('If the imagery view is not in pause mode, it should be updated when new images come in');
|
||||
test.describe('Example Imagery in Flexible layout', () => {
|
||||
test('Example Imagery in Flexible layout @unstable', async ({ page, browserName, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
|
||||
test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox');
|
||||
// eslint-disable-next-line playwright/no-skipped-test
|
||||
test.skip(browserName === 'firefox', 'This test needs to be updated to work with firefox');
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/5326'
|
||||
@@ -532,7 +521,7 @@ test.describe('Example Imagery in Flexible layout', () => {
|
||||
await page.locator('.c-mode-button').click();
|
||||
|
||||
// Select local clock mode
|
||||
await page.locator('[data-testid=conductor-modeOption-realtime]').click();
|
||||
await page.locator('[data-testid=conductor-modeOption-realtime]').nth(0).click();
|
||||
|
||||
// Zoom in on next image
|
||||
await mouseZoomIn(page);
|
||||
@@ -581,6 +570,40 @@ test.describe('Example Imagery in Tabs view', () => {
|
||||
test.fixme('If the imagery view is not in pause mode, it should be updated when new images come in');
|
||||
});
|
||||
|
||||
test.describe('Example Imagery in Time Strip', () => {
|
||||
test('ensure that clicking a thumbnail loads the image in large view', async ({ page, browserName }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/5632'
|
||||
});
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
const timeStripObject = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Time Strip',
|
||||
name: 'Time Strip'.concat(' ', uuid())
|
||||
});
|
||||
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Example Imagery',
|
||||
name: 'Example Imagery'.concat(' ', uuid()),
|
||||
parent: timeStripObject.uuid
|
||||
});
|
||||
// Navigate to timestrip
|
||||
await page.goto(timeStripObject.url);
|
||||
|
||||
await page.locator('.c-imagery-tsv-container').hover();
|
||||
// get url of the hovered image
|
||||
const hoveredImg = page.locator('.c-imagery-tsv div.c-imagery-tsv__image-wrapper:hover img');
|
||||
const hoveredImgSrc = await hoveredImg.getAttribute('src');
|
||||
expect(hoveredImgSrc).toBeTruthy();
|
||||
await page.locator('.c-imagery-tsv-container').click();
|
||||
// get image of view large container
|
||||
const viewLargeImg = page.locator('img.c-imagery__main-image__image');
|
||||
const viewLargeImgSrc = await viewLargeImg.getAttribute('src');
|
||||
expect(viewLargeImgSrc).toBeTruthy();
|
||||
expect(viewLargeImgSrc).toEqual(hoveredImgSrc);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
|
||||
120
e2e/tests/functional/plugins/lad/lad.e2e.spec.js
Normal file
@@ -0,0 +1,120 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults, setStartOffset, setFixedTimeMode, setRealTimeMode } = require('../../../../appActions');
|
||||
|
||||
test.describe('Testing LAD table @unstable', () => {
|
||||
let sineWaveObject;
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await setRealTimeMode(page);
|
||||
|
||||
// Create Sine Wave Generator
|
||||
sineWaveObject = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
name: "Test Sine Wave Generator"
|
||||
});
|
||||
});
|
||||
test('telemetry value exactly matches latest telemetry value received in real time', async ({ page }) => {
|
||||
// Create LAD table
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'LAD Table',
|
||||
name: "Test LAD Table"
|
||||
});
|
||||
// Edit LAD table
|
||||
await page.locator('[title="Edit"]').click();
|
||||
|
||||
// Expand the 'My Items' folder in the left tree
|
||||
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();
|
||||
// Add the Sine Wave Generator to the LAD table and save changes
|
||||
await page.dragAndDrop('text=Test Sine Wave Generator', '.c-lad-table-wrapper');
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
// Subscribe to the Sine Wave Generator data
|
||||
// On getting data, check if the value found in the LAD table is the most recent value
|
||||
// from the Sine Wave Generator
|
||||
const getTelemValuePromise = await subscribeToTelemetry(page, sineWaveObject.uuid);
|
||||
const subscribeTelemValue = await getTelemValuePromise;
|
||||
const ladTableValuePromise = await page.waitForSelector(`text="${subscribeTelemValue}"`);
|
||||
const ladTableValue = await ladTableValuePromise.textContent();
|
||||
|
||||
expect(ladTableValue).toBe(subscribeTelemValue);
|
||||
});
|
||||
test('telemetry value exactly matches latest telemetry value received in fixed time', async ({ page }) => {
|
||||
// Create LAD table
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'LAD Table',
|
||||
name: "Test LAD Table"
|
||||
});
|
||||
// Edit LAD table
|
||||
await page.locator('[title="Edit"]').click();
|
||||
|
||||
// Expand the 'My Items' folder in the left tree
|
||||
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();
|
||||
// Add the Sine Wave Generator to the LAD table and save changes
|
||||
await page.dragAndDrop('text=Test Sine Wave Generator', '.c-lad-table-wrapper');
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
// Subscribe to the Sine Wave Generator data
|
||||
const getTelemValuePromise = await subscribeToTelemetry(page, sineWaveObject.uuid);
|
||||
// Set an offset of 1 minute and then change the time mode to fixed to set a 1 minute historical window
|
||||
await setStartOffset(page, { mins: '1' });
|
||||
await setFixedTimeMode(page);
|
||||
|
||||
// On getting data, check if the value found in the LAD table is the most recent value
|
||||
// from the Sine Wave Generator
|
||||
const subscribeTelemValue = await getTelemValuePromise;
|
||||
const ladTableValuePromise = await page.waitForSelector(`text="${subscribeTelemValue}"`);
|
||||
const ladTableValue = await ladTableValuePromise.textContent();
|
||||
|
||||
expect(ladTableValue).toBe(subscribeTelemValue);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Util for subscribing to a telemetry object by object identifier
|
||||
* Limitations: Currently only works to return telemetry once to the node scope
|
||||
* To Do: See if there's a way to await this multiple times to allow for multiple
|
||||
* values to be returned over time
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} objectIdentifier identifier for object
|
||||
* @returns {Promise<string>} the formatted sin telemetry value
|
||||
*/
|
||||
async function subscribeToTelemetry(page, objectIdentifier) {
|
||||
const getTelemValuePromise = new Promise(resolve => page.exposeFunction('getTelemValue', resolve));
|
||||
|
||||
await page.evaluate(async (telemetryIdentifier) => {
|
||||
const telemetryObject = await window.openmct.objects.get(telemetryIdentifier);
|
||||
const metadata = window.openmct.telemetry.getMetadata(telemetryObject);
|
||||
const formats = await window.openmct.telemetry.getFormatMap(metadata);
|
||||
window.openmct.telemetry.subscribe(telemetryObject, (obj) => {
|
||||
const sinVal = obj.sin;
|
||||
const formattedSinVal = formats.sin.format(sinVal);
|
||||
window.getTelemValue(formattedSinVal);
|
||||
});
|
||||
}, objectIdentifier);
|
||||
|
||||
return getTelemValuePromise;
|
||||
}
|
||||
@@ -86,6 +86,23 @@ test.describe('Notebook section tests', () => {
|
||||
//Delete 3rd section
|
||||
//1st is selected and there is no default notebook
|
||||
});
|
||||
test.fixme('Section rename operations', async ({ page }) => {
|
||||
// Create a new notebook
|
||||
// Add a section
|
||||
// Rename the section but do not confirm
|
||||
// Keyboard press 'Escape'
|
||||
// Verify that the section name reverts to the default name
|
||||
// Rename the section but do not confirm
|
||||
// Keyboard press 'Enter'
|
||||
// Verify that the section name is updated
|
||||
// Rename the section to "" (empty string)
|
||||
// Keyboard press 'Enter' to confirm
|
||||
// Verify that the section name reverts to the default name
|
||||
// Rename the section to something long that overflows the text box
|
||||
// Verify that the section name is not truncated while input is active
|
||||
// Confirm the section name edit
|
||||
// Verify that the section name is truncated now that input is not active
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Notebook page tests', () => {
|
||||
@@ -107,6 +124,23 @@ test.describe('Notebook page tests', () => {
|
||||
//Delete 3rd page
|
||||
//First is now selected and there is no default notebook
|
||||
});
|
||||
test.fixme('Page rename operations', async ({ page }) => {
|
||||
// Create a new notebook
|
||||
// Add a page
|
||||
// Rename the page but do not confirm
|
||||
// Keyboard press 'Escape'
|
||||
// Verify that the page name reverts to the default name
|
||||
// Rename the page but do not confirm
|
||||
// Keyboard press 'Enter'
|
||||
// Verify that the page name is updated
|
||||
// Rename the page to "" (empty string)
|
||||
// Keyboard press 'Enter' to confirm
|
||||
// Verify that the page name reverts to the default name
|
||||
// Rename the page to something long that overflows the text box
|
||||
// Verify that the page name is not truncated while input is active
|
||||
// Confirm the page name edit
|
||||
// Verify that the page name is truncated now that input is not active
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Notebook search tests', () => {
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
*****************************************************************************/
|
||||
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { openObjectTreeContextMenu } = require('../../../../appActions');
|
||||
const { openObjectTreeContextMenu, createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||
const path = require('path');
|
||||
|
||||
const TEST_TEXT = 'Testing text for entries.';
|
||||
@@ -30,8 +30,9 @@ const CUSTOM_NAME = 'CUSTOM_NAME';
|
||||
const NOTEBOOK_DROP_AREA = '.c-notebook__drag-area';
|
||||
|
||||
test.describe('Restricted Notebook', () => {
|
||||
let notebook;
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await startAndAddRestrictedNotebookObject(page);
|
||||
notebook = await startAndAddRestrictedNotebookObject(page);
|
||||
});
|
||||
|
||||
test('Can be renamed @addInit', async ({ page }) => {
|
||||
@@ -39,9 +40,7 @@ test.describe('Restricted Notebook', () => {
|
||||
});
|
||||
|
||||
test('Can be deleted if there are no locked pages @addInit', async ({ page, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
|
||||
await openObjectTreeContextMenu(page, myItemsFolderName, `Unnamed ${CUSTOM_NAME}`);
|
||||
await openObjectTreeContextMenu(page, notebook.url);
|
||||
|
||||
const menuOptions = page.locator('.c-menu ul');
|
||||
await expect.soft(menuOptions).toContainText('Remove');
|
||||
@@ -76,9 +75,9 @@ test.describe('Restricted Notebook', () => {
|
||||
});
|
||||
|
||||
test.describe('Restricted Notebook with at least one entry and with the page locked @addInit', () => {
|
||||
|
||||
let notebook;
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await startAndAddRestrictedNotebookObject(page);
|
||||
notebook = await startAndAddRestrictedNotebookObject(page);
|
||||
await enterTextEntry(page);
|
||||
await lockPage(page);
|
||||
|
||||
@@ -86,9 +85,9 @@ test.describe('Restricted Notebook with at least one entry and with the page loc
|
||||
await page.locator('button.c-notebook__toggle-nav-button').click();
|
||||
});
|
||||
|
||||
test('Locked page should now be in a locked state @addInit @unstable', async ({ page, openmctConfig }, testInfo) => {
|
||||
test.fixme(testInfo.project === 'chrome-beta', "Test is unreliable on chrome-beta");
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
test('Locked page should now be in a locked state @addInit @unstable', async ({ page }, testInfo) => {
|
||||
// eslint-disable-next-line playwright/no-skipped-test
|
||||
test.skip(testInfo.project === 'chrome-beta', "Test is unreliable on chrome-beta");
|
||||
// main lock message on page
|
||||
const lockMessage = page.locator('text=This page has been committed and cannot be modified or removed');
|
||||
expect.soft(await lockMessage.count()).toEqual(1);
|
||||
@@ -98,7 +97,7 @@ test.describe('Restricted Notebook with at least one entry and with the page loc
|
||||
expect.soft(await pageLockIcon.count()).toEqual(1);
|
||||
|
||||
// no way to remove a restricted notebook with a locked page
|
||||
await openObjectTreeContextMenu(page, myItemsFolderName, `Unnamed ${CUSTOM_NAME}`);
|
||||
await openObjectTreeContextMenu(page, notebook.url);
|
||||
const menuOptions = page.locator('.c-menu ul');
|
||||
|
||||
await expect(menuOptions).not.toContainText('Remove');
|
||||
@@ -178,13 +177,8 @@ async function startAndAddRestrictedNotebookObject(page) {
|
||||
// eslint-disable-next-line no-undef
|
||||
await page.addInitScript({ path: path.join(__dirname, '../../../../helper/', 'addInitRestrictedNotebook.js') });
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.click('button:has-text("Create")');
|
||||
await page.click(`text=${CUSTOM_NAME}`); // secondarily tests renamability also
|
||||
// Click text=OK
|
||||
await Promise.all([
|
||||
page.waitForNavigation({waitUntil: 'networkidle'}),
|
||||
page.click('text=OK')
|
||||
]);
|
||||
|
||||
return createDomainObjectWithDefaults(page, { type: CUSTOM_NAME });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -25,25 +25,18 @@ This test suite is dedicated to tests which verify form functionality.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||
|
||||
/**
|
||||
* Creates a notebook object and adds an entry.
|
||||
* @param {import('@playwright/test').Page} - page to load
|
||||
* @param {number} [iterations = 1] - the number of entries to create
|
||||
*/
|
||||
async function createNotebookAndEntry(page, myItemsFolderName, iterations = 1) {
|
||||
async function createNotebookAndEntry(page, iterations = 1) {
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
// Click button:has-text("Create")
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
await page.locator('[title="Create and save timestamped notes with embedded object snapshots."]').click();
|
||||
// Click button:has-text("OK")
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator(`[name="mctForm"] >> text=${myItemsFolderName}`).click(),
|
||||
page.locator('button:has-text("OK")').click()
|
||||
]);
|
||||
createDomainObjectWithDefaults(page, { type: 'Notebook' });
|
||||
|
||||
for (let iteration = 0; iteration < iterations; iteration++) {
|
||||
// Click text=To start a new entry, click here or drag and drop any object
|
||||
@@ -52,7 +45,6 @@ async function createNotebookAndEntry(page, myItemsFolderName, iterations = 1) {
|
||||
await page.locator(entryLocator).click();
|
||||
await page.locator(entryLocator).fill(`Entry ${iteration}`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,32 +52,35 @@ async function createNotebookAndEntry(page, myItemsFolderName, iterations = 1) {
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {number} [iterations = 1] - the number of entries (and tags) to create
|
||||
*/
|
||||
async function createNotebookEntryAndTags(page, myItemsFolderName, iterations = 1) {
|
||||
await createNotebookAndEntry(page, myItemsFolderName, iterations);
|
||||
async function createNotebookEntryAndTags(page, iterations = 1) {
|
||||
await createNotebookAndEntry(page, iterations);
|
||||
|
||||
for (let iteration = 0; iteration < iterations; iteration++) {
|
||||
// Click text=To start a new entry, click here or drag and drop any object
|
||||
// Hover and click "Add Tag" button
|
||||
// Hover is needed here to "slow down" the actions while running in headless mode
|
||||
await page.hover(`button:has-text("Add Tag") >> nth = ${iteration}`);
|
||||
await page.locator(`button:has-text("Add Tag") >> nth = ${iteration}`).click();
|
||||
|
||||
// Click [placeholder="Type to select tag"]
|
||||
// Click inside the tag search input
|
||||
await page.locator('[placeholder="Type to select tag"]').click();
|
||||
// Click text=Driving
|
||||
// Select the "Driving" tag
|
||||
await page.locator('[aria-label="Autocomplete Options"] >> text=Driving').click();
|
||||
|
||||
// Click button:has-text("Add Tag")
|
||||
// Hover and click "Add Tag" button
|
||||
// Hover is needed here to "slow down" the actions while running in headless mode
|
||||
await page.hover(`button:has-text("Add Tag") >> nth = ${iteration}`);
|
||||
await page.locator(`button:has-text("Add Tag") >> nth = ${iteration}`).click();
|
||||
// Click [placeholder="Type to select tag"]
|
||||
// Click inside the tag search input
|
||||
await page.locator('[placeholder="Type to select tag"]').click();
|
||||
// Click text=Science
|
||||
// Select the "Science" tag
|
||||
await page.locator('[aria-label="Autocomplete Options"] >> text=Science').click();
|
||||
}
|
||||
}
|
||||
|
||||
test.describe('Tagging in Notebooks', () => {
|
||||
test('Can load tags', async ({ page, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
test.describe('Tagging in Notebooks @addInit', () => {
|
||||
test('Can load tags', async ({ page }) => {
|
||||
|
||||
await createNotebookAndEntry(page, myItemsFolderName);
|
||||
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();
|
||||
|
||||
@@ -96,10 +91,8 @@ test.describe('Tagging in Notebooks', () => {
|
||||
await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Drilling");
|
||||
await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Driving");
|
||||
});
|
||||
test('Can add tags', async ({ page, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
|
||||
await createNotebookEntryAndTags(page, myItemsFolderName);
|
||||
test('Can add tags', async ({ page }) => {
|
||||
await createNotebookEntryAndTags(page);
|
||||
|
||||
await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Science");
|
||||
await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Driving");
|
||||
@@ -113,10 +106,8 @@ test.describe('Tagging in Notebooks', () => {
|
||||
await expect(page.locator('[aria-label="Autocomplete Options"]')).not.toContainText("Driving");
|
||||
await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Drilling");
|
||||
});
|
||||
test('Can search for tags', async ({ page, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
|
||||
await createNotebookEntryAndTags(page, myItemsFolderName);
|
||||
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"]
|
||||
@@ -135,17 +126,16 @@ test.describe('Tagging in Notebooks', () => {
|
||||
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('[aria-label="Search Result"]')).not.toBeVisible();
|
||||
await expect(page.locator('[aria-label="Search Result"]')).not.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, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
|
||||
await createNotebookEntryAndTags(page, myItemsFolderName);
|
||||
test('Can delete tags', async ({ page }) => {
|
||||
await createNotebookEntryAndTags(page);
|
||||
await page.locator('[aria-label="Notebook Entries"]').click();
|
||||
// Delete Driving
|
||||
await page.locator('text=Science Driving Add Tag >> button').nth(1).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");
|
||||
@@ -154,28 +144,31 @@ test.describe('Tagging in Notebooks', () => {
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc');
|
||||
await expect(page.locator('[aria-label="Search Result"]')).not.toContainText("Driving");
|
||||
});
|
||||
test('Tags persist across reload', async ({ page, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
|
||||
test('Can delete objects with tags and neither return in search', async ({ page }) => {
|
||||
await createNotebookEntryAndTags(page);
|
||||
// Delete Notebook
|
||||
await page.locator('button[title="More options"]').click();
|
||||
await page.locator('li[title="Remove this object from its containing object."]').click();
|
||||
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 matching results.')).toBeVisible();
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sci');
|
||||
await expect(page.locator('text=No matching results.')).toBeVisible();
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('dri');
|
||||
await expect(page.locator('text=No matching results.')).toBeVisible();
|
||||
});
|
||||
test('Tags persist across reload', async ({ page }) => {
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
// Create a clock object we can navigate to
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
// Click Clock
|
||||
await page.click('text=Clock');
|
||||
// Click button:has-text("OK")
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator(`[name="mctForm"] >> text=${myItemsFolderName}`).click(),
|
||||
page.locator('button:has-text("OK")').click()
|
||||
]);
|
||||
|
||||
await page.click('.c-disclosure-triangle');
|
||||
await createDomainObjectWithDefaults(page, { type: 'Clock' });
|
||||
|
||||
const ITERATIONS = 4;
|
||||
await createNotebookEntryAndTags(page, myItemsFolderName, ITERATIONS);
|
||||
await createNotebookEntryAndTags(page, ITERATIONS);
|
||||
|
||||
for (let iteration = 0; iteration < ITERATIONS; iteration++) {
|
||||
const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`;
|
||||
@@ -183,6 +176,11 @@ test.describe('Tagging in Notebooks', () => {
|
||||
await expect(page.locator(entryLocator)).toContainText("Driving");
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.goto('./#/browse/mine?hideTree=false'),
|
||||
page.click('.c-disclosure-triangle')
|
||||
]);
|
||||
// Click Unnamed Clock
|
||||
await page.click('text="Unnamed Clock"');
|
||||
|
||||
|
||||
|
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 |
@@ -28,9 +28,10 @@ const { test, expect } = require('../../../../pluginFixtures');
|
||||
|
||||
test.describe('Handle missing object for plots', () => {
|
||||
test('Displays empty div for missing stacked plot item @unstable', async ({ page, browserName, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
// eslint-disable-next-line playwright/no-skipped-test
|
||||
test.skip(browserName === 'firefox', 'Firefox failing due to console events being missed');
|
||||
|
||||
test.fixme(browserName === 'firefox', 'Firefox failing due to console events being missed');
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
const errorLogs = [];
|
||||
|
||||
page.on("console", (message) => {
|
||||
|
||||
@@ -20,55 +20,26 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
const { createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
|
||||
test.describe('Telemetry Table', () => {
|
||||
test('unpauses and filters data when paused by button and user changes bounds', async ({ page, openmctConfig }) => {
|
||||
test('unpauses and filters data when paused by button and user changes bounds', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/5113'
|
||||
});
|
||||
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
const bannerMessage = '.c-message-banner__message';
|
||||
const createButton = 'button:has-text("Create")';
|
||||
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
// Click create button
|
||||
await page.locator(createButton).click();
|
||||
await page.locator('li:has-text("Telemetry Table")').click();
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=OK').click(),
|
||||
// Wait for Save Banner to appear
|
||||
page.waitForSelector(bannerMessage)
|
||||
]);
|
||||
|
||||
// Save (exit edit mode)
|
||||
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(3).click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
// Click create button
|
||||
await page.locator(createButton).click();
|
||||
|
||||
// add Sine Wave Generator with defaults
|
||||
await page.locator('li:has-text("Sine Wave Generator")').click();
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=OK').click(),
|
||||
// Wait for Save Banner to appear
|
||||
page.waitForSelector(bannerMessage)
|
||||
]);
|
||||
const table = await createDomainObjectWithDefaults(page, { type: 'Telemetry Table' });
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
parent: table.uuid
|
||||
});
|
||||
|
||||
// focus the Telemetry Table
|
||||
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=Unnamed Telemetry Table').first().click()
|
||||
]);
|
||||
page.goto(table.url);
|
||||
|
||||
// Click pause button
|
||||
const pauseButton = page.locator('button.c-button.icon-pause');
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
*****************************************************************************/
|
||||
|
||||
const { test, expect } = require('../../../../baseFixtures');
|
||||
const { setFixedTimeMode, setRealTimeMode, setStartOffset, setEndOffset } = require('../../../../appActions');
|
||||
|
||||
test.describe('Time conductor operations', () => {
|
||||
test('validate start time does not exceeds end time', async ({ page }) => {
|
||||
@@ -142,93 +143,28 @@ test.describe('Time conductor input fields real-time mode', () => {
|
||||
await expect(page.locator('data-testid=conductor-end-offset-button')).toContainText('00:00:01');
|
||||
|
||||
// Verify url parameters persist after mode switch
|
||||
await page.waitForNavigation();
|
||||
await page.waitForNavigation({ waitUntil: 'networkidle' });
|
||||
expect(page.url()).toContain(`startDelta=${startDelta}`);
|
||||
expect(page.url()).toContain(`endDelta=${endDelta}`);
|
||||
});
|
||||
|
||||
test.fixme('time conductor history in fixed time mode will track changing start and end times', async ({ page }) => {
|
||||
// change start time, verify it's tracked in history
|
||||
// change end time, verify it's tracked in history
|
||||
});
|
||||
|
||||
test.fixme('time conductor history in realtime mode will track changing start and end times', async ({ page }) => {
|
||||
// change start offset, verify it's tracked in history
|
||||
// change end offset, verify it's tracked in history
|
||||
});
|
||||
|
||||
test.fixme('time conductor history allows you to set a historical timeframe', async ({ page }) => {
|
||||
// make sure there are historical history options
|
||||
// select an option and make sure the time conductor start and end bounds are updated correctly
|
||||
});
|
||||
|
||||
test.fixme('time conductor history allows you to set a realtime offsets', async ({ page }) => {
|
||||
// make sure there are realtime history options
|
||||
// select an option and verify the offsets are updated correctly
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @typedef {Object} OffsetValues
|
||||
* @property {string | undefined} hours
|
||||
* @property {string | undefined} mins
|
||||
* @property {string | undefined} secs
|
||||
*/
|
||||
|
||||
/**
|
||||
* Set the values (hours, mins, secs) for the start time offset when in realtime mode
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {OffsetValues} offset
|
||||
*/
|
||||
async function setStartOffset(page, offset) {
|
||||
const startOffsetButton = page.locator('data-testid=conductor-start-offset-button');
|
||||
await setTimeConductorOffset(page, offset, startOffsetButton);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the values (hours, mins, secs) for the end time offset when in realtime mode
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {OffsetValues} offset
|
||||
*/
|
||||
async function setEndOffset(page, offset) {
|
||||
const endOffsetButton = page.locator('data-testid=conductor-end-offset-button');
|
||||
await setTimeConductorOffset(page, offset, endOffsetButton);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the time conductor to fixed timespan mode
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function setFixedTimeMode(page) {
|
||||
await setTimeConductorMode(page, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the time conductor to realtime mode
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function setRealTimeMode(page) {
|
||||
await setTimeConductorMode(page, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the values (hours, mins, secs) for the TimeConductor offsets when in realtime mode
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {OffsetValues} offset
|
||||
* @param {import('@playwright/test').Locator} offsetButton
|
||||
*/
|
||||
async function setTimeConductorOffset(page, {hours, mins, secs}, offsetButton) {
|
||||
await offsetButton.click();
|
||||
|
||||
if (hours) {
|
||||
await page.fill('.pr-time-controls__hrs', hours);
|
||||
}
|
||||
|
||||
if (mins) {
|
||||
await page.fill('.pr-time-controls__mins', mins);
|
||||
}
|
||||
|
||||
if (secs) {
|
||||
await page.fill('.pr-time-controls__secs', secs);
|
||||
}
|
||||
|
||||
// Click the check button
|
||||
await page.locator('.icon-check').click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the time conductor mode to either fixed timespan or realtime mode.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {boolean} [isFixedTimespan=true] true for fixed timespan mode, false for realtime mode; default is true
|
||||
*/
|
||||
async function setTimeConductorMode(page, isFixedTimespan = true) {
|
||||
// Click 'mode' button
|
||||
await page.locator('.c-mode-button').click();
|
||||
|
||||
// Switch time conductor mode
|
||||
if (isFixedTimespan) {
|
||||
await page.locator('data-testid=conductor-modeOption-fixed').click();
|
||||
} else {
|
||||
await page.locator('data-testid=conductor-modeOption-realtime').click();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,27 +21,28 @@
|
||||
*****************************************************************************/
|
||||
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { openObjectTreeContextMenu } = require('../../../../appActions');
|
||||
|
||||
const options = {
|
||||
type: 'Timer'
|
||||
};
|
||||
const { openObjectTreeContextMenu, createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||
|
||||
test.describe('Timer', () => {
|
||||
test.use({ objectCreateOptions: options });
|
||||
let timer;
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
timer = await createDomainObjectWithDefaults(page, { type: 'timer' });
|
||||
});
|
||||
|
||||
test('Can perform actions on the Timer', async ({ page, openmctConfig }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/4313'
|
||||
});
|
||||
|
||||
const { myItemsFolderName } = await openmctConfig;
|
||||
const timerUrl = timer.url;
|
||||
|
||||
await test.step("From the tree context menu", async () => {
|
||||
await triggerTimerContextMenuAction(page, myItemsFolderName, 'Start');
|
||||
await triggerTimerContextMenuAction(page, myItemsFolderName, 'Pause');
|
||||
await triggerTimerContextMenuAction(page, myItemsFolderName, 'Restart at 0');
|
||||
await triggerTimerContextMenuAction(page, myItemsFolderName, 'Stop');
|
||||
await triggerTimerContextMenuAction(page, timerUrl, 'Start');
|
||||
await triggerTimerContextMenuAction(page, timerUrl, 'Pause');
|
||||
await triggerTimerContextMenuAction(page, timerUrl, 'Restart at 0');
|
||||
await triggerTimerContextMenuAction(page, timerUrl, 'Stop');
|
||||
});
|
||||
|
||||
await test.step("From the 3dot menu", async () => {
|
||||
@@ -74,9 +75,9 @@ test.describe('Timer', () => {
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {TimerAction} action
|
||||
*/
|
||||
async function triggerTimerContextMenuAction(page, myItemsFolderName, action) {
|
||||
async function triggerTimerContextMenuAction(page, timerUrl, action) {
|
||||
const menuAction = `.c-menu ul li >> text="${action}"`;
|
||||
await openObjectTreeContextMenu(page, myItemsFolderName, "Unnamed Timer");
|
||||
await openObjectTreeContextMenu(page, timerUrl);
|
||||
await page.locator(menuAction).click();
|
||||
assertTimerStateAfterAction(page, action);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults } = require('../../appActions');
|
||||
const { v4: uuid } = require('uuid');
|
||||
|
||||
test.describe('Grand Search', () => {
|
||||
test('Can search for objects, and subsequent search dropdown behaves properly', async ({ page, openmctConfig }) => {
|
||||
@@ -41,7 +43,7 @@ test.describe('Grand Search', () => {
|
||||
await expect(page.locator('[aria-label="Search Result"] >> nth=3')).toContainText(`Clock D ${myItemsFolderName} Red Folder Blue Folder`);
|
||||
// Click text=Elements >> nth=0
|
||||
await page.locator('text=Elements').first().click();
|
||||
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).not.toBeVisible();
|
||||
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeHidden();
|
||||
|
||||
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click();
|
||||
await page.locator('[aria-label="Clock A clock result"] >> text=Clock A').click();
|
||||
@@ -54,11 +56,11 @@ test.describe('Grand Search', () => {
|
||||
|
||||
// Click [aria-label="OpenMCT Search"] a >> nth=0
|
||||
await page.locator('[aria-label="OpenMCT Search"] a').first().click();
|
||||
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).not.toBeVisible();
|
||||
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeHidden();
|
||||
|
||||
// Fill [aria-label="OpenMCT Search"] input[type="search"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('foo');
|
||||
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).not.toBeVisible();
|
||||
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeHidden();
|
||||
|
||||
// Click text=Snapshot Save and Finish Editing Save and Continue Editing >> button >> nth=1
|
||||
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
|
||||
@@ -107,15 +109,21 @@ test.describe("Search Tests @unstable", () => {
|
||||
|
||||
// Verify that no results are found
|
||||
expect(await searchResults.count()).toBe(0);
|
||||
|
||||
// Verify proper message appears
|
||||
await expect(page.locator('text=No matching results.')).toBeVisible();
|
||||
});
|
||||
|
||||
test('Validate single object in search result', async ({ page }) => {
|
||||
test('Validate single object in search result @couchdb', async ({ page }) => {
|
||||
//Go to baseURL
|
||||
await page.goto("./", { waitUntil: "networkidle" });
|
||||
|
||||
// Create a folder object
|
||||
const folderName = 'testFolder';
|
||||
await createFolderObject(page, folderName);
|
||||
const folderName = uuid();
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'folder',
|
||||
name: folderName
|
||||
});
|
||||
|
||||
// Full search for object
|
||||
await page.type("input[type=search]", folderName);
|
||||
@@ -124,7 +132,7 @@ test.describe("Search Tests @unstable", () => {
|
||||
await waitForSearchCompletion(page);
|
||||
|
||||
// Get the search results
|
||||
const searchResults = await page.locator(searchResultSelector);
|
||||
const searchResults = page.locator(searchResultSelector);
|
||||
|
||||
// Verify that one result is found
|
||||
expect(await searchResults.count()).toBe(1);
|
||||
|
||||
138
e2e/tests/functional/tree.e2e.spec.js
Normal file
@@ -0,0 +1,138 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
const { test, expect } = require('../../pluginFixtures.js');
|
||||
const {
|
||||
createDomainObjectWithDefaults,
|
||||
openObjectTreeContextMenu
|
||||
} = require('../../appActions.js');
|
||||
|
||||
test.describe('Tree operations', () => {
|
||||
test('Renaming an object reorders the tree @unstable', async ({ page, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder',
|
||||
name: 'Foo'
|
||||
});
|
||||
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder',
|
||||
name: 'Bar'
|
||||
});
|
||||
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder',
|
||||
name: 'Baz'
|
||||
});
|
||||
|
||||
const clock1 = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Clock',
|
||||
name: 'aaa'
|
||||
});
|
||||
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Clock',
|
||||
name: 'www'
|
||||
});
|
||||
|
||||
// Expand the root folder
|
||||
await expandTreePaneItemByName(page, myItemsFolderName);
|
||||
|
||||
await test.step("Reorders objects with the same tree depth", async () => {
|
||||
await getAndAssertTreeItems(page, ['aaa', 'Bar', 'Baz', 'Foo', 'www']);
|
||||
await renameObjectFromContextMenu(page, clock1.url, 'zzz');
|
||||
await getAndAssertTreeItems(page, ['Bar', 'Baz', 'Foo', 'www', 'zzz']);
|
||||
});
|
||||
|
||||
await test.step("Reorders links to objects as well as original objects", async () => {
|
||||
await page.click('role=treeitem[name=/Bar/]');
|
||||
await page.dragAndDrop('role=treeitem[name=/www/]', '.c-object-view');
|
||||
await page.dragAndDrop('role=treeitem[name=/zzz/]', '.c-object-view');
|
||||
await page.click('role=treeitem[name=/Baz/]');
|
||||
await page.dragAndDrop('role=treeitem[name=/www/]', '.c-object-view');
|
||||
await page.dragAndDrop('role=treeitem[name=/zzz/]', '.c-object-view');
|
||||
await page.click('role=treeitem[name=/Foo/]');
|
||||
await page.dragAndDrop('role=treeitem[name=/www/]', '.c-object-view');
|
||||
await page.dragAndDrop('role=treeitem[name=/zzz/]', '.c-object-view');
|
||||
// Expand the unopened folders
|
||||
await expandTreePaneItemByName(page, 'Bar');
|
||||
await expandTreePaneItemByName(page, 'Baz');
|
||||
await expandTreePaneItemByName(page, 'Foo');
|
||||
|
||||
await renameObjectFromContextMenu(page, clock1.url, '___');
|
||||
await getAndAssertTreeItems(page,
|
||||
[
|
||||
"___",
|
||||
"Bar",
|
||||
"___",
|
||||
"www",
|
||||
"Baz",
|
||||
"___",
|
||||
"www",
|
||||
"Foo",
|
||||
"___",
|
||||
"www",
|
||||
"www"
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {Array<string>} expected
|
||||
*/
|
||||
async function getAndAssertTreeItems(page, expected) {
|
||||
const treeItems = page.locator('[role="treeitem"]');
|
||||
const allTexts = await treeItems.allInnerTexts();
|
||||
// Get rid of root folder ('My Items') as its position will not change
|
||||
allTexts.shift();
|
||||
expect(allTexts).toEqual(expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} name
|
||||
*/
|
||||
async function expandTreePaneItemByName(page, name) {
|
||||
const treePane = page.locator('#tree-pane');
|
||||
const treeItem = treePane.locator(`role=treeitem[expanded=false][name=/${name}/]`);
|
||||
const expandTriangle = treeItem.locator('.c-disclosure-triangle');
|
||||
await expandTriangle.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} myItemsFolderName
|
||||
* @param {string} url
|
||||
* @param {string} newName
|
||||
*/
|
||||
async function renameObjectFromContextMenu(page, url, newName) {
|
||||
await openObjectTreeContextMenu(page, url);
|
||||
await page.click('li:text("Edit Properties")');
|
||||
const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]');
|
||||
await nameInput.fill("");
|
||||
await nameInput.fill(newName);
|
||||
await page.click('[aria-label="Save"]');
|
||||
}
|
||||
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 18 KiB |
161
e2e/tests/plugins/plot/missingPlotObj.e2e.spec.js
Normal file
@@ -0,0 +1,161 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
Tests to verify log plot functionality when objects are missing
|
||||
*/
|
||||
|
||||
const { test } = require('../../../fixtures.js');
|
||||
const { expect } = require('@playwright/test');
|
||||
|
||||
test.describe('Handle missing object for plots', () => {
|
||||
test('Displays empty div for missing stacked plot item', async ({ page }) => {
|
||||
const errorLogs = [];
|
||||
|
||||
page.on("console", (message) => {
|
||||
if (message.type() === 'warning') {
|
||||
errorLogs.push(message.text());
|
||||
}
|
||||
});
|
||||
|
||||
//Make stacked plot
|
||||
await makeStackedPlot(page);
|
||||
|
||||
//Gets local storage and deletes the last sine wave generator in the stacked plot
|
||||
const localStorage = await page.evaluate(() => window.localStorage);
|
||||
const parsedData = JSON.parse(localStorage.mct);
|
||||
const keys = Object.keys(parsedData);
|
||||
const lastKey = keys[keys.length - 1];
|
||||
|
||||
delete parsedData[lastKey];
|
||||
|
||||
//Sets local storage with missing object
|
||||
await page.evaluate(
|
||||
`window.localStorage.setItem('mct', '${JSON.stringify(parsedData)}')`
|
||||
);
|
||||
|
||||
//Reloads page and clicks on stacked plot
|
||||
await Promise.all([
|
||||
page.reload(),
|
||||
page.waitForLoadState('networkidle')
|
||||
]);
|
||||
|
||||
//Verify Main section is there on load
|
||||
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Stacked Plot');
|
||||
|
||||
await page.locator('text=Open MCT My Items >> span').nth(3).click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=Unnamed Stacked Plot').first().click()
|
||||
]);
|
||||
|
||||
//Check that there is only one stacked item plot with a plot, the missing one will be empty
|
||||
await expect(page.locator(".c-plot--stacked-container:has(.gl-plot)")).toHaveCount(1);
|
||||
//Verify that console.warn is thrown
|
||||
await expect(errorLogs).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* This is used the create a stacked plot object
|
||||
* @private
|
||||
*/
|
||||
async function makeStackedPlot(page) {
|
||||
// fresh page with time range from 2022-03-29 22:00:00.000Z to 2022-03-29 22:00:30.000Z
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
// create stacked plot
|
||||
await page.locator('button.c-create-button').click();
|
||||
await page.locator('li:has-text("Stacked Plot")').click();
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation({ waitUntil: 'networkidle'}),
|
||||
page.locator('text=OK').click(),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
|
||||
//Wait until Save Banner is gone
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
||||
|
||||
// save the stacked plot
|
||||
await saveStackedPlot(page);
|
||||
|
||||
// create a sinewave generator
|
||||
await createSineWaveGenerator(page);
|
||||
|
||||
// click on stacked plot
|
||||
await page.locator('text=Open MCT My Items >> span').nth(3).click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=Unnamed Stacked Plot').first().click()
|
||||
]);
|
||||
|
||||
// create a second sinewave generator
|
||||
await createSineWaveGenerator(page);
|
||||
|
||||
// click on stacked plot
|
||||
await page.locator('text=Open MCT My Items >> span').nth(3).click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=Unnamed Stacked Plot').first().click()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is used to save a stacked plot object
|
||||
* @private
|
||||
*/
|
||||
async function saveStackedPlot(page) {
|
||||
// save stacked plot
|
||||
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
|
||||
|
||||
await Promise.all([
|
||||
page.locator('text=Save and Finish Editing').click(),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
//Wait until Save Banner is gone
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached' });
|
||||
}
|
||||
|
||||
/**
|
||||
* This is used to create a sine wave generator object
|
||||
* @private
|
||||
*/
|
||||
async function createSineWaveGenerator(page) {
|
||||
//Create sine wave generator
|
||||
await page.locator('button.c-create-button').click();
|
||||
await page.locator('li:has-text("Sine Wave Generator")').click();
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation({ waitUntil: 'networkidle'}),
|
||||
page.locator('text=OK').click(),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
//Wait until Save Banner is gone
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
||||
}
|
||||
41
e2e/tests/plugins/remoteClock/remoteClock.e2e.spec.js
Normal file
@@ -0,0 +1,41 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
const { test } = require('../../../fixtures.js');
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { expect } = require('@playwright/test');
|
||||
|
||||
test.describe('Remote Clock', () => {
|
||||
// eslint-disable-next-line require-await
|
||||
test.fixme('blocks historical requests until first tick is received', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/5221'
|
||||
});
|
||||
// addInitScript to with remote clock
|
||||
// Switch time conductor mode to 'remote clock'
|
||||
// Navigate to telemetry
|
||||
// Verify that the plot renders historical data within the correct bounds
|
||||
// Refresh the page
|
||||
// Verify again that the plot renders historical data within the correct bounds
|
||||
});
|
||||
});
|
||||
184
e2e/tests/plugins/timer/timer.e2e.spec.js
Normal file
@@ -0,0 +1,184 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
const { test } = require('../../../fixtures.js');
|
||||
const { expect } = require('@playwright/test');
|
||||
|
||||
test.describe('Timer', () => {
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
//Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
//Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
// Click 'Timer'
|
||||
await page.click('text=Timer');
|
||||
|
||||
// Click text=OK
|
||||
await Promise.all([
|
||||
page.waitForNavigation({waitUntil: 'networkidle'}),
|
||||
page.click('text=OK')
|
||||
]);
|
||||
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Timer');
|
||||
});
|
||||
|
||||
test('Can perform actions on the Timer', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/4313'
|
||||
});
|
||||
|
||||
await test.step("From the tree context menu", async () => {
|
||||
await triggerTimerContextMenuAction(page, 'Start');
|
||||
await triggerTimerContextMenuAction(page, 'Pause');
|
||||
await triggerTimerContextMenuAction(page, 'Restart at 0');
|
||||
await triggerTimerContextMenuAction(page, 'Stop');
|
||||
});
|
||||
|
||||
await test.step("From the 3dot menu", async () => {
|
||||
await triggerTimer3dotMenuAction(page, 'Start');
|
||||
await triggerTimer3dotMenuAction(page, 'Pause');
|
||||
await triggerTimer3dotMenuAction(page, 'Restart at 0');
|
||||
await triggerTimer3dotMenuAction(page, 'Stop');
|
||||
});
|
||||
|
||||
await test.step("From the object view", async () => {
|
||||
await triggerTimerViewAction(page, 'Start');
|
||||
await triggerTimerViewAction(page, 'Pause');
|
||||
await triggerTimerViewAction(page, 'Restart at 0');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Actions that can be performed on a timer from context menus.
|
||||
* @typedef {'Start' | 'Stop' | 'Pause' | 'Restart at 0'} TimerAction
|
||||
*/
|
||||
|
||||
/**
|
||||
* Actions that can be performed on a timer from the object view.
|
||||
* @typedef {'Start' | 'Pause' | 'Restart at 0'} TimerViewAction
|
||||
*/
|
||||
|
||||
/**
|
||||
* Open the timer context menu from the object tree.
|
||||
* Expands the 'My Items' folder if it is not already expanded.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function openTimerContextMenu(page) {
|
||||
const myItemsFolder = page.locator('text=Open MCT My Items >> span').nth(3);
|
||||
const className = await myItemsFolder.getAttribute('class');
|
||||
if (!className.includes('c-disclosure-triangle--expanded')) {
|
||||
await myItemsFolder.click();
|
||||
}
|
||||
|
||||
await page.locator(`a:has-text("Unnamed Timer")`).click({
|
||||
button: 'right'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a timer action from the tree context menu
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {TimerAction} action
|
||||
*/
|
||||
async function triggerTimerContextMenuAction(page, action) {
|
||||
const menuAction = `.c-menu ul li >> text="${action}"`;
|
||||
await openTimerContextMenu(page);
|
||||
await page.locator(menuAction).click();
|
||||
assertTimerStateAfterAction(page, action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a timer action from the 3dot menu
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {TimerAction} action
|
||||
*/
|
||||
async function triggerTimer3dotMenuAction(page, action) {
|
||||
const menuAction = `.c-menu ul li >> text="${action}"`;
|
||||
const threeDotMenuButton = 'button[title="More options"]';
|
||||
let isActionAvailable = false;
|
||||
let iterations = 0;
|
||||
// Dismiss/open the 3dot menu until the action is available
|
||||
// or a maxiumum number of iterations is reached
|
||||
while (!isActionAvailable && iterations <= 20) {
|
||||
await page.click('.c-object-view');
|
||||
await page.click(threeDotMenuButton);
|
||||
isActionAvailable = await page.locator(menuAction).isVisible();
|
||||
iterations++;
|
||||
}
|
||||
|
||||
await page.locator(menuAction).click();
|
||||
assertTimerStateAfterAction(page, action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a timer action from the object view
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {TimerViewAction} action
|
||||
*/
|
||||
async function triggerTimerViewAction(page, action) {
|
||||
const buttonTitle = buttonTitleFromAction(action);
|
||||
await page.click(`button[title="${buttonTitle}"]`);
|
||||
assertTimerStateAfterAction(page, action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes in a TimerViewAction and returns the button title
|
||||
* @param {TimerViewAction} action
|
||||
*/
|
||||
function buttonTitleFromAction(action) {
|
||||
switch (action) {
|
||||
case 'Start':
|
||||
return 'Start';
|
||||
case 'Pause':
|
||||
return 'Pause';
|
||||
case 'Restart at 0':
|
||||
return 'Reset';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the timer state after a timer action has been performed.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {TimerAction} action
|
||||
*/
|
||||
async function assertTimerStateAfterAction(page, action) {
|
||||
let timerStateClass;
|
||||
switch (action) {
|
||||
case 'Start':
|
||||
case 'Restart at 0':
|
||||
timerStateClass = "is-started";
|
||||
break;
|
||||
case 'Stop':
|
||||
timerStateClass = 'is-stopped';
|
||||
break;
|
||||
case 'Pause':
|
||||
timerStateClass = 'is-paused';
|
||||
break;
|
||||
}
|
||||
|
||||
await expect.soft(page.locator('.c-timer')).toHaveClass(new RegExp(timerStateClass));
|
||||
}
|
||||
111
e2e/tests/ui/layout/search/grandsearch.e2e.spec.js
Normal file
@@ -0,0 +1,111 @@
|
||||
/*****************************************************************************
|
||||
* 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 search functionality.
|
||||
*/
|
||||
|
||||
const { expect } = require('@playwright/test');
|
||||
const { test } = require('../../../../fixtures');
|
||||
|
||||
/**
|
||||
* Creates a notebook object and adds an entry.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function createClockAndDisplayLayout(page) {
|
||||
//Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
// Click button:has-text("Create")
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
// Click li:has-text("Notebook")
|
||||
await page.locator('li:has-text("Clock")').click();
|
||||
// Click button:has-text("OK")
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('button:has-text("OK")').click()
|
||||
]);
|
||||
|
||||
// Click a:has-text("My Items")
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('a:has-text("My Items") >> 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()
|
||||
]);
|
||||
}
|
||||
|
||||
test.describe('Grand Search', () => {
|
||||
test('Can search for objects, and subsequent search dropdown behaves properly', async ({ page }) => {
|
||||
await createClockAndDisplayLayout(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('Cl');
|
||||
await expect(page.locator('[aria-label="Search Result"]')).toContainText('Clock');
|
||||
// Click text=Elements >> nth=0
|
||||
await page.locator('text=Elements').first().click();
|
||||
await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible();
|
||||
|
||||
// Click [aria-label="OpenMCT Search"] [aria-label="Search Input"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click();
|
||||
// Click [aria-label="Unnamed Clock clock result"] >> text=Unnamed Clock
|
||||
await page.locator('[aria-label="Unnamed Clock clock result"] >> text=Unnamed Clock').click();
|
||||
await expect(page.locator('.js-preview-window')).toBeVisible();
|
||||
|
||||
// Click [aria-label="Close"]
|
||||
await page.locator('[aria-label="Close"]').click();
|
||||
await expect(page.locator('[aria-label="Search Result"]')).toBeVisible();
|
||||
await expect(page.locator('[aria-label="Search Result"]')).toContainText('Cloc');
|
||||
|
||||
// Click [aria-label="OpenMCT Search"] a >> nth=0
|
||||
await page.locator('[aria-label="OpenMCT Search"] a').first().click();
|
||||
await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible();
|
||||
|
||||
// Fill [aria-label="OpenMCT Search"] input[type="search"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('foo');
|
||||
await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible();
|
||||
|
||||
// Click text=Snapshot Save and Finish Editing Save and Continue Editing >> button >> nth=1
|
||||
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
|
||||
// Click text=Save and Finish Editing
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
// Click [aria-label="OpenMCT Search"] [aria-label="Search Input"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click();
|
||||
// Fill [aria-label="OpenMCT Search"] [aria-label="Search Input"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').fill('Cl');
|
||||
// Click text=Unnamed Clock
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=Unnamed Clock').click()
|
||||
]);
|
||||
await expect(page.locator('.is-object-type-clock')).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -32,39 +32,31 @@ Note: Larger testsuite sizes are OK due to the setup time associated with these
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { test, expect } = require('../../baseFixtures.js');
|
||||
const { test, expect } = require('../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults } = require('../../appActions');
|
||||
const percySnapshot = require('@percy/playwright');
|
||||
const path = require('path');
|
||||
|
||||
const VISUAL_GRACE_PERIOD = 5 * 1000; //Lets the application "simmer" before the snapshot is taken
|
||||
|
||||
const CUSTOM_NAME = 'CUSTOM_NAME';
|
||||
|
||||
test.describe('Visual - addInit', () => {
|
||||
test.use({
|
||||
clockOptions: {
|
||||
shouldAdvanceTime: true
|
||||
now: 0, //Set browser clock to UNIX Epoch
|
||||
shouldAdvanceTime: false //Don't advance the clock
|
||||
}
|
||||
});
|
||||
|
||||
test('Restricted Notebook is visually correct @addInit', async ({ page }) => {
|
||||
test('Restricted Notebook is visually correct @addInit @unstable', async ({ page, theme }) => {
|
||||
// eslint-disable-next-line no-undef
|
||||
await page.addInitScript({ path: path.join(__dirname, '../../helper', './addInitRestrictedNotebook.js') });
|
||||
//Go to baseURL
|
||||
await page.goto('./#/browse/mine?hideTree=true', { waitUntil: 'networkidle' });
|
||||
//Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
// Click text=CUSTOM_NAME
|
||||
await page.click(`text=${CUSTOM_NAME}`);
|
||||
// Click text=OK
|
||||
await Promise.all([
|
||||
page.waitForNavigation({waitUntil: 'networkidle'}),
|
||||
page.click('text=OK')
|
||||
]);
|
||||
|
||||
await createDomainObjectWithDefaults(page, { type: CUSTOM_NAME });
|
||||
|
||||
// Take a snapshot of the newly created CUSTOM_NAME notebook
|
||||
await page.waitForTimeout(VISUAL_GRACE_PERIOD);
|
||||
await percySnapshot(page, 'Restricted Notebook with CUSTOM_NAME');
|
||||
await percySnapshot(page, `Restricted Notebook with CUSTOM_NAME (theme: '${theme}')`);
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
101
e2e/tests/visual/components/tree.visual.spec.js
Normal file
@@ -0,0 +1,101 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
const { test } = require('../../../pluginFixtures.js');
|
||||
const { createDomainObjectWithDefaults } = require('../../../appActions.js');
|
||||
|
||||
const percySnapshot = require('@percy/playwright');
|
||||
|
||||
test.describe('Visual - Tree Pane', () => {
|
||||
test('Tree pane in various states @unstable', async ({ page, theme, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
await page.goto('./#/browse/mine', { waitUntil: 'networkidle' });
|
||||
|
||||
const foo = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder',
|
||||
name: "Foo Folder"
|
||||
});
|
||||
|
||||
const bar = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder',
|
||||
name: "Bar Folder",
|
||||
parent: foo.uuid
|
||||
});
|
||||
|
||||
const baz = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder',
|
||||
name: "Baz Folder",
|
||||
parent: bar.uuid
|
||||
});
|
||||
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Clock',
|
||||
name: 'A Clock'
|
||||
});
|
||||
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Clock',
|
||||
name: 'Z Clock'
|
||||
});
|
||||
|
||||
const treePane = "#tree-pane";
|
||||
|
||||
await percySnapshot(page, `Tree Pane w/ collapsed tree (theme: ${theme})`, {
|
||||
scope: treePane
|
||||
});
|
||||
|
||||
await expandTreePaneItemByName(page, myItemsFolderName);
|
||||
|
||||
await page.goto(foo.url);
|
||||
await page.dragAndDrop('role=treeitem[name=/A Clock/]', '.c-object-view');
|
||||
await page.dragAndDrop('role=treeitem[name=/Z Clock/]', '.c-object-view');
|
||||
await page.goto(bar.url);
|
||||
await page.dragAndDrop('role=treeitem[name=/A Clock/]', '.c-object-view');
|
||||
await page.dragAndDrop('role=treeitem[name=/Z Clock/]', '.c-object-view');
|
||||
await page.goto(baz.url);
|
||||
await page.dragAndDrop('role=treeitem[name=/A Clock/]', '.c-object-view');
|
||||
await page.dragAndDrop('role=treeitem[name=/Z Clock/]', '.c-object-view');
|
||||
|
||||
await percySnapshot(page, `Tree Pane w/ single level expanded (theme: ${theme})`, {
|
||||
scope: treePane
|
||||
});
|
||||
|
||||
await expandTreePaneItemByName(page, foo.name);
|
||||
await expandTreePaneItemByName(page, bar.name);
|
||||
await expandTreePaneItemByName(page, baz.name);
|
||||
|
||||
await percySnapshot(page, `Tree Pane w/ multiple levels expanded (theme: ${theme})`, {
|
||||
scope: treePane
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} name
|
||||
*/
|
||||
async function expandTreePaneItemByName(page, name) {
|
||||
const treePane = page.locator('#tree-pane');
|
||||
const treeItem = treePane.locator(`role=treeitem[expanded=false][name=/${name}/]`);
|
||||
const expandTriangle = treeItem.locator('.c-disclosure-triangle');
|
||||
await expandTriangle.click();
|
||||
}
|
||||
@@ -25,27 +25,21 @@ Collection of Visual Tests set to run in a default context. The tests within thi
|
||||
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
|
||||
in a test and not for functional verification of correctness. Visual tests are not supposed
|
||||
to "fail" on assertions. Instead, they should be used to detect changes between builds or branches.
|
||||
|
||||
Note: Larger testsuite sizes are OK due to the setup time associated with these tests.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../baseFixtures.js');
|
||||
const { test, expect } = require('../../pluginFixtures');
|
||||
const percySnapshot = require('@percy/playwright');
|
||||
|
||||
test.describe('Visual - Controlled Clock', () => {
|
||||
test.describe('Visual - Controlled Clock @localStorage', () => {
|
||||
test.use({
|
||||
storageState: './e2e/test-data/VisualTestData_storage.json',
|
||||
clockOptions: {
|
||||
now: 0, //Set browser clock to UNIX Epoch
|
||||
shouldAdvanceTime: false, //Don't advance the clock
|
||||
toFake: ["setTimeout", "nextTick"]
|
||||
shouldAdvanceTime: false //Don't advance the clock
|
||||
}
|
||||
});
|
||||
|
||||
test('Overlay Plot Loading Indicator @localstorage', async ({ page }) => {
|
||||
test('Overlay Plot Loading Indicator @localStorage', async ({ page, theme }) => {
|
||||
// Go to baseURL
|
||||
await page.goto('./#/browse/mine?hideTree=true', { waitUntil: 'networkidle' });
|
||||
|
||||
@@ -57,6 +51,6 @@ test.describe('Visual - Controlled Clock', () => {
|
||||
await page.locator('canvas >> nth=1').hover({trial: true});
|
||||
|
||||
//Take snapshot of Sine Wave Generator within Overlay Plot
|
||||
await percySnapshot(page, 'SineWaveInOverlayPlot');
|
||||
await percySnapshot(page, `SineWaveInOverlayPlot (theme: '${theme}')`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,85 +32,62 @@ to "fail" on assertions. Instead, they should be used to detect changes between
|
||||
Note: Larger testsuite sizes are OK due to the setup time associated with these tests.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../baseFixtures.js');
|
||||
const { test, expect } = require('../../pluginFixtures');
|
||||
const percySnapshot = require('@percy/playwright');
|
||||
const VISUAL_GRACE_PERIOD = 5 * 1000; //Lets the application "simmer" before the snapshot is taken
|
||||
const { createDomainObjectWithDefaults } = require('../../appActions');
|
||||
|
||||
test.describe('Visual - Default', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
//Go to baseURL and Hide Tree
|
||||
await page.goto('./#/browse/mine?hideTree=true', { waitUntil: 'networkidle' });
|
||||
});
|
||||
test.use({
|
||||
clockOptions: {
|
||||
now: 0,
|
||||
shouldAdvanceTime: true
|
||||
now: 0, //Set browser clock to UNIX Epoch
|
||||
shouldAdvanceTime: false //Don't advance the clock
|
||||
}
|
||||
});
|
||||
|
||||
test('Visual - Root and About', async ({ page }) => {
|
||||
// Go to baseURL
|
||||
await page.goto('./#/browse/mine?hideTree=true', { waitUntil: 'networkidle' });
|
||||
|
||||
test('Visual - Root and About', async ({ page, theme }) => {
|
||||
// Verify that Create button is actionable
|
||||
await expect(page.locator('button:has-text("Create")')).toBeEnabled();
|
||||
|
||||
// Take a snapshot of the Dashboard
|
||||
await page.waitForTimeout(VISUAL_GRACE_PERIOD);
|
||||
await percySnapshot(page, 'Root');
|
||||
await percySnapshot(page, `Root (theme: '${theme}')`);
|
||||
|
||||
// Click About button
|
||||
await page.click('.l-shell__app-logo');
|
||||
|
||||
// Modify the Build information in 'about' to be consistent run-over-run
|
||||
const versionInformationLocator = page.locator('ul.t-info.l-info.s-info');
|
||||
const versionInformationLocator = page.locator('ul.t-info.l-info.s-info').first();
|
||||
await expect(versionInformationLocator).toBeEnabled();
|
||||
await versionInformationLocator.evaluate(node => node.innerHTML = '<li>Version: visual-snapshot</li> <li>Build Date: Mon Nov 15 2021 08:07:51 GMT-0800 (Pacific Standard Time)</li> <li>Revision: 93049cdbc6c047697ca204893db9603b864b8c9f</li> <li>Branch: master</li>');
|
||||
|
||||
// Take a snapshot of the About modal
|
||||
await page.waitForTimeout(VISUAL_GRACE_PERIOD);
|
||||
await percySnapshot(page, 'About');
|
||||
await percySnapshot(page, `About (theme: '${theme}')`);
|
||||
});
|
||||
|
||||
test('Visual - Default Condition Set', async ({ page }) => {
|
||||
//Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
test('Visual - Default Condition Set @unstable', async ({ page, theme }) => {
|
||||
|
||||
//Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
// Click text=Condition Set
|
||||
await page.click('text=Condition Set');
|
||||
|
||||
// Click text=OK
|
||||
await page.click('text=OK');
|
||||
await createDomainObjectWithDefaults(page, { type: 'Condition Set' });
|
||||
|
||||
// Take a snapshot of the newly created Condition Set object
|
||||
await page.waitForTimeout(VISUAL_GRACE_PERIOD);
|
||||
await percySnapshot(page, 'Default Condition Set');
|
||||
await percySnapshot(page, `Default Condition Set (theme: '${theme}')`);
|
||||
});
|
||||
|
||||
test.fixme('Visual - Default Condition Widget', async ({ page }) => {
|
||||
test('Visual - Default Condition Widget @unstable', async ({ page, theme }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/5349'
|
||||
});
|
||||
//Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
//Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
// Click text=Condition Widget
|
||||
await page.click('text=Condition Widget');
|
||||
|
||||
// Click text=OK
|
||||
await page.click('text=OK');
|
||||
await createDomainObjectWithDefaults(page, { type: 'Condition Widget' });
|
||||
|
||||
// Take a snapshot of the newly created Condition Widget object
|
||||
await page.waitForTimeout(VISUAL_GRACE_PERIOD);
|
||||
await percySnapshot(page, 'Default Condition Widget');
|
||||
await percySnapshot(page, `Default Condition Widget (theme: '${theme}')`);
|
||||
});
|
||||
|
||||
test('Visual - Time Conductor start time is less than end time', async ({ page }) => {
|
||||
//Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
test('Visual - Time Conductor start time is less than end time', async ({ page, theme }) => {
|
||||
const year = new Date().getFullYear();
|
||||
|
||||
let startDate = 'xxxx-01-01 01:00:00.000Z';
|
||||
@@ -123,16 +100,14 @@ test.describe('Visual - Default', () => {
|
||||
await page.locator('input[type="text"]').first().fill(startDate.toString());
|
||||
|
||||
// verify no error msg
|
||||
await page.waitForTimeout(VISUAL_GRACE_PERIOD);
|
||||
await percySnapshot(page, 'Default Time conductor');
|
||||
await percySnapshot(page, `Default Time conductor (theme: '${theme}')`);
|
||||
|
||||
startDate = (year + 1) + startDate.substring(4);
|
||||
await page.locator('input[type="text"]').first().fill(startDate.toString());
|
||||
await page.locator('input[type="text"]').nth(1).click();
|
||||
|
||||
// verify error msg for start time (unable to capture snapshot of popup)
|
||||
await page.waitForTimeout(VISUAL_GRACE_PERIOD);
|
||||
await percySnapshot(page, 'Start time error');
|
||||
await percySnapshot(page, `Start time error (theme: '${theme}')`);
|
||||
|
||||
startDate = (year - 1) + startDate.substring(4);
|
||||
await page.locator('input[type="text"]').first().fill(startDate.toString());
|
||||
@@ -143,79 +118,51 @@ test.describe('Visual - Default', () => {
|
||||
await page.locator('input[type="text"]').first().click();
|
||||
|
||||
// verify error msg for end time (unable to capture snapshot of popup)
|
||||
await page.waitForTimeout(VISUAL_GRACE_PERIOD);
|
||||
await percySnapshot(page, 'End time error');
|
||||
await percySnapshot(page, `End time error (theme: '${theme}')`);
|
||||
});
|
||||
|
||||
test('Visual - Sine Wave Generator Form', async ({ page }) => {
|
||||
//Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
test('Visual - Sine Wave Generator Form', async ({ page, theme }) => {
|
||||
//Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
// Click text=Sine Wave Generator
|
||||
await page.click('text=Sine Wave Generator');
|
||||
|
||||
await page.waitForTimeout(VISUAL_GRACE_PERIOD);
|
||||
await percySnapshot(page, 'Default Sine Wave Generator Form');
|
||||
await percySnapshot(page, `Default Sine Wave Generator Form (theme: '${theme}')`);
|
||||
|
||||
await page.locator('.field.control.l-input-sm input').first().click();
|
||||
await page.locator('.field.control.l-input-sm input').first().fill('');
|
||||
|
||||
// Validate red x mark
|
||||
await page.waitForTimeout(VISUAL_GRACE_PERIOD);
|
||||
await percySnapshot(page, 'removed amplitude property value');
|
||||
await percySnapshot(page, `removed amplitude property value (theme: '${theme}')`);
|
||||
});
|
||||
|
||||
test('Visual - Save Successful Banner', async ({ page }) => {
|
||||
//Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
test('Visual - Save Successful Banner @unstable', async ({ page, theme }) => {
|
||||
await createDomainObjectWithDefaults(page, { type: 'Timer' });
|
||||
|
||||
//Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
//NOTE Something other than example imagery
|
||||
await page.click('text=Timer');
|
||||
|
||||
// Click text=OK
|
||||
await page.click('text=OK');
|
||||
await page.locator('.c-message-banner__message').hover({ trial: true });
|
||||
await percySnapshot(page, 'Banner message shown');
|
||||
await percySnapshot(page, `Banner message shown (theme: '${theme}')`);
|
||||
|
||||
//Wait until Save Banner is gone
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
||||
await percySnapshot(page, 'Banner message gone');
|
||||
await percySnapshot(page, `Banner message gone (theme: '${theme}')`);
|
||||
});
|
||||
|
||||
test('Visual - Display Layout Icon is correct', async ({ page }) => {
|
||||
//Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
test('Visual - Display Layout Icon is correct', async ({ page, theme }) => {
|
||||
//Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
//Hover on Display Layout option.
|
||||
await page.locator('text=Display Layout').hover();
|
||||
await percySnapshot(page, 'Display Layout Create Menu');
|
||||
await percySnapshot(page, `Display Layout Create Menu (theme: '${theme}')`);
|
||||
|
||||
});
|
||||
|
||||
test('Visual - Default Gauge is correct', async ({ page }) => {
|
||||
|
||||
//Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
//Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
await page.click('text=Gauge');
|
||||
|
||||
await page.click('text=OK');
|
||||
test('Visual - Default Gauge is correct @unstable', async ({ page, theme }) => {
|
||||
await createDomainObjectWithDefaults(page, { type: 'Gauge' });
|
||||
|
||||
// Take a snapshot of the newly created Gauge object
|
||||
await page.waitForTimeout(VISUAL_GRACE_PERIOD);
|
||||
await percySnapshot(page, 'Default Gauge');
|
||||
|
||||
await percySnapshot(page, `Default Gauge (theme: '${theme}')`);
|
||||
});
|
||||
});
|
||||
|
||||
78
e2e/tests/visual/faultManagement.visual.spec.js
Normal file
@@ -0,0 +1,78 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
const path = require('path');
|
||||
const { test } = require('../../pluginFixtures');
|
||||
const percySnapshot = require('@percy/playwright');
|
||||
|
||||
const utils = require('../../helper/faultUtils');
|
||||
|
||||
test.describe('The Fault Management Plugin Visual Test', () => {
|
||||
|
||||
test('icon test', async ({ page, theme }) => {
|
||||
// eslint-disable-next-line no-undef
|
||||
await page.addInitScript({ path: path.join(__dirname, '../../helper/', 'addInitFaultManagementPlugin.js') });
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
await percySnapshot(page, `Fault Management icon appears in tree (theme: '${theme}')`);
|
||||
});
|
||||
|
||||
test('fault list and acknowledged faults', async ({ page, theme }) => {
|
||||
await utils.navigateToFaultManagementWithStaticExample(page);
|
||||
|
||||
await percySnapshot(page, `Shows a list of faults in the standard view (theme: '${theme}')`);
|
||||
|
||||
await utils.acknowledgeFault(page, 1);
|
||||
await utils.changeViewTo(page, 'acknowledged');
|
||||
|
||||
await percySnapshot(page, `Acknowledged faults, have a checkmark on the fault icon and appear in the acknowldeged view (theme: '${theme}')`);
|
||||
});
|
||||
|
||||
test('shelved faults', async ({ page, theme }) => {
|
||||
await utils.navigateToFaultManagementWithStaticExample(page);
|
||||
|
||||
await utils.shelveFault(page, 1);
|
||||
await utils.changeViewTo(page, 'shelved');
|
||||
|
||||
await percySnapshot(page, `Shelved faults appear in the shelved view (theme: '${theme}')`);
|
||||
|
||||
await utils.openFaultRowMenu(page, 1);
|
||||
|
||||
await percySnapshot(page, `Shelved faults have a 3-dot menu with Unshelve option enabled (theme: '${theme}')`);
|
||||
});
|
||||
|
||||
test('3-dot menu for fault', async ({ page, theme }) => {
|
||||
await utils.navigateToFaultManagementWithStaticExample(page);
|
||||
|
||||
await utils.openFaultRowMenu(page, 1);
|
||||
|
||||
await percySnapshot(page, `Faults have a 3-dot menu with Acknowledge, Shelve and Unshelve (Unshelve is disabled) options (theme: '${theme}')`);
|
||||
});
|
||||
|
||||
test('ability to acknowledge or shelve', async ({ page, theme }) => {
|
||||
await utils.navigateToFaultManagementWithStaticExample(page);
|
||||
|
||||
await utils.selectFaultItem(page, 1);
|
||||
|
||||
await percySnapshot(page, `Selected faults highlight the ability to Acknowledge or Shelve above the fault list (theme: '${theme}')`);
|
||||
});
|
||||
});
|
||||
95
e2e/tests/visual/generateVisualTestData.e2e.spec.js
Normal file
@@ -0,0 +1,95 @@
|
||||
/*****************************************************************************
|
||||
* 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 generating LocalStorage via Session Storage to be used
|
||||
in some visual test suites like controlledClock.visual.spec.js. This suite should run to completion
|
||||
and generate an artifact named ./e2e/test-data/VisualTestData_storage.json . This will run
|
||||
on every Commit to ensure that this object still loads into tests correctly and will retain the
|
||||
.e2e.spec.js suffix.
|
||||
|
||||
TODO: Provide additional validation of object properties as it grows.
|
||||
|
||||
*/
|
||||
|
||||
const { test } = require('../../fixtures.js');
|
||||
const { expect } = require('@playwright/test');
|
||||
|
||||
test('Generate Visual Test Data @localStorage', async ({ page, context }) => {
|
||||
//Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
|
||||
// add overlay plot with defaults
|
||||
await page.locator('li:has-text("Overlay Plot")').click();
|
||||
|
||||
// Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184
|
||||
await page.click('form[name="mctForm"] a:has-text("My Items")');
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=OK').click(),
|
||||
//Wait for Save Banner to appear1
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
//Wait until Save Banner is gone
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
||||
|
||||
// save (exit edit mode)
|
||||
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
// click create button
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
|
||||
// add sine wave generator with defaults
|
||||
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');
|
||||
|
||||
// Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184
|
||||
await page.click('form[name="mctForm"] a:has-text("Overlay Plot")');
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=OK').click(),
|
||||
//Wait for Save Banner to appear1
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
//Wait until Save Banner is gone
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
||||
|
||||
// focus the overlay plot
|
||||
await page.locator('text=Open MCT My Items >> span').nth(3).click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=Unnamed Overlay Plot').first().click()
|
||||
]);
|
||||
|
||||
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' });
|
||||
});
|
||||
@@ -24,81 +24,61 @@
|
||||
This test suite is dedicated to tests which verify search functionality.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../baseFixtures.js');
|
||||
const { test, expect } = require('../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults } = require('../../appActions');
|
||||
|
||||
const percySnapshot = require('@percy/playwright');
|
||||
|
||||
/**
|
||||
* Creates a notebook object and adds an entry.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function createClockAndDisplayLayout(page) {
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
// Click button:has-text("Create")
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
// Click li:has-text("Notebook")
|
||||
await page.locator('li:has-text("Clock")').click();
|
||||
// Click button:has-text("OK")
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('button:has-text("OK")').click()
|
||||
]);
|
||||
|
||||
// Click a:has-text("My Items")
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('a:has-text("My Items") >> 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()
|
||||
]);
|
||||
}
|
||||
|
||||
test.describe('Grand Search', () => {
|
||||
test('Can search for objects, and subsequent search dropdown behaves properly', async ({ page }) => {
|
||||
await createClockAndDisplayLayout(page);
|
||||
test.beforeEach(async ({ page, theme }) => {
|
||||
//Go to baseURL and Hide Tree
|
||||
await page.goto('./#/browse/mine?hideTree=true', { waitUntil: 'networkidle' });
|
||||
});
|
||||
test.use({
|
||||
clockOptions: {
|
||||
now: 0, //Set browser clock to UNIX Epoch
|
||||
shouldAdvanceTime: false //Don't advance the clock
|
||||
}
|
||||
});
|
||||
//This needs to be rewritten to use a non clock or non display layout object
|
||||
test('Can search for objects, and subsequent search dropdown behaves properly @unstable', async ({ page, theme }) => {
|
||||
// await createDomainObjectWithDefaults(page, 'Display Layout');
|
||||
// await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
|
||||
// await page.locator('text=Save and Finish Editing').click();
|
||||
const folder1 = 'Folder1';
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder',
|
||||
name: folder1
|
||||
});
|
||||
|
||||
// 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('Cl');
|
||||
await expect(page.locator('[aria-label="Search Result"]')).toContainText('Clock');
|
||||
await percySnapshot(page, 'Searching for Clocks');
|
||||
// Click text=Elements >> nth=0
|
||||
await page.locator('text=Elements').first().click();
|
||||
await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible();
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill(folder1);
|
||||
await expect(page.locator('[aria-label="Search Result"]')).toContainText(folder1);
|
||||
await percySnapshot(page, 'Searching for Folder Object');
|
||||
|
||||
// Click [aria-label="OpenMCT Search"] [aria-label="Search Input"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click();
|
||||
// Click [aria-label="Unnamed Clock clock result"] >> text=Unnamed Clock
|
||||
await page.locator('[aria-label="Unnamed Clock clock result"] >> text=Unnamed Clock').click();
|
||||
await percySnapshot(page, 'Preview for clock should display when editing enabled and search item clicked');
|
||||
|
||||
// Click [aria-label="Close"]
|
||||
await page.locator('[aria-label="Close"]').click();
|
||||
await percySnapshot(page, 'Search should still be showing after preview closed');
|
||||
|
||||
// Click text=Snapshot Save and Finish Editing Save and Continue Editing >> button >> nth=1
|
||||
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
|
||||
// Click text=Save and Finish Editing
|
||||
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
// Click [aria-label="OpenMCT Search"] [aria-label="Search Input"]
|
||||
|
||||
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click();
|
||||
// Fill [aria-label="OpenMCT Search"] [aria-label="Search Input"]
|
||||
|
||||
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').fill('Cl');
|
||||
// Click text=Unnamed Clock
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=Unnamed Clock').click()
|
||||
]);
|
||||
await percySnapshot(page, 'Clicking on search results should navigate to them if not editing');
|
||||
await percySnapshot(page, `Clicking on search results should navigate to them if not editing (theme: '${theme}')`);
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -20,59 +20,36 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
export default function () {
|
||||
import utils from './utils';
|
||||
|
||||
export default function (staticFaults = false) {
|
||||
return function install(openmct) {
|
||||
openmct.install(openmct.plugins.FaultManagement());
|
||||
|
||||
const faultsData = utils.randomFaults(staticFaults);
|
||||
|
||||
openmct.faults.addProvider({
|
||||
request(domainObject, options) {
|
||||
const faults = JSON.parse(localStorage.getItem('faults'));
|
||||
|
||||
return Promise.resolve(faults.alarms);
|
||||
return Promise.resolve(faultsData);
|
||||
},
|
||||
subscribe(domainObject, callback) {
|
||||
const faultsData = JSON.parse(localStorage.getItem('faults')).alarms;
|
||||
|
||||
function getRandomIndex(start, end) {
|
||||
return Math.floor(start + (Math.random() * (end - start + 1)));
|
||||
}
|
||||
|
||||
let id = setInterval(() => {
|
||||
const index = getRandomIndex(0, faultsData.length - 1);
|
||||
const randomFaultData = faultsData[index];
|
||||
const randomFault = randomFaultData.fault;
|
||||
randomFault.currentValueInfo.value = Math.random();
|
||||
callback({
|
||||
fault: randomFault,
|
||||
type: 'alarms'
|
||||
});
|
||||
}, 300);
|
||||
|
||||
return () => {
|
||||
clearInterval(id);
|
||||
};
|
||||
return () => {};
|
||||
},
|
||||
supportsRequest(domainObject) {
|
||||
const faults = localStorage.getItem('faults');
|
||||
|
||||
return faults && domainObject.type === 'faultManagement';
|
||||
return domainObject.type === 'faultManagement';
|
||||
},
|
||||
supportsSubscribe(domainObject) {
|
||||
const faults = localStorage.getItem('faults');
|
||||
|
||||
return faults && domainObject.type === 'faultManagement';
|
||||
return domainObject.type === 'faultManagement';
|
||||
},
|
||||
acknowledgeFault(fault, { comment = '' }) {
|
||||
console.log('acknowledgeFault', fault);
|
||||
console.log('comment', comment);
|
||||
utils.acknowledgeFault(fault);
|
||||
|
||||
return Promise.resolve({
|
||||
success: true
|
||||
});
|
||||
},
|
||||
shelveFault(fault, shelveData) {
|
||||
console.log('shelveFault', fault);
|
||||
console.log('shelveData', shelveData);
|
||||
shelveFault(fault, duration) {
|
||||
utils.shelveFault(fault, duration);
|
||||
|
||||
return Promise.resolve({
|
||||
success: true
|
||||
76
example/faultManagement/utils.js
Normal file
@@ -0,0 +1,76 @@
|
||||
const SEVERITIES = ['WATCH', 'WARNING', 'CRITICAL'];
|
||||
const NAMESPACE = '/Example/fault-';
|
||||
const getRandom = {
|
||||
severity: () => SEVERITIES[Math.floor(Math.random() * 3)],
|
||||
value: () => Math.random() + Math.floor(Math.random() * 21) - 10,
|
||||
fault: (num, staticFaults) => {
|
||||
let val = getRandom.value();
|
||||
let severity = getRandom.severity();
|
||||
let time = Date.now() - num;
|
||||
|
||||
if (staticFaults) {
|
||||
let severityIndex = num > 3 ? num % 3 : num;
|
||||
|
||||
val = num;
|
||||
severity = SEVERITIES[severityIndex - 1];
|
||||
time = num;
|
||||
}
|
||||
|
||||
return {
|
||||
type: num,
|
||||
fault: {
|
||||
acknowledged: false,
|
||||
currentValueInfo: {
|
||||
value: val,
|
||||
rangeCondition: severity,
|
||||
monitoringResult: severity
|
||||
},
|
||||
id: `id-${num}`,
|
||||
name: `Example Fault ${num}`,
|
||||
namespace: NAMESPACE + num,
|
||||
seqNum: 0,
|
||||
severity: severity,
|
||||
shelved: false,
|
||||
shortDescription: '',
|
||||
triggerTime: time,
|
||||
triggerValueInfo: {
|
||||
value: val,
|
||||
rangeCondition: severity,
|
||||
monitoringResult: severity
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
function shelveFault(fault, opts = {
|
||||
shelved: true,
|
||||
comment: '',
|
||||
shelveDuration: 90000
|
||||
}) {
|
||||
fault.shelved = true;
|
||||
|
||||
setTimeout(() => {
|
||||
fault.shelved = false;
|
||||
}, opts.shelveDuration);
|
||||
}
|
||||
|
||||
function acknowledgeFault(fault) {
|
||||
fault.acknowledged = true;
|
||||
}
|
||||
|
||||
function randomFaults(staticFaults, count = 5) {
|
||||
let faults = [];
|
||||
|
||||
for (let x = 1, y = count + 1; x < y; x++) {
|
||||
faults.push(getRandom.fault(x, staticFaults));
|
||||
}
|
||||
|
||||
return faults;
|
||||
}
|
||||
|
||||
export default {
|
||||
randomFaults,
|
||||
shelveFault,
|
||||
acknowledgeFault
|
||||
};
|
||||
35
package.json
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "openmct",
|
||||
"version": "2.1.0-SNAPSHOT",
|
||||
"version": "2.0.8",
|
||||
"description": "The Open MCT core platform",
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "7.18.2",
|
||||
"@babel/eslint-parser": "7.18.9",
|
||||
"@braintree/sanitize-url": "6.0.0",
|
||||
"@percy/cli": "1.2.1",
|
||||
"@percy/cli": "1.10.0",
|
||||
"@percy/playwright": "1.0.4",
|
||||
"@playwright/test": "1.23.0",
|
||||
"@types/eventemitter3": "^1.0.0",
|
||||
@@ -23,19 +23,18 @@
|
||||
"d3-axis": "3.0.0",
|
||||
"d3-scale": "3.3.0",
|
||||
"d3-selection": "3.0.0",
|
||||
"eslint": "8.18.0",
|
||||
"eslint": "8.22.0",
|
||||
"eslint-plugin-compat": "4.0.2",
|
||||
"eslint-plugin-playwright": "0.10.0",
|
||||
"eslint-plugin-vue": "9.1.1",
|
||||
"eslint-plugin-playwright": "0.11.1",
|
||||
"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": "0.8.0",
|
||||
"jasmine-core": "4.2.0",
|
||||
"jsdoc": "3.6.11",
|
||||
"imports-loader": "4.0.1",
|
||||
"jasmine-core": "4.3.0",
|
||||
"karma": "6.3.20",
|
||||
"karma-chrome-launcher": "3.1.1",
|
||||
"karma-cli": "2.0.0",
|
||||
@@ -46,22 +45,20 @@
|
||||
"karma-sourcemap-loader": "0.3.8",
|
||||
"karma-spec-reporter": "0.0.34",
|
||||
"karma-webpack": "5.0.0",
|
||||
"lighthouse": "9.6.1",
|
||||
"location-bar": "3.0.1",
|
||||
"lodash": "4.17.21",
|
||||
"mini-css-extract-plugin": "2.6.1",
|
||||
"moment": "2.29.4",
|
||||
"moment-duration-format": "2.3.2",
|
||||
"moment-timezone": "0.5.34",
|
||||
"node-bourbon": "4.2.3",
|
||||
"moment-timezone": "0.5.37",
|
||||
"nyc":"15.1.0",
|
||||
"painterro": "1.2.78",
|
||||
"plotly.js-basic-dist": "2.12.0",
|
||||
"plotly.js-gl2d-dist": "2.12.0",
|
||||
"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.52.2",
|
||||
"sass": "1.54.4",
|
||||
"sass-loader": "13.0.2",
|
||||
"sinon": "14.0.0",
|
||||
"style-loader": "^1.0.1",
|
||||
@@ -90,19 +87,17 @@
|
||||
"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:stable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep-invert @unstable",
|
||||
"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\"",
|
||||
"test:e2e:unstable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @unstable",
|
||||
"test:e2e:local": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome",
|
||||
"test:e2e:updatesnapshots": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @snapshot --update-snapshots",
|
||||
"test:e2e:visual": "percy exec --config ./e2e/.percy.yml -- npx playwright test --config=e2e/playwright-visual.config.js",
|
||||
"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",
|
||||
"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",
|
||||
"jsdoc": "jsdoc -c jsdoc.json -R API.md -r -d dist/docs/api",
|
||||
"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'",
|
||||
"otherdoc": "node docs/gendocs.js --in docs/src --out dist/docs --suppress-toc 'docs/src/index.md|docs/src/process/index.md'",
|
||||
"docs": "npm run jsdoc ; npm run otherdoc",
|
||||
"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",
|
||||
|
||||
@@ -40,6 +40,8 @@ const ANNOTATION_TYPES = Object.freeze({
|
||||
PLOT_SPATIAL: 'PLOT_SPATIAL'
|
||||
});
|
||||
|
||||
const ANNOTATION_TYPE = 'annotation';
|
||||
|
||||
/**
|
||||
* @typedef {Object} Tag
|
||||
* @property {String} key a unique identifier for the tag
|
||||
@@ -54,7 +56,7 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
|
||||
this.ANNOTATION_TYPES = ANNOTATION_TYPES;
|
||||
|
||||
this.openmct.types.addType('annotation', {
|
||||
this.openmct.types.addType(ANNOTATION_TYPE, {
|
||||
name: 'Annotation',
|
||||
description: 'A user created note or comment about time ranges, pixel space, and geospatial features.',
|
||||
creatable: false,
|
||||
@@ -136,6 +138,10 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
this.availableTags[tagKey] = tagsDefinition;
|
||||
}
|
||||
|
||||
isAnnotation(domainObject) {
|
||||
return domainObject && (domainObject.type === ANNOTATION_TYPE);
|
||||
}
|
||||
|
||||
getAvailableTags() {
|
||||
if (this.availableTags) {
|
||||
const rearrangedToArray = Object.keys(this.availableTags).map(tagKey => {
|
||||
@@ -271,7 +277,10 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
const searchResults = (await Promise.all(this.openmct.objects.search(matchingTagKeys, abortController, this.openmct.objects.SEARCH_TYPES.TAGS))).flat();
|
||||
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);
|
||||
});
|
||||
|
||||
return appliedTargetsModels;
|
||||
return resultsWithValidPath;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,15 +27,26 @@ describe("The Annotation API", () => {
|
||||
let openmct;
|
||||
let mockObjectProvider;
|
||||
let mockDomainObject;
|
||||
let mockFolderObject;
|
||||
let mockAnnotationObject;
|
||||
|
||||
beforeEach((done) => {
|
||||
openmct = createOpenMct();
|
||||
openmct.install(new ExampleTagsPlugin());
|
||||
const availableTags = openmct.annotation.getAvailableTags();
|
||||
mockFolderObject = {
|
||||
type: 'root',
|
||||
name: 'folderFoo',
|
||||
location: '',
|
||||
identifier: {
|
||||
key: 'someParent',
|
||||
namespace: 'fooNameSpace'
|
||||
}
|
||||
};
|
||||
mockDomainObject = {
|
||||
type: 'notebook',
|
||||
name: 'fooRabbitNotebook',
|
||||
location: 'fooNameSpace:someParent',
|
||||
identifier: {
|
||||
key: 'some-object',
|
||||
namespace: 'fooNameSpace'
|
||||
@@ -68,6 +79,8 @@ describe("The Annotation API", () => {
|
||||
return mockDomainObject;
|
||||
} else if (identifier.key === mockAnnotationObject.identifier.key) {
|
||||
return mockAnnotationObject;
|
||||
} else if (identifier.key === mockFolderObject.identifier.key) {
|
||||
return mockFolderObject;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
@@ -150,6 +163,7 @@ describe("The Annotation API", () => {
|
||||
// use local worker
|
||||
sharedWorkerToRestore = openmct.objects.inMemorySearchProvider.worker;
|
||||
openmct.objects.inMemorySearchProvider.worker = null;
|
||||
await openmct.objects.inMemorySearchProvider.index(mockFolderObject);
|
||||
await openmct.objects.inMemorySearchProvider.index(mockDomainObject);
|
||||
await openmct.objects.inMemorySearchProvider.index(mockAnnotationObject);
|
||||
});
|
||||
|
||||
@@ -25,6 +25,7 @@ import FormProperties from './components/FormProperties.vue';
|
||||
|
||||
import EventEmitter from 'EventEmitter';
|
||||
import Vue from 'vue';
|
||||
import _ from 'lodash';
|
||||
|
||||
export default class FormsAPI extends EventEmitter {
|
||||
constructor(openmct) {
|
||||
@@ -158,7 +159,8 @@ export default class FormsAPI extends EventEmitter {
|
||||
key = property.join('.');
|
||||
}
|
||||
|
||||
changes[key] = data.value;
|
||||
_.set(changes, `${key}`, data.value);
|
||||
self.emit('formPropertiesChanged', changes);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -63,6 +63,8 @@ class InMemorySearchProvider {
|
||||
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);
|
||||
this.onerror = this.onWorkerError.bind(this);
|
||||
this.startIndexing = this.startIndexing.bind(this);
|
||||
|
||||
@@ -75,6 +77,12 @@ class InMemorySearchProvider {
|
||||
this.worker.port.close();
|
||||
}
|
||||
|
||||
Object.keys(this.indexedCompositions).forEach(keyString => {
|
||||
const composition = this.indexedCompositions[keyString];
|
||||
composition.off('add', this.onCompositionAdded);
|
||||
composition.off('remove', this.onCompositionRemoved);
|
||||
});
|
||||
|
||||
this.destroyObservers(this.indexedIds);
|
||||
this.destroyObservers(this.indexedCompositions);
|
||||
});
|
||||
@@ -259,7 +267,6 @@ class InMemorySearchProvider {
|
||||
}
|
||||
|
||||
onAnnotationCreation(annotationObject) {
|
||||
|
||||
const objectProvider = this.openmct.objects.getProvider(annotationObject.identifier);
|
||||
if (objectProvider === undefined || objectProvider.search === undefined) {
|
||||
const provider = this;
|
||||
@@ -281,17 +288,34 @@ class InMemorySearchProvider {
|
||||
provider.index(domainObject);
|
||||
}
|
||||
|
||||
onCompositionMutation(domainObject, composition) {
|
||||
onCompositionAdded(newDomainObjectToIndex) {
|
||||
const provider = this;
|
||||
const indexedComposition = domainObject.composition;
|
||||
const identifiersToIndex = composition
|
||||
.filter(identifier => !indexedComposition
|
||||
.some(indexedIdentifier => this.openmct.objects
|
||||
.areIdsEqual([identifier, indexedIdentifier])));
|
||||
// The object comes in as a mutable domain object, which has functions,
|
||||
// which the index function cannot handle as it will eventually be serialized
|
||||
// using structuredClone. Thus we're using JSON.parse/JSON.stringify to discard
|
||||
// those functions.
|
||||
const nonMutableDomainObject = JSON.parse(JSON.stringify(newDomainObjectToIndex));
|
||||
|
||||
identifiersToIndex.forEach(identifier => {
|
||||
this.openmct.objects.get(identifier).then(objectToIndex => provider.index(objectToIndex));
|
||||
});
|
||||
const objectProvider = this.openmct.objects.getProvider(nonMutableDomainObject.identifier);
|
||||
if (objectProvider === undefined || objectProvider.search === undefined) {
|
||||
provider.index(nonMutableDomainObject);
|
||||
}
|
||||
}
|
||||
|
||||
onCompositionRemoved(domainObjectToRemoveIdentifier) {
|
||||
const keyString = this.openmct.objects.makeKeyString(domainObjectToRemoveIdentifier);
|
||||
if (this.indexedIds[keyString]) {
|
||||
// we store the unobserve function in the indexedId map
|
||||
this.indexedIds[keyString]();
|
||||
delete this.indexedIds[keyString];
|
||||
}
|
||||
|
||||
const composition = this.indexedCompositions[keyString];
|
||||
if (composition) {
|
||||
composition.off('add', this.onCompositionAdded);
|
||||
composition.off('remove', this.onCompositionRemoved);
|
||||
delete this.indexedCompositions[keyString];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -305,6 +329,7 @@ class InMemorySearchProvider {
|
||||
async index(domainObject) {
|
||||
const provider = this;
|
||||
const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
|
||||
const composition = this.openmct.composition.get(domainObject);
|
||||
|
||||
if (!this.indexedIds[keyString]) {
|
||||
this.indexedIds[keyString] = this.openmct.objects.observe(
|
||||
@@ -312,11 +337,12 @@ class InMemorySearchProvider {
|
||||
'name',
|
||||
this.onNameMutation.bind(this, domainObject)
|
||||
);
|
||||
this.indexedCompositions[keyString] = this.openmct.objects.observe(
|
||||
domainObject,
|
||||
'composition',
|
||||
this.onCompositionMutation.bind(this, domainObject)
|
||||
);
|
||||
if (composition) {
|
||||
composition.on('add', this.onCompositionAdded);
|
||||
composition.on('remove', this.onCompositionRemoved);
|
||||
this.indexedCompositions[keyString] = composition;
|
||||
}
|
||||
|
||||
if (domainObject.type === 'annotation') {
|
||||
this.indexedTags[keyString] = this.openmct.objects.observe(
|
||||
domainObject,
|
||||
@@ -338,8 +364,6 @@ class InMemorySearchProvider {
|
||||
}
|
||||
}
|
||||
|
||||
const composition = this.openmct.composition.get(domainObject);
|
||||
|
||||
if (composition !== undefined) {
|
||||
const children = await composition.load();
|
||||
|
||||
|
||||
@@ -34,11 +34,11 @@ import InMemorySearchProvider from './InMemorySearchProvider';
|
||||
* Uniquely identifies a domain object.
|
||||
*
|
||||
* @typedef Identifier
|
||||
* @memberof module:openmct.ObjectAPI~
|
||||
* @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
|
||||
* within that namespace
|
||||
* @memberof module:openmct.ObjectAPI~
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -88,7 +88,7 @@ export default class ObjectAPI {
|
||||
this.cache = {};
|
||||
this.interceptorRegistry = new InterceptorRegistry();
|
||||
|
||||
this.SYNCHRONIZED_OBJECT_TYPES = ['notebook', 'plan'];
|
||||
this.SYNCHRONIZED_OBJECT_TYPES = ['notebook', 'plan', 'annotation'];
|
||||
|
||||
this.errors = {
|
||||
Conflict: ConflictError
|
||||
@@ -233,11 +233,7 @@ export default class ObjectAPI {
|
||||
|
||||
delete this.cache[keystring];
|
||||
|
||||
if (!result) {
|
||||
//no result means resource either doesn't exist or is missing
|
||||
//otherwise it's an error, and we shouldn't apply interceptors
|
||||
result = this.applyGetInterceptors(identifier);
|
||||
}
|
||||
result = this.applyGetInterceptors(identifier);
|
||||
|
||||
return result;
|
||||
});
|
||||
@@ -387,7 +383,13 @@ export default class ObjectAPI {
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
return result.catch((error) => {
|
||||
if (error instanceof this.errors.Conflict) {
|
||||
this.openmct.notifications.error(`Conflict detected while saving ${this.makeKeyString(domainObject.identifier)}`);
|
||||
}
|
||||
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -608,27 +610,60 @@ export default class ObjectAPI {
|
||||
* @param {module:openmct.ObjectAPI~Identifier[]} identifiers
|
||||
*/
|
||||
areIdsEqual(...identifiers) {
|
||||
const firstIdentifier = utils.parseKeyString(identifiers[0]);
|
||||
|
||||
return identifiers.map(utils.parseKeyString)
|
||||
.every(identifier => {
|
||||
return identifier === identifiers[0]
|
||||
|| (identifier.namespace === identifiers[0].namespace
|
||||
&& identifier.key === identifiers[0].key);
|
||||
return identifier === firstIdentifier
|
||||
|| (identifier.namespace === firstIdentifier.namespace
|
||||
&& identifier.key === firstIdentifier.key);
|
||||
});
|
||||
}
|
||||
|
||||
getOriginalPath(identifier, path = []) {
|
||||
return this.get(identifier).then((domainObject) => {
|
||||
path.push(domainObject);
|
||||
let location = domainObject.location;
|
||||
/**
|
||||
* Given an original path check if the path is reachable via root
|
||||
* @param {Array<Object>} originalPath an array of path objects to check
|
||||
* @returns {boolean} whether the domain object is reachable
|
||||
*/
|
||||
isReachable(originalPath) {
|
||||
if (originalPath && originalPath.length) {
|
||||
return (originalPath[originalPath.length - 1].type === 'root');
|
||||
}
|
||||
|
||||
if (location) {
|
||||
return this.getOriginalPath(utils.parseKeyString(location), path);
|
||||
} else {
|
||||
return path;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
#pathContainsDomainObject(keyStringToCheck, path) {
|
||||
if (!keyStringToCheck) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return path.some(pathElement => {
|
||||
const identifierToCheck = utils.parseKeyString(keyStringToCheck);
|
||||
|
||||
return this.areIdsEqual(identifierToCheck, pathElement.identifier);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an identifier, constructs the original path by walking up its parents
|
||||
* @param {module:openmct.ObjectAPI~Identifier} identifier
|
||||
* @param {Array<module:openmct.DomainObject>} path an array of path objects
|
||||
* @returns {Promise<Array<module:openmct.DomainObject>>} a promise containing an array of domain objects
|
||||
*/
|
||||
async getOriginalPath(identifier, path = []) {
|
||||
const domainObject = await this.get(identifier);
|
||||
path.push(domainObject);
|
||||
const { location } = domainObject;
|
||||
if (location && (!this.#pathContainsDomainObject(location, path))) {
|
||||
// if we have a location, and we don't already have this in our constructed path,
|
||||
// then keep walking up the path
|
||||
return this.getOriginalPath(utils.parseKeyString(location), path);
|
||||
} else {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
isObjectPathToALink(domainObject, objectPath) {
|
||||
return objectPath !== undefined
|
||||
&& objectPath.length > 1
|
||||
|
||||
@@ -7,6 +7,7 @@ describe("The Object API", () => {
|
||||
let openmct = {};
|
||||
let mockDomainObject;
|
||||
const TEST_NAMESPACE = "test-namespace";
|
||||
const TEST_KEY = "test-key";
|
||||
const FIFTEEN_MINUTES = 15 * 60 * 1000;
|
||||
|
||||
beforeEach((done) => {
|
||||
@@ -22,7 +23,7 @@ describe("The Object API", () => {
|
||||
mockDomainObject = {
|
||||
identifier: {
|
||||
namespace: TEST_NAMESPACE,
|
||||
key: "test-key"
|
||||
key: TEST_KEY
|
||||
},
|
||||
name: "test object",
|
||||
type: "test-type"
|
||||
@@ -84,6 +85,31 @@ describe("The Object API", () => {
|
||||
expect(mockProvider.create).not.toHaveBeenCalled();
|
||||
expect(mockProvider.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("Shows a notification on persistence conflict", () => {
|
||||
beforeEach(() => {
|
||||
openmct.notifications.error = jasmine.createSpy('error');
|
||||
});
|
||||
|
||||
it("on create", () => {
|
||||
mockProvider.create.and.returnValue(Promise.reject(new openmct.objects.errors.Conflict("Test Conflict error")));
|
||||
|
||||
return objectAPI.save(mockDomainObject).catch(() => {
|
||||
expect(openmct.notifications.error).toHaveBeenCalledWith(`Conflict detected while saving ${TEST_NAMESPACE}:${TEST_KEY}`);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it("on update", () => {
|
||||
mockProvider.update.and.returnValue(Promise.reject(new openmct.objects.errors.Conflict("Test Conflict error")));
|
||||
mockDomainObject.persisted = Date.now() - FIFTEEN_MINUTES;
|
||||
mockDomainObject.modified = Date.now();
|
||||
|
||||
return objectAPI.save(mockDomainObject).catch(() => {
|
||||
expect(openmct.notifications.error).toHaveBeenCalledWith(`Conflict detected while saving ${TEST_NAMESPACE}:${TEST_KEY}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -138,21 +164,33 @@ describe("The Object API", () => {
|
||||
});
|
||||
|
||||
it("Caches multiple requests for the same object", () => {
|
||||
const promises = [];
|
||||
expect(mockProvider.get.calls.count()).toBe(0);
|
||||
objectAPI.get(mockDomainObject.identifier);
|
||||
promises.push(objectAPI.get(mockDomainObject.identifier));
|
||||
expect(mockProvider.get.calls.count()).toBe(1);
|
||||
objectAPI.get(mockDomainObject.identifier);
|
||||
promises.push(objectAPI.get(mockDomainObject.identifier));
|
||||
expect(mockProvider.get.calls.count()).toBe(1);
|
||||
|
||||
return Promise.all(promises);
|
||||
});
|
||||
|
||||
it("applies any applicable interceptors", () => {
|
||||
expect(mockDomainObject.changed).toBeUndefined();
|
||||
objectAPI.get(mockDomainObject.identifier).then((object) => {
|
||||
|
||||
return objectAPI.get(mockDomainObject.identifier).then((object) => {
|
||||
expect(object.changed).toBeTrue();
|
||||
expect(object.alsoChanged).toBeTrue();
|
||||
expect(object.shouldNotBeChanged).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("displays a notification in the event of an error", () => {
|
||||
mockProvider.get.and.returnValue(Promise.reject());
|
||||
|
||||
return objectAPI.get(mockDomainObject.identifier).catch(() => {
|
||||
expect(openmct.notifications.error).toHaveBeenCalledWith(`Failed to retrieve object ${TEST_NAMESPACE}:${TEST_KEY}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -168,7 +206,7 @@ describe("The Object API", () => {
|
||||
testObject = {
|
||||
identifier: {
|
||||
namespace: TEST_NAMESPACE,
|
||||
key: 'test-key'
|
||||
key: TEST_KEY
|
||||
},
|
||||
name: 'test object',
|
||||
type: 'notebook',
|
||||
@@ -195,6 +233,8 @@ describe("The Object API", () => {
|
||||
"observeObjectChanges"
|
||||
]);
|
||||
mockProvider.get.and.returnValue(Promise.resolve(testObject));
|
||||
mockProvider.create.and.returnValue(Promise.resolve(true));
|
||||
mockProvider.update.and.returnValue(Promise.resolve(true));
|
||||
mockProvider.observeObjectChanges.and.callFake(() => {
|
||||
callbacks[0](updatedTestObject);
|
||||
callbacks.splice(0, 1);
|
||||
@@ -337,6 +377,73 @@ describe("The Object API", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("getOriginalPath", () => {
|
||||
let mockGrandParentObject;
|
||||
let mockParentObject;
|
||||
let mockChildObject;
|
||||
|
||||
beforeEach(() => {
|
||||
const mockObjectProvider = jasmine.createSpyObj("mock object provider", [
|
||||
"create",
|
||||
"update",
|
||||
"get"
|
||||
]);
|
||||
|
||||
mockGrandParentObject = {
|
||||
type: 'folder',
|
||||
name: 'Grand Parent Folder',
|
||||
location: 'fooNameSpace:child',
|
||||
identifier: {
|
||||
key: 'grandParent',
|
||||
namespace: 'fooNameSpace'
|
||||
}
|
||||
};
|
||||
mockParentObject = {
|
||||
type: 'folder',
|
||||
name: 'Parent Folder',
|
||||
location: 'fooNameSpace:grandParent',
|
||||
identifier: {
|
||||
key: 'parent',
|
||||
namespace: 'fooNameSpace'
|
||||
}
|
||||
};
|
||||
mockChildObject = {
|
||||
type: 'folder',
|
||||
name: 'Child Folder',
|
||||
location: 'fooNameSpace:parent',
|
||||
identifier: {
|
||||
key: 'child',
|
||||
namespace: 'fooNameSpace'
|
||||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line require-await
|
||||
mockObjectProvider.get = async (identifier) => {
|
||||
if (identifier.key === mockGrandParentObject.identifier.key) {
|
||||
return mockGrandParentObject;
|
||||
} else if (identifier.key === mockParentObject.identifier.key) {
|
||||
return mockParentObject;
|
||||
} else if (identifier.key === mockChildObject.identifier.key) {
|
||||
return mockChildObject;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
openmct.objects.addProvider('fooNameSpace', mockObjectProvider);
|
||||
|
||||
mockObjectProvider.create.and.returnValue(Promise.resolve(true));
|
||||
mockObjectProvider.update.and.returnValue(Promise.resolve(true));
|
||||
|
||||
openmct.objects.addProvider('fooNameSpace', mockObjectProvider);
|
||||
});
|
||||
|
||||
it('can construct paths even with cycles', async () => {
|
||||
const objectPath = await objectAPI.getOriginalPath(mockChildObject.identifier);
|
||||
expect(objectPath.length).toEqual(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("transactions", () => {
|
||||
beforeEach(() => {
|
||||
spyOn(openmct.editor, 'isEditing').and.returnValue(true);
|
||||
|
||||
@@ -91,6 +91,10 @@ define([
|
||||
* @returns keyString
|
||||
*/
|
||||
function makeKeyString(identifier) {
|
||||
if (!identifier) {
|
||||
throw new Error("Cannot make key string from null identifier");
|
||||
}
|
||||
|
||||
if (isKeyString(identifier)) {
|
||||
return identifier;
|
||||
}
|
||||
|
||||
@@ -83,9 +83,11 @@ class UserAPI extends EventEmitter {
|
||||
* @throws Will throw an error if no user provider is set
|
||||
*/
|
||||
getCurrentUser() {
|
||||
this.noProviderCheck();
|
||||
|
||||
return this._provider.getCurrentUser();
|
||||
if (!this.hasProvider()) {
|
||||
return Promise.resolve(undefined);
|
||||
} else {
|
||||
return this._provider.getCurrentUser();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -97,11 +97,11 @@ export default {
|
||||
|
||||
},
|
||||
followTimeContext() {
|
||||
this.timeContext.on('bounds', this.reloadTelemetry);
|
||||
this.timeContext.on('bounds', this.reloadTelemetryOnBoundsChange);
|
||||
},
|
||||
stopFollowingTimeContext() {
|
||||
if (this.timeContext) {
|
||||
this.timeContext.off('bounds', this.reloadTelemetry);
|
||||
this.timeContext.off('bounds', this.reloadTelemetryOnBoundsChange);
|
||||
}
|
||||
},
|
||||
addToComposition(telemetryObject) {
|
||||
@@ -181,6 +181,11 @@ export default {
|
||||
this.composition.on('remove', this.removeTelemetryObject);
|
||||
this.composition.load();
|
||||
},
|
||||
reloadTelemetryOnBoundsChange(bounds, isTick) {
|
||||
if (!isTick) {
|
||||
this.reloadTelemetry();
|
||||
}
|
||||
},
|
||||
reloadTelemetry() {
|
||||
this.valuesByTimestamp = {};
|
||||
|
||||
|
||||
@@ -51,7 +51,11 @@ export default class TelemetryCriterion extends EventEmitter {
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.telemetryObjectIdAsString = this.openmct.objects.makeKeyString(this.telemetryDomainObjectDefinition.telemetry);
|
||||
this.telemetryObjectIdAsString = "";
|
||||
if (![undefined, null, ""].includes(this.telemetryDomainObjectDefinition?.telemetry)) {
|
||||
this.telemetryObjectIdAsString = this.openmct.objects.makeKeyString(this.telemetryDomainObjectDefinition.telemetry);
|
||||
}
|
||||
|
||||
this.updateTelemetryObjects(this.telemetryDomainObjectDefinition.telemetryObjects);
|
||||
if (this.isValid() && this.isStalenessCheck() && this.isValidInput()) {
|
||||
this.subscribeForStaleData();
|
||||
|
||||
@@ -517,7 +517,19 @@ export default {
|
||||
initializeItems() {
|
||||
this.telemetryViewMap = {};
|
||||
this.objectViewMap = {};
|
||||
this.layoutItems.forEach(this.trackItem);
|
||||
|
||||
let removedItems = [];
|
||||
this.layoutItems.forEach((item) => {
|
||||
if (item.identifier) {
|
||||
if (this.containsObject(item.identifier)) {
|
||||
this.trackItem(item);
|
||||
} else {
|
||||
removedItems.push(this.openmct.objects.makeKeyString(item.identifier));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
removedItems.forEach(this.removeFromConfiguration);
|
||||
},
|
||||
isItemAlreadyTracked(child) {
|
||||
let found = false;
|
||||
|
||||
@@ -232,10 +232,12 @@ export default {
|
||||
this.removeSelectable();
|
||||
}
|
||||
|
||||
this.telemetryCollection.off('add', this.setLatestValues);
|
||||
this.telemetryCollection.off('clear', this.refreshData);
|
||||
if (this.telemetryCollection) {
|
||||
this.telemetryCollection.off('add', this.setLatestValues);
|
||||
this.telemetryCollection.off('clear', this.refreshData);
|
||||
|
||||
this.telemetryCollection.destroy();
|
||||
this.telemetryCollection.destroy();
|
||||
}
|
||||
|
||||
if (this.mutablePromise) {
|
||||
this.mutablePromise.then(() => {
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
*****************************************************************************/
|
||||
|
||||
import { createOpenMct, resetApplicationState } from 'utils/testing';
|
||||
import Vue from 'vue';
|
||||
import DisplayLayoutPlugin from './plugin';
|
||||
|
||||
describe('the plugin', function () {
|
||||
@@ -117,6 +118,59 @@ describe('the plugin', function () {
|
||||
|
||||
});
|
||||
|
||||
describe('on load', () => {
|
||||
let displayLayoutItem;
|
||||
let item;
|
||||
|
||||
beforeEach((done) => {
|
||||
item = {
|
||||
'width': 32,
|
||||
'height': 18,
|
||||
'x': 78,
|
||||
'y': 8,
|
||||
'identifier': {
|
||||
'namespace': '',
|
||||
'key': 'bdeb91ab-3a7e-4a71-9dd2-39d73644e136'
|
||||
},
|
||||
'hasFrame': true,
|
||||
'type': 'line-view', // so no telemetry functionality is triggered, just want to test the sync
|
||||
'id': 'c0ff485a-344c-4e70-8d83-a9d9998a69fc'
|
||||
|
||||
};
|
||||
displayLayoutItem = {
|
||||
'composition': [
|
||||
// no item in compostion, but item in configuration items
|
||||
],
|
||||
'configuration': {
|
||||
'items': [
|
||||
item
|
||||
],
|
||||
'layoutGrid': [
|
||||
10,
|
||||
10
|
||||
]
|
||||
},
|
||||
'name': 'Display Layout',
|
||||
'type': 'layout',
|
||||
'identifier': {
|
||||
'namespace': '',
|
||||
'key': 'c5e636c1-6771-4c9c-b933-8665cab189b3'
|
||||
}
|
||||
};
|
||||
|
||||
const applicableViews = openmct.objectViews.get(displayLayoutItem, []);
|
||||
const displayLayoutViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'layout.view');
|
||||
const view = displayLayoutViewProvider.view(displayLayoutItem);
|
||||
view.show(child, false);
|
||||
|
||||
Vue.nextTick(done);
|
||||
});
|
||||
|
||||
it('will sync compostion and layout items', () => {
|
||||
expect(displayLayoutItem.configuration.items.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('the alpha numeric format view', () => {
|
||||
let displayLayoutItem;
|
||||
let telemetryItem;
|
||||
|
||||
@@ -71,6 +71,8 @@ import FaultManagementToolbar from './FaultManagementToolbar.vue';
|
||||
|
||||
import { FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS, FILTER_ITEMS, SORT_ITEMS } from './constants';
|
||||
|
||||
const SEARCH_KEYS = ['id', 'triggerValueInfo', 'currentValueInfo', 'triggerTime', 'severity', 'name', 'shortDescription', 'namespace'];
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FaultManagementListHeader,
|
||||
@@ -125,27 +127,19 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
filterUsingSearchTerm(fault) {
|
||||
if (fault?.id?.toString().toLowerCase().includes(this.searchTerm)) {
|
||||
return true;
|
||||
if (!fault) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (fault?.triggerValueInfo?.toString().toLowerCase().includes(this.searchTerm)) {
|
||||
return true;
|
||||
}
|
||||
let match = false;
|
||||
|
||||
if (fault?.currentValueInfo?.toString().toLowerCase().includes(this.searchTerm)) {
|
||||
return true;
|
||||
}
|
||||
SEARCH_KEYS.forEach((key) => {
|
||||
if (fault[key]?.toString().toLowerCase().includes(this.searchTerm)) {
|
||||
match = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (fault?.triggerTime.toString().toLowerCase().includes(this.searchTerm)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (fault?.severity.toString().toLowerCase().includes(this.searchTerm)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return match;
|
||||
},
|
||||
isSelected(fault) {
|
||||
return Boolean(this.selectedFaults[fault.id]);
|
||||
|
||||
@@ -24,10 +24,22 @@ import {
|
||||
createOpenMct,
|
||||
resetApplicationState
|
||||
} from '../../utils/testing';
|
||||
import { FAULT_MANAGEMENT_TYPE } from './constants';
|
||||
import {
|
||||
FAULT_MANAGEMENT_TYPE,
|
||||
FAULT_MANAGEMENT_VIEW,
|
||||
FAULT_MANAGEMENT_NAMESPACE
|
||||
} from './constants';
|
||||
|
||||
describe("The Fault Management Plugin", () => {
|
||||
let openmct;
|
||||
const faultDomainObject = {
|
||||
name: 'it is not your fault',
|
||||
type: FAULT_MANAGEMENT_TYPE,
|
||||
identifier: {
|
||||
key: 'nobodies',
|
||||
namespace: 'fault'
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
openmct = createOpenMct();
|
||||
@@ -38,15 +50,54 @@ describe("The Fault Management Plugin", () => {
|
||||
});
|
||||
|
||||
it('is not installed by default', () => {
|
||||
let typeDef = openmct.types.get(FAULT_MANAGEMENT_TYPE).definition;
|
||||
const typeDef = openmct.types.get(FAULT_MANAGEMENT_TYPE).definition;
|
||||
|
||||
expect(typeDef.name).toBe('Unknown Type');
|
||||
});
|
||||
|
||||
it('can be installed', () => {
|
||||
openmct.install(openmct.plugins.FaultManagement());
|
||||
let typeDef = openmct.types.get(FAULT_MANAGEMENT_TYPE).definition;
|
||||
const typeDef = openmct.types.get(FAULT_MANAGEMENT_TYPE).definition;
|
||||
|
||||
expect(typeDef.name).toBe('Fault Management');
|
||||
});
|
||||
|
||||
describe('once it is installed', () => {
|
||||
beforeEach(() => {
|
||||
openmct.install(openmct.plugins.FaultManagement());
|
||||
});
|
||||
|
||||
it('provides a view for fault management types', () => {
|
||||
const applicableViews = openmct.objectViews.get(faultDomainObject, []);
|
||||
const faultManagementView = applicableViews.find(
|
||||
(viewProvider) => viewProvider.key === FAULT_MANAGEMENT_VIEW
|
||||
);
|
||||
|
||||
expect(applicableViews.length).toEqual(1);
|
||||
expect(faultManagementView).toBeDefined();
|
||||
});
|
||||
|
||||
it('provides an inspector view for fault management types', () => {
|
||||
const faultDomainObjectSelection = [[
|
||||
{
|
||||
context: {
|
||||
item: faultDomainObject
|
||||
}
|
||||
}
|
||||
]];
|
||||
const applicableInspectorViews = openmct.inspectorViews.get(faultDomainObjectSelection);
|
||||
|
||||
expect(applicableInspectorViews.length).toEqual(1);
|
||||
});
|
||||
|
||||
it('creates a root object for fault management', async () => {
|
||||
const root = await openmct.objects.getRoot();
|
||||
const rootCompositionCollection = openmct.composition.get(root);
|
||||
const rootComposition = await rootCompositionCollection.load();
|
||||
const faultObject = rootComposition.find(obj => obj.identifier.namespace === FAULT_MANAGEMENT_NAMESPACE);
|
||||
|
||||
expect(faultObject).toBeDefined();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,6 +24,7 @@ import PropertiesAction from './PropertiesAction';
|
||||
import CreateWizard from './CreateWizard';
|
||||
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import _ from 'lodash';
|
||||
|
||||
export default class CreateAction extends PropertiesAction {
|
||||
constructor(openmct, type, parentDomainObject) {
|
||||
@@ -50,19 +51,15 @@ export default class CreateAction extends PropertiesAction {
|
||||
return;
|
||||
}
|
||||
|
||||
const properties = key.split('.');
|
||||
let object = this.domainObject;
|
||||
const propertiesLength = properties.length;
|
||||
properties.forEach((property, index) => {
|
||||
const isComplexProperty = propertiesLength > 1 && index !== propertiesLength - 1;
|
||||
if (isComplexProperty && object[property] !== null) {
|
||||
object = object[property];
|
||||
} else {
|
||||
object[property] = value;
|
||||
}
|
||||
});
|
||||
const existingValue = this.domainObject[`${key}`];
|
||||
if (!(existingValue instanceof Array) && (typeof existingValue === 'object')) {
|
||||
value = {
|
||||
...existingValue,
|
||||
...value
|
||||
};
|
||||
}
|
||||
|
||||
object = value;
|
||||
_.set(this.domainObject, `${key}`, value);
|
||||
});
|
||||
|
||||
const parentDomainObject = parentDomainObjectPath[0];
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
import PropertiesAction from './PropertiesAction';
|
||||
import CreateWizard from './CreateWizard';
|
||||
|
||||
export default class EditPropertiesAction extends PropertiesAction {
|
||||
constructor(openmct) {
|
||||
super(openmct);
|
||||
@@ -54,23 +55,26 @@ export default class EditPropertiesAction extends PropertiesAction {
|
||||
_onSave(changes) {
|
||||
try {
|
||||
Object.entries(changes).forEach(([key, value]) => {
|
||||
const properties = key.split('.');
|
||||
let object = this.domainObject;
|
||||
const propertiesLength = properties.length;
|
||||
properties.forEach((property, index) => {
|
||||
const isComplexProperty = propertiesLength > 1 && index !== propertiesLength - 1;
|
||||
if (isComplexProperty && object[property] !== null) {
|
||||
object = object[property];
|
||||
} else {
|
||||
object[property] = value;
|
||||
}
|
||||
});
|
||||
const existingValue = this.domainObject[`${key}`];
|
||||
if (!(existingValue instanceof Array) && (typeof existingValue === 'object')) {
|
||||
value = {
|
||||
...existingValue,
|
||||
...value
|
||||
};
|
||||
}
|
||||
|
||||
object = value;
|
||||
this.openmct.objects.mutate(this.domainObject, key, value);
|
||||
this.openmct.notifications.info('Save successful');
|
||||
this.openmct.objects.mutate(this.domainObject, `${key}`, value);
|
||||
});
|
||||
if (this.openmct.editor.isEditing()) {
|
||||
this.openmct.editor.save();
|
||||
}
|
||||
|
||||
this.openmct.notifications.info('Save successful');
|
||||
} catch (error) {
|
||||
if (this.openmct.editor.isEditing()) {
|
||||
this.openmct.editor.cancel();
|
||||
}
|
||||
|
||||
this.openmct.notifications.error('Error saving objects');
|
||||
console.error(error);
|
||||
}
|
||||
@@ -81,6 +85,9 @@ export default class EditPropertiesAction extends PropertiesAction {
|
||||
*/
|
||||
_onCancel() {
|
||||
//noop
|
||||
if (this.openmct.editor.isEditing()) {
|
||||
this.openmct.editor.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,6 +99,12 @@ export default class EditPropertiesAction extends PropertiesAction {
|
||||
const createWizard = new CreateWizard(this.openmct, this.domainObject, objectPath[1]);
|
||||
const formStructure = createWizard.getFormStructure(false);
|
||||
formStructure.title = 'Edit ' + this.domainObject.name;
|
||||
//If we're in edit mode AND want to edit properties for the same domain object
|
||||
//In this case, saving will put the object in view-only mode
|
||||
//TODO: Maybe we should block editing properties if someone is in Edit mode?
|
||||
if (!this.openmct.editor.isEditing()) {
|
||||
this.openmct.editor.edit();
|
||||
}
|
||||
|
||||
return this.openmct.forms.showForm(formStructure)
|
||||
.then(this._onSave.bind(this))
|
||||
|
||||
@@ -151,6 +151,7 @@ describe('EditPropertiesAction plugin', () => {
|
||||
function callback(newObject) {
|
||||
expect(newObject.name).not.toEqual(oldName);
|
||||
expect(newObject.name).toEqual(newName);
|
||||
expect(openmct.editor.isEditing()).toBeFalse();
|
||||
|
||||
unObserve();
|
||||
done();
|
||||
@@ -164,6 +165,7 @@ describe('EditPropertiesAction plugin', () => {
|
||||
openmct.forms.on('onFormPropertyChange', deBouncedFormChange);
|
||||
|
||||
function handleFormPropertyChange(data) {
|
||||
expect(openmct.editor.isEditing()).toBeTrue();
|
||||
const form = document.querySelector('.js-form');
|
||||
const title = form.querySelector('input');
|
||||
const notes = form.querySelector('textArea');
|
||||
@@ -213,10 +215,12 @@ describe('EditPropertiesAction plugin', () => {
|
||||
editPropertiesAction.invoke([domainObject])
|
||||
.then(() => {
|
||||
expect(domainObject.name).toEqual(name);
|
||||
expect(openmct.editor.isEditing()).toBeFalse();
|
||||
done();
|
||||
})
|
||||
.catch(() => {
|
||||
expect(domainObject.name).toEqual(name);
|
||||
expect(openmct.editor.isEditing()).toBeFalse();
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -100,6 +100,7 @@ export default {
|
||||
components: {
|
||||
ToggleSwitch
|
||||
},
|
||||
inject: ["openmct"],
|
||||
props: {
|
||||
model: {
|
||||
type: Object,
|
||||
@@ -107,6 +108,8 @@ export default {
|
||||
}
|
||||
},
|
||||
data() {
|
||||
this.changes = {};
|
||||
|
||||
return {
|
||||
isUseTelemetryLimits: this.model.value.isUseTelemetryLimits,
|
||||
isDisplayMinMax: this.model.value.isDisplayMinMax,
|
||||
@@ -118,21 +121,30 @@ export default {
|
||||
min: this.model.value.min
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.openmct.forms.on('formPropertiesChanged', this.onFormPropertyChange);
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.openmct.forms.off('formPropertiesChanged', this.onFormPropertyChange);
|
||||
},
|
||||
methods: {
|
||||
onFormPropertyChange(data) {
|
||||
this.changes = data?.configuration?.gaugeController || {};
|
||||
},
|
||||
onChange(event) {
|
||||
const data = {
|
||||
model: this.model,
|
||||
value: {
|
||||
gaugeType: this.model.value.gaugeType,
|
||||
isDisplayMinMax: this.isDisplayMinMax,
|
||||
isDisplayCurVal: this.isDisplayCurVal,
|
||||
isDisplayUnits: this.isDisplayUnits,
|
||||
gaugeType: this.changes.gaugeType || this.model.value.gaugeType,
|
||||
isDisplayMinMax: this.changes.isDisplayMinMax === undefined ? this.isDisplayMinMax : this.changes.isDisplayMinMax,
|
||||
isDisplayCurVal: this.changes.isDisplayCurVal === undefined ? this.isDisplayCurVal : this.changes.isDisplayCurVal,
|
||||
isDisplayUnits: this.changes.isDisplayUnits === undefined ? this.isDisplayUnits : this.changes.isDisplayUnits,
|
||||
isUseTelemetryLimits: this.isUseTelemetryLimits,
|
||||
limitLow: this.limitLow,
|
||||
limitHigh: this.limitHigh,
|
||||
max: this.max,
|
||||
min: this.min,
|
||||
precision: this.model.value.precision
|
||||
precision: this.changes.precision === undefined ? this.model.value.precision : this.changes.precision
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
68
src/plugins/imagery/components/ImageThumbnail.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="c-imagery__thumb c-thumb"
|
||||
:class="{
|
||||
'active': active,
|
||||
'selected': selected,
|
||||
'real-time': realTime
|
||||
}"
|
||||
:title="image.formattedTime"
|
||||
>
|
||||
<a
|
||||
href=""
|
||||
:download="image.imageDownloadName"
|
||||
@click.prevent
|
||||
>
|
||||
<img
|
||||
class="c-thumb__image"
|
||||
:src="image.url"
|
||||
>
|
||||
</a>
|
||||
<div class="c-thumb__timestamp">{{ image.formattedTime }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
image: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
active: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
selected: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
realTime: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -166,26 +166,15 @@
|
||||
class="c-imagery__thumbs-scroll-area"
|
||||
@scroll="handleScroll"
|
||||
>
|
||||
<div
|
||||
<ImageThumbnail
|
||||
v-for="(image, index) in imageHistory"
|
||||
:key="image.url + image.time"
|
||||
class="c-imagery__thumb c-thumb"
|
||||
:class="{ selected: focusedImageIndex === index && isPaused }"
|
||||
:title="image.formattedTime"
|
||||
@click="thumbnailClicked(index)"
|
||||
>
|
||||
<a
|
||||
href=""
|
||||
:download="image.imageDownloadName"
|
||||
@click.prevent
|
||||
>
|
||||
<img
|
||||
class="c-thumb__image"
|
||||
:src="image.url"
|
||||
>
|
||||
</a>
|
||||
<div class="c-thumb__timestamp">{{ image.formattedTime }}</div>
|
||||
</div>
|
||||
:image="image"
|
||||
:active="focusedImageIndex === index"
|
||||
:selected="focusedImageIndex === index && isPaused"
|
||||
:real-time="!isFixed"
|
||||
@click.native="thumbnailClicked(index)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@@ -205,6 +194,7 @@ import moment from 'moment';
|
||||
import RelatedTelemetry from './RelatedTelemetry/RelatedTelemetry';
|
||||
import Compass from './Compass/Compass.vue';
|
||||
import ImageControls from './ImageControls.vue';
|
||||
import ImageThumbnail from './ImageThumbnail.vue';
|
||||
import imageryData from "../../imagery/mixins/imageryData";
|
||||
|
||||
const REFRESH_CSS_MS = 500;
|
||||
@@ -229,9 +219,11 @@ const SHOW_THUMBS_THRESHOLD_HEIGHT = 200;
|
||||
const SHOW_THUMBS_FULLSIZE_THRESHOLD_HEIGHT = 600;
|
||||
|
||||
export default {
|
||||
name: 'ImageryView',
|
||||
components: {
|
||||
Compass,
|
||||
ImageControls
|
||||
ImageControls,
|
||||
ImageThumbnail
|
||||
},
|
||||
mixins: [imageryData],
|
||||
inject: ['openmct', 'domainObject', 'objectPath', 'currentView', 'imageFreshnessOptions'],
|
||||
@@ -254,6 +246,7 @@ export default {
|
||||
visibleLayers: [],
|
||||
durationFormatter: undefined,
|
||||
imageHistory: [],
|
||||
bounds: {},
|
||||
timeSystem: timeSystem,
|
||||
keyString: undefined,
|
||||
autoScroll: true,
|
||||
@@ -526,20 +519,17 @@ export default {
|
||||
},
|
||||
watch: {
|
||||
imageHistory: {
|
||||
handler(newHistory, oldHistory) {
|
||||
handler(newHistory, _oldHistory) {
|
||||
const newSize = newHistory.length;
|
||||
let imageIndex;
|
||||
let imageIndex = newSize > 0 ? newSize - 1 : undefined;
|
||||
if (this.focusedImageTimestamp !== undefined) {
|
||||
const foundImageIndex = newHistory.findIndex(img => img.time === this.focusedImageTimestamp);
|
||||
imageIndex = foundImageIndex > -1
|
||||
? foundImageIndex
|
||||
: newSize - 1;
|
||||
} else {
|
||||
imageIndex = newSize > 0
|
||||
? newSize - 1
|
||||
: undefined;
|
||||
if (foundImageIndex > -1) {
|
||||
imageIndex = foundImageIndex;
|
||||
}
|
||||
}
|
||||
|
||||
this.setFocusedImage(imageIndex);
|
||||
this.nextImageIndex = imageIndex;
|
||||
|
||||
if (this.previousFocusedImage && newHistory.length) {
|
||||
@@ -569,6 +559,16 @@ export default {
|
||||
this.resetAgeCSS();
|
||||
this.updateRelatedTelemetryForFocusedImage();
|
||||
this.getImageNaturalDimensions();
|
||||
},
|
||||
bounds() {
|
||||
this.scrollToFocused();
|
||||
},
|
||||
isFixed(newValue) {
|
||||
const isRealTime = !newValue;
|
||||
// if realtime unpause which will focus on latest image
|
||||
if (isRealTime) {
|
||||
this.paused(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
@@ -610,6 +610,7 @@ export default {
|
||||
this.handleScroll = _.debounce(this.handleScroll, SCROLL_LATENCY);
|
||||
this.handleThumbWindowResizeEnded = _.debounce(this.handleThumbWindowResizeEnded, SCROLL_LATENCY);
|
||||
this.handleThumbWindowResizeStart = _.debounce(this.handleThumbWindowResizeStart, SCROLL_LATENCY);
|
||||
this.scrollToFocused = _.debounce(this.scrollToFocused, 400);
|
||||
|
||||
if (this.$refs.thumbsWrapper) {
|
||||
this.thumbWrapperResizeObserver = new ResizeObserver(this.handleThumbWindowResizeStart);
|
||||
@@ -845,7 +846,8 @@ export default {
|
||||
if (domThumb) {
|
||||
domThumb.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center'
|
||||
block: 'center',
|
||||
inline: 'center'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -258,13 +258,22 @@
|
||||
min-width: $w;
|
||||
width: $w;
|
||||
|
||||
&.active {
|
||||
background: $colorSelectedBg;
|
||||
color: $colorSelectedFg;
|
||||
}
|
||||
&:hover {
|
||||
background: $colorThumbHoverBg;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: $colorPausedBg !important;
|
||||
color: $colorPausedFg !important;
|
||||
// fixed time - selected bg will match active bg color
|
||||
background: $colorSelectedBg;
|
||||
color: $colorSelectedFg;
|
||||
&.real-time {
|
||||
// real time - bg orange when selected
|
||||
background: $colorPausedBg !important;
|
||||
color: $colorPausedFg !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__image {
|
||||
|
||||
@@ -139,6 +139,7 @@ export default {
|
||||
// forcibly reset the imageContainer size to prevent an aspect ratio distortion
|
||||
delete this.imageContainerWidth;
|
||||
delete this.imageContainerHeight;
|
||||
this.bounds = bounds; // setting bounds for ImageryView watcher
|
||||
},
|
||||
timeSystemChange() {
|
||||
this.timeSystem = this.timeContext.timeSystem();
|
||||
|
||||
@@ -27,10 +27,13 @@ export default function MissingObjectInterceptor(openmct) {
|
||||
},
|
||||
invoke: (identifier, object) => {
|
||||
if (object === undefined) {
|
||||
const keyString = openmct.objects.makeKeyString(identifier);
|
||||
openmct.notifications.error(`Failed to retrieve object ${keyString}`);
|
||||
|
||||
return {
|
||||
identifier,
|
||||
type: 'unknown',
|
||||
name: 'Missing: ' + openmct.objects.makeKeyString(identifier)
|
||||
name: 'Missing: ' + keyString
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -83,7 +83,6 @@ export default class LinkAction {
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
this.openmct.forms.showForm(formStructure)
|
||||
.then(this.onSave.bind(this));
|
||||
}
|
||||
@@ -91,8 +90,8 @@ export default class LinkAction {
|
||||
validate(currentParent) {
|
||||
return (data) => {
|
||||
|
||||
// default current parent to ROOT, if it's undefined, then it's a root level item
|
||||
if (currentParent === undefined) {
|
||||
// default current parent to ROOT, if it's null, then it's a root level item
|
||||
if (!currentParent) {
|
||||
currentParent = {
|
||||
identifier: {
|
||||
key: 'ROOT',
|
||||
@@ -101,24 +100,23 @@ export default class LinkAction {
|
||||
};
|
||||
}
|
||||
|
||||
const parentCandidate = data.value[0];
|
||||
const currentParentKeystring = this.openmct.objects.makeKeyString(currentParent.identifier);
|
||||
const parentCandidateKeystring = this.openmct.objects.makeKeyString(parentCandidate.identifier);
|
||||
const parentCandidatePath = data.value;
|
||||
const parentCandidate = parentCandidatePath[0];
|
||||
const objectKeystring = this.openmct.objects.makeKeyString(this.object.identifier);
|
||||
|
||||
if (!this.openmct.objects.isPersistable(parentCandidate.identifier)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!parentCandidateKeystring || !currentParentKeystring) {
|
||||
// check if moving to same place
|
||||
if (this.openmct.objects.areIdsEqual(parentCandidate.identifier, currentParent.identifier)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parentCandidateKeystring === currentParentKeystring) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parentCandidateKeystring === objectKeystring) {
|
||||
// check if moving to a child
|
||||
if (parentCandidatePath.some(candidatePath => {
|
||||
return this.openmct.objects.areIdsEqual(candidatePath.identifier, this.object.identifier);
|
||||
})) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -145,26 +145,24 @@ export default class MoveAction {
|
||||
const parentCandidatePath = data.value;
|
||||
const parentCandidate = parentCandidatePath[0];
|
||||
|
||||
// check if moving to same place
|
||||
if (this.openmct.objects.areIdsEqual(parentCandidate.identifier, currentParent.identifier)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// check if moving to a child
|
||||
if (parentCandidatePath.some(candidatePath => {
|
||||
return this.openmct.objects.areIdsEqual(candidatePath.identifier, this.object.identifier);
|
||||
})) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.openmct.objects.isPersistable(parentCandidate.identifier)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let currentParentKeystring = this.openmct.objects.makeKeyString(currentParent.identifier);
|
||||
let parentCandidateKeystring = this.openmct.objects.makeKeyString(parentCandidate.identifier);
|
||||
let objectKeystring = this.openmct.objects.makeKeyString(this.object.identifier);
|
||||
|
||||
if (!parentCandidateKeystring || !currentParentKeystring) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parentCandidateKeystring === currentParentKeystring) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parentCandidateKeystring === objectKeystring) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const parentCandidateComposition = parentCandidate.composition;
|
||||
if (parentCandidateComposition && parentCandidateComposition.indexOf(objectKeystring) !== -1) {
|
||||
return false;
|
||||
|
||||
@@ -69,27 +69,27 @@ describe("the plugin", () => {
|
||||
});
|
||||
|
||||
describe('adds an interceptor that returns a "My Items" model for', () => {
|
||||
let myItemsMissing;
|
||||
let mockMissingProvider;
|
||||
let myItemsObject;
|
||||
let mockNotFoundProvider;
|
||||
let activeProvider;
|
||||
|
||||
beforeEach(async () => {
|
||||
mockMissingProvider = {
|
||||
get: () => Promise.resolve(missingObj),
|
||||
mockNotFoundProvider = {
|
||||
get: () => Promise.reject(new Error('Not found')),
|
||||
create: () => Promise.resolve(missingObj),
|
||||
update: () => Promise.resolve(missingObj)
|
||||
};
|
||||
|
||||
activeProvider = mockMissingProvider;
|
||||
activeProvider = mockNotFoundProvider;
|
||||
spyOn(openmct.objects, 'getProvider').and.returnValue(activeProvider);
|
||||
myItemsMissing = await openmct.objects.get(myItemsIdentifier);
|
||||
myItemsObject = await openmct.objects.get(myItemsIdentifier);
|
||||
});
|
||||
|
||||
it('missing objects', () => {
|
||||
let idsMatchMissing = openmct.objects.areIdsEqual(myItemsMissing.identifier, myItemsIdentifier);
|
||||
let idsMatch = openmct.objects.areIdsEqual(myItemsObject.identifier, myItemsIdentifier);
|
||||
|
||||
expect(myItemsMissing).toBeDefined();
|
||||
expect(idsMatchMissing).toBeTrue();
|
||||
expect(myItemsObject).toBeDefined();
|
||||
expect(idsMatch).toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -296,12 +296,17 @@ export default {
|
||||
window.addEventListener('orientationchange', this.formatSidebar);
|
||||
window.addEventListener('hashchange', this.setSectionAndPageFromUrl);
|
||||
this.filterAndSortEntries();
|
||||
this.unobserveEntries = this.openmct.objects.observe(this.domainObject, '*', this.filterAndSortEntries);
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.unlisten) {
|
||||
this.unlisten();
|
||||
}
|
||||
|
||||
if (this.unobserveEntries) {
|
||||
this.unobserveEntries();
|
||||
}
|
||||
|
||||
window.removeEventListener('orientationchange', this.formatSidebar);
|
||||
window.removeEventListener('hashchange', this.setSectionAndPageFromUrl);
|
||||
},
|
||||
|
||||
@@ -88,6 +88,7 @@
|
||||
:annotation-type="openmct.annotation.ANNOTATION_TYPES.NOTEBOOK"
|
||||
:annotation-search-type="openmct.objects.SEARCH_TYPES.NOTEBOOK_ANNOTATIONS"
|
||||
:target-specific-details="{entryId: entry.id}"
|
||||
@tags-updated="timestampAndUpdate"
|
||||
/>
|
||||
|
||||
<div class="c-snapshots c-ne__embeds">
|
||||
@@ -146,6 +147,8 @@ import { saveNotebookImageDomainObject, updateNamespaceOfDomainObject } from '..
|
||||
|
||||
import Moment from 'moment';
|
||||
|
||||
const UNKNOWN_USER = 'Unknown';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NotebookEmbed,
|
||||
@@ -206,7 +209,8 @@ export default {
|
||||
|
||||
return {
|
||||
targetKeyString,
|
||||
entryId: this.entry.id
|
||||
entryId: this.entry.id,
|
||||
modified: this.entry.modified
|
||||
};
|
||||
},
|
||||
createdOnTime() {
|
||||
@@ -283,7 +287,7 @@ export default {
|
||||
await this.addNewEmbed(objectPath);
|
||||
}
|
||||
|
||||
this.$emit('updateEntry', this.entry);
|
||||
this.timestampAndUpdate();
|
||||
},
|
||||
findPositionInArray(array, id) {
|
||||
let position = -1;
|
||||
@@ -321,7 +325,7 @@ export default {
|
||||
// TODO: remove notebook snapshot object using object remove API
|
||||
this.entry.embeds.splice(embedPosition, 1);
|
||||
|
||||
this.$emit('updateEntry', this.entry);
|
||||
this.timestampAndUpdate();
|
||||
},
|
||||
updateEmbed(newEmbed) {
|
||||
this.entry.embeds.some(e => {
|
||||
@@ -333,6 +337,17 @@ export default {
|
||||
return found;
|
||||
});
|
||||
|
||||
this.timestampAndUpdate();
|
||||
},
|
||||
async timestampAndUpdate() {
|
||||
const user = await this.openmct.user.getCurrentUser();
|
||||
|
||||
if (user === undefined) {
|
||||
this.entry.modifiedBy = UNKNOWN_USER;
|
||||
}
|
||||
|
||||
this.entry.modified = Date.now();
|
||||
|
||||
this.$emit('updateEntry', this.entry);
|
||||
},
|
||||
editingEntry() {
|
||||
@@ -342,7 +357,7 @@ export default {
|
||||
const value = $event.target.innerText;
|
||||
if (value !== this.entry.text && value.match(/\S/)) {
|
||||
this.entry.text = value;
|
||||
this.$emit('updateEntry', this.entry);
|
||||
this.timestampAndUpdate();
|
||||
} else {
|
||||
this.$emit('cancelEdit');
|
||||
}
|
||||
|
||||
@@ -12,8 +12,10 @@
|
||||
<template v-if="!page.isLocked">
|
||||
<div
|
||||
class="c-list__item__name js-list__item__name"
|
||||
:class="[{ 'c-input-inline': isSelected }]"
|
||||
:data-id="page.id"
|
||||
:contenteditable="true"
|
||||
:contenteditable="isSelected"
|
||||
@keydown.escape="updateName"
|
||||
@keydown.enter="updateName"
|
||||
@blur="updateName"
|
||||
>{{ pageName }}</div>
|
||||
@@ -32,8 +34,9 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PopupMenu from './PopupMenu.vue';
|
||||
import { KEY_ENTER, KEY_ESCAPE } from '../utils/notebook-key-code';
|
||||
import RemoveDialog from '../utils/removeDialog';
|
||||
import PopupMenu from './PopupMenu.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -107,36 +110,39 @@ export default {
|
||||
removeDialog.show();
|
||||
},
|
||||
selectPage(event) {
|
||||
const target = event.target;
|
||||
const id = target.dataset.id;
|
||||
const { target: { dataset: { id } } } = event;
|
||||
|
||||
if (!this.page.isLocked) {
|
||||
const page = target.closest('.js-list__item');
|
||||
const input = page.querySelector('.js-list__item__name');
|
||||
|
||||
if (page.className.indexOf('is-selected') > -1) {
|
||||
input.classList.add('c-input-inline');
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!id) {
|
||||
if (this.isSelected || !id) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit('selectPage', id);
|
||||
},
|
||||
updateName(event) {
|
||||
const target = event.target;
|
||||
const name = target.textContent.toString();
|
||||
target.classList.remove('c-input-inline');
|
||||
|
||||
if (name === '' || this.page.name === name) {
|
||||
renamePage(target) {
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit('renamePage', Object.assign(this.page, { name }));
|
||||
target.textContent = target.textContent ? target.textContent.trim() : `Unnamed ${this.pageTitle}`;
|
||||
|
||||
if (this.page.name === target.textContent) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit('renamePage', Object.assign(this.page, { name: target.textContent }));
|
||||
},
|
||||
updateName(event) {
|
||||
const { target, keyCode, type } = event;
|
||||
|
||||
if (keyCode === KEY_ESCAPE) {
|
||||
target.textContent = this.page.name;
|
||||
} else if (keyCode === KEY_ENTER || type === 'blur') {
|
||||
this.renamePage(target);
|
||||
}
|
||||
|
||||
target.scrollLeft = '0';
|
||||
|
||||
target.blur();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -7,8 +7,10 @@
|
||||
>
|
||||
<span
|
||||
class="c-list__item__name js-list__item__name"
|
||||
:class="[{ 'c-input-inline': isSelected && !section.isLocked }]"
|
||||
:data-id="section.id"
|
||||
contenteditable="true"
|
||||
:contenteditable="isSelected && !section.isLocked"
|
||||
@keydown.escape="updateName"
|
||||
@keydown.enter="updateName"
|
||||
@blur="updateName"
|
||||
>{{ sectionName }}</span>
|
||||
@@ -20,8 +22,9 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PopupMenu from './PopupMenu.vue';
|
||||
import { KEY_ENTER, KEY_ESCAPE } from '../utils/notebook-key-code';
|
||||
import RemoveDialog from '../utils/removeDialog';
|
||||
import PopupMenu from './PopupMenu.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -96,36 +99,39 @@ export default {
|
||||
removeDialog.show();
|
||||
},
|
||||
selectSection(event) {
|
||||
const target = event.target;
|
||||
const id = target.dataset.id;
|
||||
const { target: { dataset: { id } } } = event;
|
||||
|
||||
if (!this.section.isLocked) {
|
||||
const section = target.closest('.js-list__item');
|
||||
const input = section.querySelector('.js-list__item__name');
|
||||
|
||||
if (section.className.indexOf('is-selected') > -1) {
|
||||
input.classList.add('c-input-inline');
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!id) {
|
||||
if (this.isSelected || !id) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit('selectSection', id);
|
||||
},
|
||||
updateName(event) {
|
||||
const target = event.target;
|
||||
target.classList.remove('c-input-inline');
|
||||
const name = target.textContent.trim();
|
||||
|
||||
if (name === '' || this.section.name === name) {
|
||||
renameSection(target) {
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit('renameSection', Object.assign(this.section, { name }));
|
||||
target.textContent = target.textContent ? target.textContent.trim() : `Unnamed ${this.sectionTitle}`;
|
||||
|
||||
if (this.section.name === target.textContent) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit('renameSection', Object.assign(this.section, { name: target.textContent }));
|
||||
},
|
||||
updateName(event) {
|
||||
const { target, keyCode, type } = event;
|
||||
|
||||
if (keyCode === KEY_ESCAPE) {
|
||||
target.textContent = this.section.name;
|
||||
} else if (keyCode === KEY_ENTER || type === 'blur') {
|
||||
this.renameSection(target);
|
||||
}
|
||||
|
||||
target.scrollLeft = '0';
|
||||
|
||||
target.blur();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
}
|
||||
|
||||
&__name {
|
||||
flex: 0 1 auto;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
&__menu-indicator {
|
||||
|
||||
@@ -211,10 +211,17 @@ describe("Notebook plugin:", () => {
|
||||
|
||||
describe("synchronization", () => {
|
||||
|
||||
let objectCloneToSyncFrom;
|
||||
|
||||
beforeEach(() => {
|
||||
objectCloneToSyncFrom = structuredClone(notebookViewObject);
|
||||
objectCloneToSyncFrom.persisted = notebookViewObject.modified + 1;
|
||||
});
|
||||
|
||||
it("updates an entry when another user modifies it", () => {
|
||||
expect(getEntryText(0).innerText).toBe("First Test Entry");
|
||||
notebookViewObject.configuration.entries["test-section-1"]["test-page-1"][0].text = "Modified entry text";
|
||||
objectProviderObserver(notebookViewObject);
|
||||
objectCloneToSyncFrom.configuration.entries["test-section-1"]["test-page-1"][0].text = "Modified entry text";
|
||||
objectProviderObserver(objectCloneToSyncFrom);
|
||||
|
||||
return Vue.nextTick().then(() => {
|
||||
expect(getEntryText(0).innerText).toBe("Modified entry text");
|
||||
@@ -223,13 +230,13 @@ describe("Notebook plugin:", () => {
|
||||
|
||||
it("shows new entry when another user adds one", () => {
|
||||
expect(allNotebookEntryElements().length).toBe(2);
|
||||
notebookViewObject.configuration.entries["test-section-1"]["test-page-1"].push({
|
||||
objectCloneToSyncFrom.configuration.entries["test-section-1"]["test-page-1"].push({
|
||||
"id": "entry-3",
|
||||
"createdOn": 0,
|
||||
"text": "Third Test Entry",
|
||||
"embeds": []
|
||||
});
|
||||
objectProviderObserver(notebookViewObject);
|
||||
objectProviderObserver(objectCloneToSyncFrom);
|
||||
|
||||
return Vue.nextTick().then(() => {
|
||||
expect(allNotebookEntryElements().length).toBe(3);
|
||||
@@ -237,9 +244,9 @@ describe("Notebook plugin:", () => {
|
||||
});
|
||||
it("removes an entry when another user removes one", () => {
|
||||
expect(allNotebookEntryElements().length).toBe(2);
|
||||
let entries = notebookViewObject.configuration.entries["test-section-1"]["test-page-1"];
|
||||
notebookViewObject.configuration.entries["test-section-1"]["test-page-1"] = entries.splice(0, 1);
|
||||
objectProviderObserver(notebookViewObject);
|
||||
let entries = objectCloneToSyncFrom.configuration.entries["test-section-1"]["test-page-1"];
|
||||
objectCloneToSyncFrom.configuration.entries["test-section-1"]["test-page-1"] = entries.splice(0, 1);
|
||||
objectProviderObserver(objectCloneToSyncFrom);
|
||||
|
||||
return Vue.nextTick().then(() => {
|
||||
expect(allNotebookEntryElements().length).toBe(1);
|
||||
@@ -256,8 +263,8 @@ describe("Notebook plugin:", () => {
|
||||
};
|
||||
|
||||
expect(allNotebookPageElements().length).toBe(2);
|
||||
notebookViewObject.configuration.sections[0].pages.push(newPage);
|
||||
objectProviderObserver(notebookViewObject);
|
||||
objectCloneToSyncFrom.configuration.sections[0].pages.push(newPage);
|
||||
objectProviderObserver(objectCloneToSyncFrom);
|
||||
|
||||
return Vue.nextTick().then(() => {
|
||||
expect(allNotebookPageElements().length).toBe(3);
|
||||
@@ -267,8 +274,8 @@ describe("Notebook plugin:", () => {
|
||||
|
||||
it("updates the notebook when a user removes a page", () => {
|
||||
expect(allNotebookPageElements().length).toBe(2);
|
||||
notebookViewObject.configuration.sections[0].pages.splice(0, 1);
|
||||
objectProviderObserver(notebookViewObject);
|
||||
objectCloneToSyncFrom.configuration.sections[0].pages.splice(0, 1);
|
||||
objectProviderObserver(objectCloneToSyncFrom);
|
||||
|
||||
return Vue.nextTick().then(() => {
|
||||
expect(allNotebookPageElements().length).toBe(1);
|
||||
@@ -291,8 +298,8 @@ describe("Notebook plugin:", () => {
|
||||
};
|
||||
|
||||
expect(allNotebookSectionElements().length).toBe(2);
|
||||
notebookViewObject.configuration.sections.push(newSection);
|
||||
objectProviderObserver(notebookViewObject);
|
||||
objectCloneToSyncFrom.configuration.sections.push(newSection);
|
||||
objectProviderObserver(objectCloneToSyncFrom);
|
||||
|
||||
return Vue.nextTick().then(() => {
|
||||
expect(allNotebookSectionElements().length).toBe(3);
|
||||
@@ -301,8 +308,8 @@ describe("Notebook plugin:", () => {
|
||||
|
||||
it("updates the notebook when a user removes a section", () => {
|
||||
expect(allNotebookSectionElements().length).toBe(2);
|
||||
notebookViewObject.configuration.sections.splice(0, 1);
|
||||
objectProviderObserver(notebookViewObject);
|
||||
objectCloneToSyncFrom.configuration.sections.splice(0, 1);
|
||||
objectProviderObserver(objectCloneToSyncFrom);
|
||||
|
||||
return Vue.nextTick().then(() => {
|
||||
expect(allNotebookSectionElements().length).toBe(1);
|
||||
|
||||
3
src/plugins/notebook/utils/notebook-key-code.js
Normal file
@@ -0,0 +1,3 @@
|
||||
// Key codes for `KeyboardEvent.keyCode`.
|
||||
export const KEY_ENTER = 13;
|
||||
export const KEY_ESCAPE = 27;
|
||||
5
src/plugins/persistence/couch/.env.ci
Normal file
@@ -0,0 +1,5 @@
|
||||
OPENMCT_DATABASE_NAME=openmct
|
||||
COUCH_ADMIN_USER=admin
|
||||
COUCH_ADMIN_PASSWORD=password
|
||||
COUCH_BASE_LOCAL=http://localhost:5984
|
||||
COUCH_NODE_NAME=nonode@nohost
|
||||