Compare commits

...

40 Commits

Author SHA1 Message Date
Shefali
7bccb73729 Merge branch 'release/2.0.8' of https://github.com/nasa/openmct into release/2.0.8 2022-08-24 09:23:37 -07:00
Shefali Joshi
78df5fc2a5 Don't re-request historical data on ticks (#5701)
Don't rerequest telemetry on ticks.
2022-08-23 19:51:26 -07:00
Scott Bell
b53cc810f5 Only index if provider does not support search - mct5690 (#5693)
* only index if provider does not support search

* add some tests

* fix tests

* [e2e] Add search couchdb test for duplicates

* [e2e] Modify existing search test instead

* lint

Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
2022-08-23 14:08:34 -07:00
Jamie V
5386ceb94c [Time Conductor] History not working correctly (#5687)
* the check for fixed time vs realtime was not updating, have fixed this

* merging in related changes from master pr #4414

* lint fixes

* Update src/plugins/timeConductor/ConductorHistory.vue

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

* setting time mode directly on load

* fixing issue where realtime history was being wiped on reloads while viewing fixed time

* formatting

* stubbed in some tests

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2022-08-23 09:36:02 -07:00
Shefali
5a4dd11955 Merge branch 'release/2.0.8' of https://github.com/nasa/openmct into release/2.0.8 2022-08-23 09:23:19 -07:00
Jesse Mazzella
affb7a5311 [Docs] Update CouchDB local install documentation (#5692)
* Update local CouchDB install docs to include docker workflow

* reformat to source configuration scripts where possible

* correct couchdb case

Co-authored-by: John Hill <john.c.hill@nasa.gov>
2022-08-23 09:00:04 -07:00
Shefali
1e1133046a Merge branch 'release/2.0.7' of https://github.com/nasa/openmct into release/2.0.8 2022-08-22 12:06:38 -07:00
Shefali Joshi
06b321588e Handle couch db not found errors so that interceptors are still invoked. (#5654)
* Fix tests for interceptors
* [e2e] Add test for 'mine' folder initialization
* [e2e] don't fail on expected console errors

Co-authored-by: Andrew Henry <akhenry@gmail.com>
Co-authored-by: Scott Bell <scott@traclabs.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
2022-08-22 10:15:52 -07:00
John Hill
909901b0f3 [CI] Enable couchdb e2e testing in open source (#5655) 2022-08-19 13:14:27 -07:00
Charles Hacskaylo
865f95c0b6 Time List 5534 for release/2.0.8 (#5678)
* Changes to Time List view. Closes #5534.
- Compacted table row spacing.
- Set all timeframes to display by default when creating a new Time List.
- Removed 'Upload plan' file button from properties.

* Changes to Time List view. Closes #5534.
- Better hint text for editing Timeframe Inspector section.

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2022-08-19 18:11:30 +00:00
Michael Rogers
cb1e04b0d6 Set Focused Image index after a imagery is selected from a timestrip - 5632 (#5664)
* Set focused image when timestamp prop is passed in

* Unused var

* Create timestrip with imagery child

* Add equality check for hovered image and view large image url

* Cleanup
2022-08-19 17:39:58 +00:00
Shefali Joshi
b0c2e66613 Moves condition set fix into 2.0.8 (#5673) 2022-08-19 15:47:43 +00:00
Jesse Mazzella
d162b5dbd8 [e2e] Stabilize notebook tag tests (#5681)
* Use more deterministic selector

* Hover first to "slow down" e2e actions while in headless mode
2022-08-18 21:23:58 +02:00
Jamie V
64565b1bbb [Display Layout] Composition and configuration sync (#5669)
LGTM
2022-08-18 06:52:45 -07:00
Scott Bell
f721980bf0 Mct5549 fix indexer composition error (#5610) 2022-08-18 05:58:54 -07:00
Jesse Mazzella
b47712a0f4 Sort tree items locally on rename (#5643)
* fix typo

* Sort the tree items locally on object rename

* Use the navigationPath as a key

- This ensures that objects AND linked objects will be sorted

* add 'tree' and 'treeitem' roles to mct-tree

* WIP tree item reordering test

* Select the first object that matches

* Test that all object links are also reordered

* Get the final uuid before queryParams as notebook sections have uuids

* Make `openObjectTreeContextMenu` more deterministic and update usage

* Add `expandPathToTreeItem` and `expandTreeItemByName` appActions

* add `#tree-pane` id for the tree view

* Add tree visual component test suite and bump percy-cli

* Remove tree appActions

* Better variable name

Co-authored-by: Scott Bell <scott@traclabs.com>
2022-08-17 18:16:40 +02:00
Jamie V
57f3d4eba0 [Fault Management] New Example Provider, Unit and e2e tests (#5579)
* added unit tests for fault management plugin

* modified the example fault provider to work out of the box

* updating for new e2e folder structure

* part of the e2e tests

* WIP

* Imagery thumbnail regression fixes - 5327 (#5569)

* Add an active class to thumbnail to indicate current focused image

* Differentiate bg color between real-time and fixed

* scrollIntoView inline: center

* Added watcher for bounds change to trigger thumbnail scroll

* Resolve merge conflict with requestHistory change to telemetry collection

* Split thumbnail into sub component

* Monitor isFixed value to unpause playback status

* updated search to include name, namespace and description added some more e2e tests

* added rest of e2e tests

* fixed my init script, had to disable lint for no-force because it was not working without it, saw online this may be a pw bug

* fix: removing maelstrom theme from application (#5600)

* added some tests for no faults

* visual tests

* added visual tests for fault management

* created utils file for shared functionality between function and visual tests

* updating to 2.0.8

* tryin to remove imagery changes from master

* trying to trigger a refresh

* tryin to refresh

* updated search to include name, namespace and description added some more e2e tests

* added rest of e2e tests

* fix: removing maelstrom theme from application (#5600)

* fixed my init script, had to disable lint for no-force because it was not working without it, saw online this may be a pw bug

* added some tests for no faults

* visual tests

* added visual tests for fault management

* created utils file for shared functionality between function and visual tests

* updating to 2.0.8

* no clue

* still no clue

* removing imports and chaning to requires

* updating utils file to work with require

* fixing paths

* fixing a test I had messed up when adding static exmaple faults

* ONE LAST PATH FIX... hopefully

* typo in files fix

* fix folder typo

* thought I got this one, but apparently not, well I did now! who is laughing now!?

Co-authored-by: Michael Rogers <contact@mhrogers.com>
Co-authored-by: Vitor Henckel <vitor@henckel.com.br>
2022-08-15 14:09:23 -07:00
Shefali Joshi
064a865c9b [Condition Set] Add check for empty string being passed to the makeKeyString util by TelemetryCriterion (#5636) (#5663)
* Check telemetry is defined before using makeKeyString util

* Add optional chaining in the check

* Add e2e test

* Add check for undefined

Co-authored-by: Khalid Adil <khalidadil29@gmail.com>
2022-08-15 14:21:21 -05:00
Scott Bell
61bf60783c Prevent cyclic references in link & move actions (#5635)
* do not create circular refs

* add negative validation test

* move to plugin

* add link test too

* fix docs

* refactored per john request

* fix path

* use appAction lib

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2022-08-10 19:05:38 +00:00
Shefali
5dc718b78d Update version number 2022-08-10 11:34:12 -07:00
Scott Bell
41f8cb404d Check for circular references in originalPath - 5615 (#5619)
* check for circular references

* add test

* fix test

* address PR comments by making comments better

* fix docs...again
2022-08-09 12:11:03 +02:00
Khalid Adil
c6c58af12c [e2e] Tests for Display Layout and LAD Tables and telemetry (#5607) 2022-08-08 13:30:20 -07:00
Andrew Henry
15a0a87251 Revert "Have in-memory search indexer use composition API (#5578)" (#5609)
This reverts commit 7cf11e177c.
2022-08-04 19:15:54 +00:00
Alize Nguyen
59a8614f1c Add parsing for areIdsEqual util to consistently remove folders (#5589)
* Add parsing util to identifier for ID comparison

* Moved firstIdentifier to top of function

* Lint fix

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2022-08-04 11:42:43 -07:00
Scott Bell
7cf11e177c Have in-memory search indexer use composition API (#5578)
* need to remove tags and objects on composition removal
* had to separate out emits from load as it was causing memory indexer to loop upon itself
2022-08-04 18:20:38 +00:00
Scott Bell
1a44652470 Search should indicate in progress and no results states, filter orphaned results (#5599)
* no matching result implemented

* now filtering annotations that are orphaned

* filter object results without valid paths

* add progress bar

* added e2e tests

* removed extraneous click

* fix typos

* fix unit tests

* lint

* address pr comments

* fix tests

* fix tests, centralize logic to object api, check for root instead

* remove debug statement

* lint

* fix documentation

* lint

* fix doc

* made some optimizations after talking with akhenry

* fix test

* update docs

* fix docs
2022-08-04 11:06:16 -07:00
Andrew Henry
51d16f812a Include the plan source map when generating the time list/plan hybrid object (#5604) 2022-08-03 18:17:16 -07:00
Charles Hacskaylo
25de5653e8 Fix menu style in Snow theme (#5557) 2022-08-03 11:24:04 -07:00
John Hill
cb6014d69f Update package.json (#5601) 2022-08-03 11:12:31 -07:00
Jesse Mazzella
36736eb8a0 [e2e] Improve appActions (#5592)
* update selectors to use aria labels

* Update appActions

- Create new function `getHashUrlToDomainObject` to get the browse url to a given object given its uuid

- Create new function `getFocusedObjectUuid`... self explanatory :)

- Update `createDomainObjectWIthDefaults` to make use of the new url generation

- Update `createDomainObject...`'s arguments to be more organized, and accept a parent object

- Update some docs, still need to clarify some

* Update appActions e2e tests

- Refactor for organization

- Test our new appActions in one go

* Update existing usages of `createDomainObject...` to match the new API

* fix accidental renamed export

* Fix jsdoc return types

* refactor telemetryTable test to use appActions

* Improve selectors

* Refactor test

* improve selector

* add clock mode appActions

* lint

* Fix jsdoc

* Code review comments

* mark failing visual tests as fixme temporarily
2022-08-03 00:48:47 +00:00
Michael Rogers
a13a6002c5 Imagery thumbnail regression fixes - 5327 (#5591)
* Add an active class to thumbnail to indicate current focused image

* Differentiate bg color between real-time and fixed

* scrollIntoView inline: center

* Added watcher for bounds change to trigger thumbnail scroll

* Resolve merge conflict with requestHistory change to telemetry collection

* Split thumbnail into sub component

* Monitor isFixed value to unpause playback status

Co-authored-by: Khalid Adil <khalidadil29@gmail.com>
2022-08-02 13:44:01 -05:00
dependabot[bot]
f2ed518300 Bump @babel/eslint-parser from 7.18.2 to 7.18.9 (#5530)
Bumps [@babel/eslint-parser](https://github.com/babel/babel/tree/HEAD/eslint/babel-eslint-parser) from 7.18.2 to 7.18.9.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.18.9/eslint/babel-eslint-parser)

---
updated-dependencies:
- dependency-name: "@babel/eslint-parser"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-31 14:36:52 -07:00
dependabot[bot]
bf8d1561ae Bump @percy/cli from 1.6.4 to 1.7.2 (#5583)
Bumps [@percy/cli](https://github.com/percy/cli/tree/HEAD/packages/cli) from 1.6.4 to 1.7.2.
- [Release notes](https://github.com/percy/cli/releases)
- [Commits](https://github.com/percy/cli/commits/v1.7.2/packages/cli)

---
updated-dependencies:
- dependency-name: "@percy/cli"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-31 13:41:15 -07:00
John Hill
2e1ede1427 Add Visual Test for Snow Theme and add visual tests to PR execution (#5570) 2022-07-29 17:35:43 -07:00
Jesse Mazzella
fc3614dfbd [e2e] More VIPER compatibility fixes (#5582) 2022-07-29 16:50:40 -07:00
Andrew Henry
22924f18fc Better handling of persistence errors (#5576)
* conflict errors in particular
2022-07-29 14:29:34 -07:00
Andrew Henry
60f20c64d5 Fix multi user notebook (#5563)
* Detect remote changes to notebook object and re-render entries. Detect changes to tags as well

* Do not throw an error when getCurrentUser is called, just return undefined. Code needs a way of testing whether there is a valid user

* Support remote sync of annotations for notebook entries

* Fix bug in notebook spec that prevented multi-user notebook regression being detected

* Fixes edge case where an annotation does not initially exist

* Use structuredClone instead of JSON functions. Fix logical error in entry modification attribution. Address magical value

Co-authored-by: Scott Bell <scott@traclabs.com>
2022-07-29 11:14:09 +02:00
dependabot[bot]
8dc8a1c0a9 Bump jasmine-core from 4.2.0 to 4.3.0 (#5553)
Bumps [jasmine-core](https://github.com/jasmine/jasmine) from 4.2.0 to 4.3.0.
- [Release notes](https://github.com/jasmine/jasmine/releases)
- [Changelog](https://github.com/jasmine/jasmine/blob/main/RELEASE.md)
- [Commits](https://github.com/jasmine/jasmine/compare/v4.2.0...v4.3.0)

---
updated-dependencies:
- dependency-name: jasmine-core
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2022-07-28 20:19:13 -07:00
dependabot[bot]
710259b5f0 Bump eslint-plugin-vue from 9.1.1 to 9.3.0 (#5554)
Bumps [eslint-plugin-vue](https://github.com/vuejs/eslint-plugin-vue) from 9.1.1 to 9.3.0.
- [Release notes](https://github.com/vuejs/eslint-plugin-vue/releases)
- [Commits](https://github.com/vuejs/eslint-plugin-vue/compare/v9.1.1...v9.3.0)

---
updated-dependencies:
- dependency-name: eslint-plugin-vue
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2022-07-28 13:56:04 -07:00
Scott Bell
e774eb01f3 5211 tests arent using source maps master (#5566)
* removed unused require
* simplify config

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2022-07-28 11:23:41 -07:00
99 changed files with 3567 additions and 1011 deletions

View File

@@ -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 * * *"

37
.github/workflows/e2e-couchdb.yml vendored Normal file
View 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

View File

@@ -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 }}

View File

@@ -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;

View File

@@ -148,6 +148,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

View File

@@ -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
};

View 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());
});

View 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));
});

View 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
View 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
};

View 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());
});

View File

@@ -32,6 +32,7 @@ const config = {
projects: [
{
name: 'chrome',
testMatch: '**/*.e2e.spec.js', // only run e2e tests
use: {
browserName: 'chromium'
}

View File

@@ -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
}]
]
};

View File

@@ -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;

View 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}`);
});
});
});

View File

@@ -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' });

View File

@@ -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');

View File

@@ -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

View File

@@ -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);
// });
});

View File

@@ -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' });

View 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);
});
});

View File

@@ -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();

View 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
});

View File

@@ -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
});

View File

@@ -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);
});
});

View File

@@ -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;
}

View File

@@ -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);
});
});

View File

@@ -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 }) => {
@@ -208,7 +200,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();
@@ -532,7 +524,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 +573,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
*/

View 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;
}

View File

@@ -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,8 @@ 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('Locked page should now be in a locked state @addInit @unstable', async ({ page }, testInfo) => {
test.fixme(testInfo.project === 'chrome-beta', "Test is unreliable on chrome-beta");
const { myItemsFolderName } = openmctConfig;
// 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 +96,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 +176,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 });
}
/**

View File

@@ -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"]
@@ -139,13 +130,12 @@ test.describe('Tagging in Notebooks', () => {
await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible();
});
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"');

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -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');

View File

@@ -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();
}
}

View File

@@ -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);
}

View File

@@ -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 }) => {
@@ -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);

View 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"]');
}

View File

@@ -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}')`);
});
});

View 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();
}

View File

@@ -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}')`);
});
});

View File

@@ -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.fixme('Visual - Default Condition Set', 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.fixme('Visual - Default Condition Widget', 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.fixme('Visual - Save Successful Banner', 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.fixme('Visual - Default Gauge is correct', 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}')`);
});
});

View 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}')`);
});
});

View File

@@ -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}')`);
});
});

View File

@@ -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

View 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
};

View File

@@ -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.8.1",
"@percy/playwright": "1.0.4",
"@playwright/test": "1.23.0",
"@types/eventemitter3": "^1.0.0",
@@ -26,7 +26,7 @@
"eslint": "8.18.0",
"eslint-plugin-compat": "4.0.2",
"eslint-plugin-playwright": "0.10.0",
"eslint-plugin-vue": "9.1.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",
@@ -34,7 +34,7 @@
"git-rev-sync": "3.0.2",
"html2canvas": "1.4.1",
"imports-loader": "0.8.0",
"jasmine-core": "4.2.0",
"jasmine-core": "4.3.0",
"jsdoc": "3.6.11",
"karma": "6.3.20",
"karma-chrome-launcher": "3.1.1",
@@ -90,11 +90,12 @@
"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",

View File

@@ -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;
}
}

View File

@@ -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);
});

View File

@@ -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();

View File

@@ -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

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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();
}
}
/**

View File

@@ -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 = {};

View File

@@ -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();

View File

@@ -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;

View File

@@ -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(() => {

View File

@@ -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;

View File

@@ -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]);

View File

@@ -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();
});
});
});

View 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>

View File

@@ -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'
});
}
},

View File

@@ -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 {

View File

@@ -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();

View File

@@ -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
};
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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();
});
});

View File

@@ -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);
},

View File

@@ -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');
}

View File

@@ -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);

View 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

View File

@@ -217,9 +217,11 @@ class CouchObjectProvider {
this.indicator.setIndicatorToState(DISCONNECTED);
console.error(error.message);
throw new Error(`CouchDB Error - No response"`);
}
} else {
console.error(error.message);
console.error(error.message);
throw error;
}
}
}
@@ -287,7 +289,7 @@ class CouchObjectProvider {
this.objectQueue[key] = new CouchObjectQueue(undefined, response[REV]);
}
if (isNotebookType(object)) {
if (isNotebookType(object) || object.type === 'annotation') {
//Temporary measure until object sync is supported for all object types
//Always update notebook revision number because we have realtime sync, so always assume it's the latest.
this.objectQueue[key].updateRevision(response[REV]);
@@ -653,7 +655,6 @@ class CouchObjectProvider {
let document = new CouchDocument(key, queued.model);
document.metadata.created = Date.now();
this.request(key, "PUT", document).then((response) => {
console.log('create check response', key);
this.#checkResponse(response, queued.intermediateResponse, key);
}).catch(error => {
queued.intermediateResponse.reject(error);

View File

@@ -1,52 +1,145 @@
# Introduction
These instructions are for setting up CouchDB for a **development** environment. For a production environment, we recommend running Open MCT behind a proxy server (e.g., Nginx or Apache), and securing the CouchDB server properly:
https://docs.couchdb.org/en/main/intro/security.html
# Installing CouchDB
## macOS
### Installing with admin privileges to your computer
## Introduction
These instructions are for setting up CouchDB for a **development** environment. For a production environment, we recommend running Open MCT behind a proxy server (e.g., Nginx or Apache), and securing the CouchDB server properly:
<https://docs.couchdb.org/en/main/intro/security.html>
## Docker Quickstart
The following process is the preferred way of using CouchDB as it is automatic and closely resembles a production environment.
Requirement:
Get docker compose (or recent version of docker) installed on your machine. We recommend [Docker Desktop](https://www.docker.com/products/docker-desktop/)
1. Open a terminal to this current working directory (`cd openmct/src/plugins/persistence/couch`)
2. Create and start the `couchdb` container:
```sh
docker compose -f ./couchdb-compose.yaml up --detach
```
3. Copy `.env.ci` file to file named `.env.local`
4. (Optional) Change the values of `.env.local` if desired
5. Set the environment variables in bash by sourcing the env file
```sh
export $(cat .env.local | xargs)
```
6. Execute the configuration script:
```sh
sh ./setup-couchdb.sh
```
7. `cd` to the workspace root directory (the same directory as `index.html`)
8. Update `index.html` to use the CouchDB plugin as persistence store:
```sh
sh ./src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh
```
9. ✅ Done!
Open MCT will now use your local CouchDB container as its persistence store. Access the CouchDB instance manager by visiting <http://localhost:5984/_utils>.
## macOS
While we highly recommend using the CouchDB docker-compose installation, it is still possible to install CouchDB through other means.
### Installing CouchDB
1. Install CouchDB using: `brew install couchdb`.
2. Edit `/usr/local/etc/local.ini` and add the following settings:
```
```txt
[admins]
admin = youradminpassword
```
And set the server up for single node:
```
```txt
[couchdb]
single_node=true
```
Enable CORS
```
```txt
[chttpd]
enable_cors = true
[cors]
origins = http://localhost:8080
```
### Installing without admin privileges to your computer
1. Install CouchDB following these instructions: https://docs.brew.sh/Installation#untar-anywhere.
### Installing CouchDB without admin privileges to your computer
If `brew` is not available on your mac machine, you'll need to get the CouchDB installed using the official sourcefiles.
1. Install CouchDB following these instructions: <https://docs.brew.sh/Installation#untar-anywhere>.
1. Edit `local.ini` in Homebrew's `/etc/` directory as directed above in the 'Installing with admin privileges to your computer' section.
## Other Operating Systems
Follow the installation instructions from the CouchDB installation guide: https://docs.couchdb.org/en/stable/install/index.html
Follow the installation instructions from the CouchDB installation guide: <https://docs.couchdb.org/en/stable/install/index.html>
# Configuring CouchDB
## Configuration script
The simplest way to config a CouchDB instance is to use our provided tooling:
1. Copy `.env.ci` file to file named `.env.local`
2. Set the environment variables in bash by sourcing the env file
```sh
export $(cat .env.local | xargs)
```
3. Execute the configuration script:
```sh
sh ./setup-couchdb.sh
```
## Manual Configuration
1. Start CouchDB by running: `couchdb`.
2. Add the `_global_changes` database using `curl` (note the `youradminpassword` should be changed to what you set above 👆): `curl -X PUT http://admin:youradminpassword@127.0.0.1:5984/_global_changes`
3. Navigate to http://localhost:5984/_utils
3. Navigate to <http://localhost:5984/_utils>
4. Create a database called `openmct`
5. Navigate to http://127.0.0.1:5984/_utils/#/database/openmct/permissions
5. Navigate to <http://127.0.0.1:5984/_utils/#/database/openmct/permissions>
6. Remove permission restrictions in CouchDB from Open MCT by deleting `_admin` roles for both `Admin` and `Member`.
# Configuring Open MCT
# Configuring Open MCT to use CouchDB
## Configuration script
The simplest way to config a CouchDB instance is to use our provided tooling:
1. `cd` to the workspace root directory (the same directory as `index.html`)
2. Update `index.html` to use the CouchDB plugin as persistence store:
```sh
sh ./src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh
```
## Manual Configuration
1. Edit `openmct/index.html` comment out the following line:
```
openmct.install(openmct.plugins.LocalStorage());
```
Add a line to install the CouchDB plugin for Open MCT:
```
openmct.install(openmct.plugins.CouchDB("http://localhost:5984/openmct"));
```
2. Start Open MCT by running `npm start` in the `openmct` path.
3. Navigate to http://localhost:8080/ and create a random object in Open MCT (e.g., a 'Clock') and save. You may get an error saying that the object failed to persist - this is a known error that you can ignore, and will only happen the first time you save - just try again.
4. Navigate to: http://127.0.0.1:5984/_utils/#database/openmct/_all_docs
5. Look at the 'JSON' tab and ensure you can see the specific object you created above.
6. All done! 🏆
```js
openmct.install(openmct.plugins.LocalStorage());
```
Add a line to install the CouchDB plugin for Open MCT:
```js
openmct.install(openmct.plugins.CouchDB("http://localhost:5984/openmct"));
```
# Validating a successful Installation
1. Start Open MCT by running `npm start` in the `openmct` path.
2. Navigate to <http://localhost:8080/> and create a random object in Open MCT (e.g., a 'Clock') and save. You may get an error saying that the object failed to persist - this is a known error that you can ignore, and will only happen the first time you save - just try again.
3. Navigate to: <http://127.0.0.1:5984/_utils/#database/openmct/_all_docs>
4. Look at the 'JSON' tab and ensure you can see the specific object you created above.
5. All done! 🏆

View File

@@ -0,0 +1,14 @@
version: "3"
services:
couchdb:
image: couchdb:${COUCHDB_IMAGE_TAG:-3.2.1}
ports:
- "5984:5984"
- "5986:5986"
volumes:
- couchdb:/opt/couchdb/data
environment:
COUCHDB_USER: admin
COUCHDB_PASSWORD: password
volumes:
couchdb:

View File

@@ -0,0 +1,3 @@
#!/bin/bash -e
sed -i'.bak' -e 's/LocalStorage()/CouchDB("http:\/\/localhost:5984\/openmct")/g' index.html

View File

@@ -0,0 +1,125 @@
#!/bin/bash -e
# Do a couple checks for environment variables we expect to have a value.
if [ -z "${OPENMCT_DATABASE_NAME}" ] ; then
echo "OPENMCT_DATABASE_NAME has no value" 1>&2
exit 1
fi
if [ -z "${COUCH_ADMIN_USER}" ] ; then
echo "COUCH_ADMIN_USER has no value" 1>&2
exit 1
fi
if [ -z "${COUCH_BASE_LOCAL}" ] ; then
echo "COUCH_BASE_LOCAL has no value" 1>&2
exit 1
fi
# Come up with what we'll be providing to curl's -u option. Always supply the username from the environment,
# and optionally supply the password from the environment, if it has a value.
CURL_USERPASS_ARG="${COUCH_ADMIN_USER}"
if [ "${COUCH_ADMIN_PASSWORD}" ] ; then
CURL_USERPASS_ARG+=":${COUCH_ADMIN_PASSWORD}"
fi
system_tables_exist () {
resource_exists $COUCH_BASE_LOCAL/_users
}
create_users_db () {
curl -su "${CURL_USERPASS_ARG}" -X PUT $COUCH_BASE_LOCAL/_users
}
create_replicator_db () {
curl -su "${CURL_USERPASS_ARG}" -X PUT $COUCH_BASE_LOCAL/_replicator
}
setup_system_tables () {
users_db_response=$(create_users_db)
if [ "{\"ok\":true}" == "${users_db_response}" ]; then
echo Successfully created users db
replicator_db_response=$(create_replicator_db)
if [ "{\"ok\":true}" == "${replicator_db_response}" ]; then
echo Successfully created replicator DB
else
echo Unable to create replicator DB
fi
else
echo Unable to create users db
fi
}
resource_exists () {
response=$(curl -u "${CURL_USERPASS_ARG}" -s -o /dev/null -I -w "%{http_code}" $1);
if [ "200" == "${response}" ]; then
echo "TRUE"
else
echo "FALSE";
fi
}
db_exists () {
resource_exists $COUCH_BASE_LOCAL/$OPENMCT_DATABASE_NAME
}
create_db () {
response=$(curl -su "${CURL_USERPASS_ARG}" -XPUT $COUCH_BASE_LOCAL/$OPENMCT_DATABASE_NAME);
echo $response
}
admin_user_exists () {
response=$(curl -su "${CURL_USERPASS_ARG}" -o /dev/null -I -w "%{http_code}" $COUCH_BASE_LOCAL/_node/$COUCH_NODE_NAME/_config/admins/$COUCH_ADMIN_USER);
if [ "200" == "${response}" ]; then
echo "TRUE"
else
echo "FALSE";
fi
}
create_admin_user () {
echo Creating admin user
curl -X PUT $COUCH_BASE_LOCAL/_node/$COUCH_NODE_NAME/_config/admins/$COUCH_ADMIN_USER -d \'"$COUCH_ADMIN_PASSWORD"\'
}
if [ "$(admin_user_exists)" == "FALSE" ]; then
echo "Admin user does not exist, creating..."
create_admin_user
else
echo "Admin user exists"
fi
if [ "TRUE" == $(system_tables_exist) ]; then
echo System tables exist, skipping creation
else
echo Is fresh install, creating system tables
setup_system_tables
fi
if [ "FALSE" == $(db_exists) ]; then
response=$(create_db)
if [ "{\"ok\":true}" == "${response}" ]; then
echo Database successfully created
else
echo Database creation failed
fi
else
echo Database already exists, nothing to do
fi
echo "Updating _replicator database permissions"
response=$(curl -su "${CURL_USERPASS_ARG}" --location --request PUT $COUCH_BASE_LOCAL/_replicator/_security --header 'Content-Type: application/json' --data-raw '{ "admins": {"roles": []},"members": {"roles": []}}');
if [ "{\"ok\":true}" == "${response}" ]; then
echo "Database permissions successfully updated"
else
echo "Database permissions not updated"
fi
echo "Updating ${OPENMCT_DATABASE_NAME} database permissions"
response=$(curl -su "${CURL_USERPASS_ARG}" --location --request PUT $COUCH_BASE_LOCAL/$OPENMCT_DATABASE_NAME/_security --header 'Content-Type: application/json' --data-raw '{ "admins": {"roles": []},"members": {"roles": []}}');
if [ "{\"ok\":true}" == "${response}" ]; then
echo "Database permissions successfully updated"
else
echo "Database permissions not updated"
fi

View File

@@ -32,7 +32,7 @@ define([
'./autoflow/AutoflowTabularPlugin',
'./timeConductor/plugin',
'../../example/imagery/plugin',
'../../example/faultManagment/exampleFaultSource',
'../../example/faultManagement/exampleFaultSource',
'./imagery/plugin',
'./summaryWidget/plugin',
'./URLIndicatorPlugin/URLIndicatorPlugin',

View File

@@ -39,7 +39,7 @@
const DEFAULT_DURATION_FORMATTER = 'duration';
const LOCAL_STORAGE_HISTORY_KEY_FIXED = 'tcHistory';
const LOCAL_STORAGE_HISTORY_KEY_REALTIME = 'tcHistoryRealtime';
const DEFAULT_RECORDS = 10;
const DEFAULT_RECORDS_LENGTH = 10;
import { millisecondsToDHMS } from "utils/duration";
import UTCTimeFormat from "../utcTimeSystem/UTCTimeFormat.js";
@@ -79,16 +79,14 @@ export default {
* @timespans {start, end} number representing timestamp
*/
fixedHistory: {},
presets: []
presets: [],
isFixed: this.openmct.time.clock() === undefined
};
},
computed: {
currentHistory() {
return this.mode + 'History';
},
isFixed() {
return this.openmct.time.clock() === undefined;
},
historyForCurrentTimeSystem() {
const history = this[this.currentHistory][this.timeSystem.key];
@@ -96,7 +94,7 @@ export default {
},
storageKey() {
let key = LOCAL_STORAGE_HISTORY_KEY_FIXED;
if (this.mode !== 'fixed') {
if (!this.isFixed) {
key = LOCAL_STORAGE_HISTORY_KEY_REALTIME;
}
@@ -108,6 +106,7 @@ export default {
handler() {
// only for fixed time since we track offsets for realtime
if (this.isFixed) {
this.updateMode();
this.addTimespan();
}
},
@@ -115,28 +114,35 @@ export default {
},
offsets: {
handler() {
this.updateMode();
this.addTimespan();
},
deep: true
},
timeSystem: {
handler(ts) {
this.updateMode();
this.loadConfiguration();
this.addTimespan();
},
deep: true
},
mode: function () {
this.getHistoryFromLocalStorage();
this.initializeHistoryIfNoHistory();
this.updateMode();
this.loadConfiguration();
}
},
mounted() {
this.updateMode();
this.getHistoryFromLocalStorage();
this.initializeHistoryIfNoHistory();
},
methods: {
updateMode() {
this.isFixed = this.openmct.time.clock() === undefined;
this.getHistoryFromLocalStorage();
this.initializeHistoryIfNoHistory();
},
getHistoryMenuItems() {
const history = this.historyForCurrentTimeSystem.map(timespan => {
let name;
@@ -203,8 +209,8 @@ export default {
currentHistory = currentHistory.filter(ts => !(ts.start === timespan.start && ts.end === timespan.end));
currentHistory.unshift(timespan); // add to front
if (currentHistory.length > this.records) {
currentHistory.length = this.records;
if (currentHistory.length > this.MAX_RECORDS_LENGTH) {
currentHistory.length = this.MAX_RECORDS_LENGTH;
}
this.$set(this[this.currentHistory], key, currentHistory);
@@ -231,7 +237,7 @@ export default {
.filter(option => option.timeSystem === this.timeSystem.key);
this.presets = this.loadPresets(configurations);
this.records = this.loadRecords(configurations);
this.MAX_RECORDS_LENGTH = this.loadRecords(configurations);
},
loadPresets(configurations) {
const configuration = configurations.find(option => {
@@ -243,9 +249,9 @@ export default {
},
loadRecords(configurations) {
const configuration = configurations.find(option => option.records);
const records = configuration ? configuration.records : DEFAULT_RECORDS;
const maxRecordsLength = configuration ? configuration.records : DEFAULT_RECORDS_LENGTH;
return records;
return maxRecordsLength;
},
formatTime(time) {
let format = this.timeSystem.timeFormat;

View File

@@ -131,15 +131,15 @@ describe('time conductor', () => {
describe('duration functions', () => {
it('should transform milliseconds to DHMS', () => {
const functionResults = [millisecondsToDHMS(0), millisecondsToDHMS(86400000),
millisecondsToDHMS(129600000), millisecondsToDHMS(661824000)];
const validResults = [' ', '+ 1d', '+ 1d 12h', '+ 7d 15h 50m 24s'];
millisecondsToDHMS(129600000), millisecondsToDHMS(661824000), millisecondsToDHMS(213927028)];
const validResults = [' ', '+ 1d', '+ 1d 12h', '+ 7d 15h 50m 24s', '+ 2d 11h 25m 27s 28ms'];
expect(validResults).toEqual(functionResults);
});
it('should get precise duration', () => {
const functionResults = [getPreciseDuration(0), getPreciseDuration(643680000),
getPreciseDuration(1605312000)];
const validResults = ['00:00:00:00', '07:10:48:00', '18:13:55:12'];
getPreciseDuration(1605312000), getPreciseDuration(213927028)];
const validResults = ['00:00:00:00:000', '07:10:48:00:000', '18:13:55:12:000', '02:11:25:27:028'];
expect(validResults).toEqual(functionResults);
});
});

View File

@@ -188,7 +188,8 @@ export default {
if (domainObject.type === 'plan') {
this.getPlanDataAndSetConfig({
...this.domainObject,
selectFile: domainObject.selectFile
selectFile: domainObject.selectFile,
sourceMap: domainObject.sourceMap
});
}
},

View File

@@ -32,7 +32,7 @@
<div
v-if="canEdit"
class="c-inspect-properties__hint span-all"
>These settings are not previewed and will be applied after editing is completed.</div>
>These settings don't affect the view while editing, but will be applied after editing is finished.</div>
<div
class="c-inspect-properties__label"
title="Sort order of the timelist."

View File

@@ -33,28 +33,16 @@ export default function () {
description: 'A configurable, time-ordered list view of activities for a compatible mission plan file.',
creatable: true,
cssClass: 'icon-timelist',
form: [
{
name: 'Upload Plan (JSON File)',
key: 'selectFile',
control: 'file-input',
text: 'Select File...',
type: 'application/json',
property: [
"selectFile"
]
}
],
initialize: function (domainObject) {
domainObject.configuration = {
sortOrderIndex: 0,
futureEventsIndex: 0,
futureEventsIndex: 1,
futureEventsDurationIndex: 0,
futureEventsDuration: 20,
currentEventsIndex: 1,
currentEventsDurationIndex: 0,
currentEventsDuration: 20,
pastEventsIndex: 0,
pastEventsIndex: 1,
pastEventsDurationIndex: 0,
pastEventsDuration: 20,
filter: ''

View File

@@ -95,14 +95,12 @@ describe('the plugin', function () {
originalRouterPath = openmct.router.path;
mockComposition = new EventEmitter();
mockComposition.load = () => {
mockComposition.emit('add', planObject);
return Promise.resolve([planObject]);
// eslint-disable-next-line require-await
mockComposition.load = async () => {
return [planObject];
};
spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
openmct.on('start', done);
openmct.start(appHolder);
});
@@ -268,6 +266,8 @@ describe('the plugin', function () {
});
it('loads the plan from composition', () => {
mockComposition.emit('add', planObject);
return Vue.nextTick(() => {
const items = element.querySelectorAll(LIST_ITEM_CLASS);
expect(items.length).toEqual(2);
@@ -319,6 +319,8 @@ describe('the plugin', function () {
});
it('activities', () => {
mockComposition.emit('add', planObject);
return Vue.nextTick(() => {
const items = element.querySelectorAll(LIST_ITEM_CLASS);
expect(items.length).toEqual(1);
@@ -370,6 +372,8 @@ describe('the plugin', function () {
});
it('hides past events', () => {
mockComposition.emit('add', planObject);
return Vue.nextTick(() => {
const items = element.querySelectorAll(LIST_ITEM_CLASS);
expect(items.length).toEqual(1);

View File

@@ -32,6 +32,12 @@
.c-list-item {
/* Time Lists */
td {
$p: $interiorMarginSm;
padding-top: $p;
padding-bottom: $p;
}
&.--is-current {
background-color: $colorCurrentBg;
border-top: 1px solid $colorCurrentBorder !important;

View File

@@ -25,13 +25,14 @@
/******************************************************** CONTROL-SPECIFIC MIXINS */
@mixin menuOuter() {
border-radius: $basicCr;
box-shadow: $shdwMenuInner, $shdwMenu;
box-shadow: $shdwMenu;
@if $shdwMenuInner != none {
box-shadow: $shdwMenuInner, $shdwMenu;
}
background: $colorMenuBg;
color: $colorMenuFg;
//filter: $filterMenu; // 2022: causing all kinds of weird visual bugs in Chrome
text-shadow: $shdwMenuText;
padding: $interiorMarginSm;
//box-shadow: $shdwMenu;
display: flex;
flex-direction: column;
position: absolute;
@@ -60,14 +61,13 @@
cursor: pointer;
display: flex;
padding: nth($menuItemPad, 1) nth($menuItemPad, 2);
transition: $transIn;
white-space: nowrap;
@include hover {
background: $colorMenuHovBg;
color: $colorMenuHovFg;
&:before {
color: $colorMenuHovIc;
color: $colorMenuHovIc !important;
}
}

View File

@@ -97,14 +97,17 @@ export default {
this.tagsChanged(this.annotation.tags);
},
deep: true
},
annotationQuery: {
handler() {
this.unloadAnnotation();
this.loadAnnotation();
},
deep: true
}
},
async mounted() {
this.annotation = await this.openmct.annotation.getAnnotation(this.annotationQuery, this.annotationSearchType);
this.addAnnotationListener(this.annotation);
if (this.annotation && this.annotation.tags) {
this.tagsChanged(this.annotation.tags);
}
mounted() {
this.loadAnnotation();
},
destroyed() {
if (this.removeTagsListener) {
@@ -114,7 +117,23 @@ export default {
methods: {
addAnnotationListener(annotation) {
if (annotation && !this.removeTagsListener) {
this.removeTagsListener = this.openmct.objects.observe(annotation, 'tags', this.tagsChanged);
this.removeTagsListener = this.openmct.objects.observe(annotation, '*', (newAnnotation) => {
this.tagsChanged(newAnnotation.tags);
this.annotation = newAnnotation;
});
}
},
async loadAnnotation() {
this.annotation = await this.openmct.annotation.getAnnotation(this.annotationQuery, this.annotationSearchType);
this.addAnnotationListener(this.annotation);
if (this.annotation && this.annotation.tags) {
this.tagsChanged(this.annotation.tags);
}
},
unloadAnnotation() {
if (this.removeTagsListener) {
this.removeTagsListener();
this.removeTagsListener = undefined;
}
},
tagsChanged(newTags) {
@@ -133,8 +152,11 @@ export default {
this.addedTags.push(newTagValue);
this.userAddingTag = true;
},
tagRemoved(tagToRemove) {
return this.openmct.annotation.removeAnnotationTag(this.annotation, tagToRemove);
async tagRemoved(tagToRemove) {
const result = await this.openmct.annotation.removeAnnotationTag(this.annotation, tagToRemove);
this.$emit('tags-updated');
return result;
},
async tagAdded(newTag) {
const annotationWasCreated = this.annotation === null || this.annotation === undefined;
@@ -146,6 +168,8 @@ export default {
this.tagsChanged(this.annotation.tags);
this.userAddingTag = false;
this.$emit('tags-updated');
}
}
};

View File

@@ -53,6 +53,7 @@
type="horizontal"
>
<pane
id="tree-pane"
class="l-shell__pane-tree"
handle="after"
label="Browse"

View File

@@ -41,6 +41,8 @@
<div
ref="mainTree"
class="c-tree-and-search__tree c-tree"
role="tree"
aria-expanded="true"
>
<div>
@@ -467,7 +469,7 @@ export default {
}
},
scrollEndEvent() {
if (!this.$refs.srcrollable) {
if (!this.$refs.scrollable) {
return;
}
@@ -576,14 +578,17 @@ export default {
};
},
addTreeItemObserver(domainObject, parentObjectPath) {
if (this.observers[domainObject.identifier.key]) {
this.observers[domainObject.identifier.key]();
const objectPath = [domainObject].concat(parentObjectPath);
const navigationPath = this.buildNavigationPath(objectPath);
if (this.observers[navigationPath]) {
this.observers[navigationPath]();
}
this.observers[domainObject.identifier.key] = this.openmct.objects.observe(
this.observers[navigationPath] = this.openmct.objects.observe(
domainObject,
'name',
this.updateTreeItems.bind(this, parentObjectPath)
this.sortTreeItems.bind(this, parentObjectPath)
);
},
async updateTreeItems(parentObjectPath) {
@@ -610,6 +615,44 @@ export default {
}
}
},
sortTreeItems(parentObjectPath) {
const navigationPath = this.buildNavigationPath(parentObjectPath);
const parentItem = this.getTreeItemByPath(navigationPath);
// If the parent is not sortable, skip sorting
if (!this.isSortable(parentObjectPath)) {
return;
}
// Sort the renamed object and its siblings (direct descendants of the parent)
const directDescendants = this.getChildrenInTreeFor(parentItem, false);
directDescendants.sort(this.sortNameAscending);
// Take a copy of the sorted descendants array
const sortedTreeItems = directDescendants.slice();
directDescendants.forEach(descendant => {
const parent = this.getTreeItemByPath(descendant.navigationPath);
// If descendant is not open, skip
if (!this.isTreeItemOpen(parent)) {
return;
}
// If descendant is open but has no children, skip
const children = this.getChildrenInTreeFor(parent, true);
if (children.length === 0) {
return;
}
// Splice in the children of the descendant
const parentIndex = sortedTreeItems.map(item => item.navigationPath).indexOf(parent.navigationPath);
sortedTreeItems.splice(parentIndex + 1, 0, ...children);
});
// Splice in all of the sorted descendants
this.treeItems.splice(this.treeItems.indexOf(parentItem) + 1, sortedTreeItems.length, ...sortedTreeItems);
},
buildNavigationPath(objectPath) {
return '/browse/' + [...objectPath].reverse()
.map((object) => this.openmct.objects.makeKeyString(object.identifier))

View File

@@ -77,7 +77,6 @@ export default {
}
this.searchValue = value;
this.searchLoading = true;
// clear any previous search results
this.annotationSearchResults = [];
this.objectSearchResults = [];
@@ -85,8 +84,13 @@ export default {
if (this.searchValue) {
await this.getSearchResults();
} else {
this.searchLoading = false;
this.$refs.searchResultsDropDown.showResults(this.annotationSearchResults, this.objectSearchResults);
const dropdownOptions = {
searchLoading: this.searchLoading,
searchValue: this.searchValue,
annotationSearchResults: this.annotationSearchResults,
objectSearchResults: this.objectSearchResults
};
this.$refs.searchResultsDropDown.showResults(dropdownOptions);
}
},
getPathsForObjects(objectsNeedingPaths) {
@@ -103,6 +107,8 @@ export default {
async getSearchResults() {
// an abort controller will be passed in that will be used
// to cancel an active searches if necessary
this.searchLoading = true;
this.$refs.searchResultsDropDown.showSearchStarted();
this.abortSearchController = new AbortController();
const abortSignal = this.abortSearchController.signal;
try {
@@ -110,10 +116,15 @@ export default {
const fullObjectSearchResults = await Promise.all(this.openmct.objects.search(this.searchValue, abortSignal));
const aggregatedObjectSearchResults = fullObjectSearchResults.flat();
const aggregatedObjectSearchResultsWithPaths = await this.getPathsForObjects(aggregatedObjectSearchResults);
const filterAnnotations = aggregatedObjectSearchResultsWithPaths.filter(result => {
return result.type !== 'annotation';
const filterAnnotationsAndValidPaths = aggregatedObjectSearchResultsWithPaths.filter(result => {
if (this.openmct.annotation.isAnnotation(result)) {
return false;
}
return this.openmct.objects.isReachable(result?.originalPath);
});
this.objectSearchResults = filterAnnotations;
this.objectSearchResults = filterAnnotationsAndValidPaths;
this.searchLoading = false;
this.showSearchResults();
} catch (error) {
console.error(`😞 Error searching`, error);
@@ -125,7 +136,13 @@ export default {
}
},
showSearchResults() {
this.$refs.searchResultsDropDown.showResults(this.annotationSearchResults, this.objectSearchResults);
const dropdownOptions = {
searchLoading: this.searchLoading,
searchValue: this.searchValue,
annotationSearchResults: this.annotationSearchResults,
objectSearchResults: this.objectSearchResults
};
this.$refs.searchResultsDropDown.showResults(dropdownOptions);
document.body.addEventListener('click', this.handleOutsideClick);
},
handleOutsideClick(event) {

View File

@@ -39,7 +39,11 @@ describe("GrandSearch", () => {
let mockAnnotationObject;
let mockDisplayLayout;
let mockFolderObject;
let mockAnotherFolderObject;
let mockTopObject;
let originalRouterPath;
let mockNewObject;
let mockObjectProvider;
beforeEach((done) => {
openmct = createOpenMct();
@@ -53,6 +57,7 @@ describe("GrandSearch", () => {
mockDomainObject = {
type: 'notebook',
name: 'fooRabbitNotebook',
location: 'fooNameSpace:topObject',
identifier: {
key: 'some-object',
namespace: 'fooNameSpace'
@@ -70,17 +75,39 @@ describe("GrandSearch", () => {
}
}
};
mockTopObject = {
type: 'root',
name: 'Top Folder',
composition: [],
identifier: {
key: 'topObject',
namespace: 'fooNameSpace'
}
};
mockAnotherFolderObject = {
type: 'folder',
name: 'Another Test Folder',
composition: [],
location: 'fooNameSpace:topObject',
identifier: {
key: 'someParent',
namespace: 'fooNameSpace'
}
};
mockFolderObject = {
type: 'folder',
name: 'Test Folder',
composition: [],
location: 'fooNameSpace:someParent',
identifier: {
key: 'some-folder',
key: 'someFolder',
namespace: 'fooNameSpace'
}
};
mockDisplayLayout = {
type: 'layout',
name: 'Bar Layout',
composition: [],
identifier: {
key: 'some-layout',
namespace: 'fooNameSpace'
@@ -105,9 +132,19 @@ describe("GrandSearch", () => {
}
}
};
mockNewObject = {
type: 'folder',
name: 'New Apple Test Folder',
composition: [],
location: 'fooNameSpace:topObject',
identifier: {
key: 'newApple',
namespace: 'fooNameSpace'
}
};
openmct.router.isNavigatedObject = jasmine.createSpy().and.returnValue(false);
const mockObjectProvider = jasmine.createSpyObj("mock object provider", [
mockObjectProvider = jasmine.createSpyObj("mock object provider", [
"create",
"update",
"get"
@@ -122,6 +159,12 @@ describe("GrandSearch", () => {
return mockDisplayLayout;
} else if (identifier.key === mockFolderObject.identifier.key) {
return mockFolderObject;
} else if (identifier.key === mockAnotherFolderObject.identifier.key) {
return mockAnotherFolderObject;
} else if (identifier.key === mockTopObject.identifier.key) {
return mockTopObject;
} else if (identifier.key === mockNewObject.identifier.key) {
return mockNewObject;
} else {
return null;
}
@@ -144,6 +187,7 @@ describe("GrandSearch", () => {
// use local worker
sharedWorkerToRestore = openmct.objects.inMemorySearchProvider.worker;
openmct.objects.inMemorySearchProvider.worker = null;
await openmct.objects.inMemorySearchProvider.index(mockTopObject);
await openmct.objects.inMemorySearchProvider.index(mockDomainObject);
await openmct.objects.inMemorySearchProvider.index(mockDisplayLayout);
await openmct.objects.inMemorySearchProvider.index(mockFolderObject);
@@ -172,6 +216,7 @@ describe("GrandSearch", () => {
openmct.objects.inMemorySearchProvider.worker = sharedWorkerToRestore;
openmct.router.path = originalRouterPath;
grandSearchComponent.$destroy();
document.body.removeChild(parent);
return resetApplicationState(openmct);
});
@@ -179,25 +224,62 @@ describe("GrandSearch", () => {
it("should render an object search result", async () => {
await grandSearchComponent.$children[0].searchEverything('foo');
await Vue.nextTick();
const searchResult = document.querySelector('[aria-label="fooRabbitNotebook notebook result"]');
expect(searchResult).toBeDefined();
const searchResults = document.querySelectorAll('[aria-label="fooRabbitNotebook notebook result"]');
expect(searchResults.length).toBe(1);
expect(searchResults[0].innerText).toContain('Rabbit');
});
it("should render an object search result if new object added", async () => {
const composition = openmct.composition.get(mockFolderObject);
composition.add(mockNewObject);
await grandSearchComponent.$children[0].searchEverything('apple');
await Vue.nextTick();
const searchResults = document.querySelectorAll('[aria-label="New Apple Test Folder folder result"]');
expect(searchResults.length).toBe(1);
expect(searchResults[0].innerText).toContain('Apple');
});
it("should not use InMemorySearch provider if object provider provides search", async () => {
// eslint-disable-next-line require-await
mockObjectProvider.search = async (query, abortSignal, searchType) => {
if (searchType === openmct.objects.SEARCH_TYPES.OBJECTS) {
return mockNewObject;
} else {
return [];
}
};
mockObjectProvider.supportsSearchType = (someType) => {
return true;
};
const composition = openmct.composition.get(mockFolderObject);
composition.add(mockNewObject);
await grandSearchComponent.$children[0].searchEverything('apple');
await Vue.nextTick();
const searchResults = document.querySelectorAll('[aria-label="New Apple Test Folder folder result"]');
// This will be of length 2 (doubles) if we're incorrectly searching with InMemorySearchProvider as well
expect(searchResults.length).toBe(1);
expect(searchResults[0].innerText).toContain('Apple');
});
it("should render an annotation search result", async () => {
await grandSearchComponent.$children[0].searchEverything('S');
await Vue.nextTick();
const annotationResult = document.querySelector('[aria-label="Search Result"]');
expect(annotationResult).toBeDefined();
const annotationResults = document.querySelectorAll('[aria-label="Search Result"]');
expect(annotationResults.length).toBe(2);
expect(annotationResults[1].innerText).toContain('Driving');
});
it("should preview object search results in edit mode if object clicked", async () => {
await grandSearchComponent.$children[0].searchEverything('Folder');
grandSearchComponent._provided.openmct.router.path = [mockDisplayLayout];
await Vue.nextTick();
const searchResult = document.querySelector('[name="Test Folder"]');
expect(searchResult).toBeDefined();
searchResult.click();
const searchResults = document.querySelectorAll('[name="Test Folder"]');
expect(searchResults.length).toBe(1);
expect(searchResults[0].innerText).toContain('Folder');
searchResults[0].click();
const previewWindow = document.querySelector('.js-preview-window');
expect(previewWindow).toBeDefined();
expect(previewWindow.innerText).toContain('Snapshot');
});
});

View File

@@ -22,8 +22,6 @@
<template>
<div
v-if="(annotationResults && annotationResults.length) ||
(objectResults && objectResults.length)"
class="c-gsearch__dropdown"
>
<div
@@ -58,25 +56,40 @@
@click.native="selectedResult"
/>
</div>
<div
v-if="searchLoading"
> <progress-bar
:model="{progressText: 'Searching...',
progressPerc: undefined
}"
/>
</div>
<div
v-if="!searchLoading && (!annotationResults || !annotationResults.length) &&
(!objectResults || !objectResults.length)"
>No matching results.
</div>
</div>
</div>
</div>
</template>
</div></template>
<script>
import AnnotationSearchResult from './AnnotationSearchResult.vue';
import ObjectSearchResult from './ObjectSearchResult.vue';
import ProgressBar from '@/ui/components/ProgressBar.vue';
export default {
name: 'SearchResultsDropDown',
components: {
AnnotationSearchResult,
ObjectSearchResult
ObjectSearchResult,
ProgressBar
},
inject: ['openmct'],
data() {
return {
resultsShown: false,
searchLoading: false,
annotationResults: [],
objectResults: [],
previewVisible: false
@@ -91,12 +104,18 @@ export default {
previewChanged(changedPreviewState) {
this.previewVisible = changedPreviewState;
},
showResults(passedAnnotationResults, passedObjectResults) {
if ((passedAnnotationResults && passedAnnotationResults.length)
|| (passedObjectResults && passedObjectResults.length)) {
showSearchStarted() {
this.searchLoading = true;
this.resultsShown = true;
this.annotationResults = [];
this.objectResults = [];
},
showResults({searchLoading, searchValue, annotationSearchResults, objectSearchResults}) {
this.searchLoading = searchLoading;
this.annotationResults = annotationSearchResults;
this.objectResults = objectSearchResults;
if (searchValue?.length) {
this.resultsShown = true;
this.annotationResults = passedAnnotationResults;
this.objectResults = passedObjectResults;
} else {
this.resultsShown = false;
}

View File

@@ -1,7 +1,9 @@
<template>
<div
:style="treeItemStyles"
class="c-tree__item-h"
role="treeitem"
:style="treeItemStyles"
:aria-expanded="(!activeSearch && hasComposition) ? (isOpen || isLoading) ? 'true' : 'false' : undefined"
>
<div
class="c-tree__item"

View File

@@ -32,8 +32,16 @@ function normalizeAge(num) {
return isWhole ? hundredtized / 100 : num;
}
function padLeadingZeros(num, numOfLeadingZeros) {
return num.toString().padStart(numOfLeadingZeros, '0');
}
function toDoubleDigits(num) {
return num >= 10 ? num : `0${num}`;
return padLeadingZeros(num, 2);
}
function toTripleDigits(num) {
return padLeadingZeros(num, 3);
}
function addTimeSuffix(value, suffix) {
@@ -46,7 +54,8 @@ export function millisecondsToDHMS(numericDuration) {
addTimeSuffix(Math.floor(normalizeAge(ms / ONE_DAY)), 'd'),
addTimeSuffix(Math.floor(normalizeAge((ms % ONE_DAY) / ONE_HOUR)), 'h'),
addTimeSuffix(Math.floor(normalizeAge((ms % ONE_HOUR) / ONE_MINUTE)), 'm'),
addTimeSuffix(Math.floor(normalizeAge((ms % ONE_MINUTE) / ONE_SECOND)), 's')
addTimeSuffix(Math.floor(normalizeAge((ms % ONE_MINUTE) / ONE_SECOND)), 's'),
addTimeSuffix(Math.floor(normalizeAge(ms % ONE_SECOND)), "ms")
].filter(Boolean).join(' ');
return `${ dhms ? '+' : ''} ${dhms}`;
@@ -59,7 +68,8 @@ export function getPreciseDuration(value) {
toDoubleDigits(Math.floor(normalizeAge(ms / ONE_DAY))),
toDoubleDigits(Math.floor(normalizeAge((ms % ONE_DAY) / ONE_HOUR))),
toDoubleDigits(Math.floor(normalizeAge((ms % ONE_HOUR) / ONE_MINUTE))),
toDoubleDigits(Math.floor(normalizeAge((ms % ONE_MINUTE) / ONE_SECOND)))
toDoubleDigits(Math.floor(normalizeAge((ms % ONE_MINUTE) / ONE_SECOND))),
toTripleDigits(Math.floor(normalizeAge(ms % ONE_SECOND)))
].join(":");
}

View File

@@ -2,7 +2,6 @@
// instrumentation using babel-plugin-istanbul (see babel.coverage.js)
const config = require('./webpack.dev');
const path = require('path');
const vueLoaderRule = config.module.rules.find(r => r.use === 'vue-loader');
// eslint-disable-next-line no-undef
const CI = process.env.CI === 'true';
@@ -34,8 +33,11 @@ config.module.rules.push({
use: {
loader: 'babel-loader',
options: {
retainLines: true,
// eslint-disable-next-line no-undef
configFile: path.resolve(process.cwd(), 'babel.coverage.js')
plugins: [['babel-plugin-istanbul', {
extension: ['.js', '.vue']
}]]
}
}
});