Compare commits

..

16 Commits

Author SHA1 Message Date
John Hill
263cc695cb Merge branch 'release/2.0.8' of https://github.com/nasa/openmct into gds-tests-1 2022-08-11 06:05:13 -07:00
John Hill
dc1ffac27b add suite 2022-08-11 06:04:25 -07:00
John Hill
4e51d7830a rename 2022-08-11 06:04:18 -07: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
75 changed files with 456 additions and 2141 deletions

View File

@@ -7,13 +7,12 @@ updates:
interval: "daily"
open-pull-requests-limit: 10
labels:
- "pr:e2e"
- "type:maintenance"
- "dependencies"
- "pr:e2e"
- "pr:daveit"
- "pr:visual"
- "pr:platform"
ignore:
- dependency-name: "@playwright/test" #we source the container instead of the dependency in CI
- package-ecosystem: "github-actions"
directory: "/"

View File

@@ -1,37 +0,0 @@
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

6
API.md
View File

@@ -390,7 +390,7 @@ A telemetry object is a domain object with a telemetry property. To take an exa
{
"key": "value",
"name": "Value",
"unit": "kilograms",
"units": "kilograms",
"format": "float",
"min": 0,
"max": 100,
@@ -425,7 +425,7 @@ attribute | type | flags | notes
`name` | string | optional | a human readable label for this field. If omitted, defaults to `key`.
`source` | string | optional | identifies the property of a datum where this value is stored. If omitted, defaults to `key`.
`format` | string | optional | a specific format identifier, mapping to a formatter. If omitted, uses a default formatter. For enumerations, use `enum`. For timestamps, use `utc` if you are using utc dates, otherwise use a key mapping to your custom date format.
`unit` | string | optional | the unit of this value, e.g. `km`, `seconds`, `parsecs`
`units` | string | optional | the units of this value, e.g. `km`, `seconds`, `parsecs`
`min` | number | optional | the minimum possible value of this measurement. Will be used by plots, gauges, etc to automatically set a min value.
`max` | number | optional | the maximum possible value of this measurement. Will be used by plots, gauges, etc to automatically set a max value.
`enumerations` | array | optional | for objects where `format` is `"enum"`, this array tracks all possible enumerations of the value. Each entry in this array is an object, with a `value` property that is the numerical value of the enumeration, and a `string` property that is the text value of the enumeration. ex: `{"value": 0, "string": "OFF"}`. If you use an enumerations array, `min` and `max` will be set automatically for you.
@@ -1082,4 +1082,4 @@ View provider Example:
return openmct.priority.HIGH;
}
}
```
```

View File

@@ -102,15 +102,20 @@ async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine
/**
* Open the given `domainObject`'s context menu from the object tree.
* Expands the path to the object and scrolls to it if necessary.
* Expands the 'My Items' folder if it is not already expanded.
*
* @param {import('@playwright/test').Page} page
* @param {string} url the url to the object
* @param {string} myItemsFolderName the name of the "My Items" folder
* @param {string} domainObjectName the display name of the `domainObject`
*/
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({
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({
button: 'right'
});
}
@@ -124,7 +129,7 @@ async function openObjectTreeContextMenu(page, url) {
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);
return window.location.href.match(regexp).at(-1);
}, UUIDv4Regexp);
return focusedObjectUuid;

View File

@@ -1,28 +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 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

@@ -1,30 +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 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

@@ -1,28 +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 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());
});

View File

@@ -1,277 +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.
*****************************************************************************/
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

@@ -1,108 +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 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

@@ -28,8 +28,7 @@ const { test, expect } = require('../../../../baseFixtures');
test.describe('Sine Wave Generator', () => {
test('Create new Sine Wave Generator Object and validate create Form Logic', async ({ page, browserName }) => {
// eslint-disable-next-line playwright/no-skipped-test
test.skip(browserName === 'firefox', 'This test needs to be updated to work with firefox');
test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox');
//Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' });

View File

@@ -50,7 +50,7 @@ test.describe('Clock Generator CRUD Operations', () => {
await page.locator('.icon-arrow-down').click();
// Verify clicking on the autocomplete arrow collapses the dropdown
await expect(page.locator(".c-input--autocomplete__options")).toBeHidden();
await expect(page.locator(".c-input--autocomplete__options")).not.toBeVisible();
// Click timezone input to open dropdown
await page.locator('.c-input--autocomplete__input').click();
@@ -60,7 +60,7 @@ test.describe('Clock Generator CRUD Operations', () => {
// Verify clicking outside the autocomplete dropdown collapses it
await page.locator('text=Timezone').click();
// Verify clicking on the autocomplete arrow collapses the dropdown
await expect(page.locator(".c-input--autocomplete__options")).toBeHidden();
await expect(page.locator(".c-input--autocomplete__options")).not.toBeVisible();
});
});

View File

@@ -27,7 +27,6 @@ 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;
@@ -179,24 +178,3 @@ 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,98 @@
/*****************************************************************************
* 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');
const { createDomainObjectWithDefaults } = require('../../appActions');
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, { 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');
const newObjectName = "Renamed Timer";
//We've created an example of a shared function which pases the page and newObjectName values
await renameObjectFrom3DotMenu(page, newObjectName);
//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, { 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');
const newObjectName = "Renamed Timer";
const newObjectName2 = "Re-Renamed Timer";
//We've created an example of a shared function which pases the page and newObjectName values
await renameObjectFrom3DotMenu(page, newObjectName);
//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);
await renameObjectFrom3DotMenu(page, newObjectName2);
//Assert that the name has changed in the browser bar to the second value
await expect(page.locator('.l-browse-bar__object-name')).toContainText(newObjectName2);
});
//If you run out of time to write new tests, please stub in the missing tests in place with a test.fixme and BDD-style test steps. Someone will carry the baton!
test.fixme('Can Rename Timer Object from Tree', async ({ page }) => {
//Create a new object
//Copy this object
//Delete first object
//Expect copied object to persist
});
});
//Structure: custom functions should be declared last. We are leaning on JSDoc pretty heavily to describe functionality. It is not required, but heavily recommended.
/**
* This is an example of a function which is shared between testcases in this test suite. When refactoring, we'll be looking
* for common functionality which makes sense to generalize for the entire test framework.
* @param {import('@playwright/test').Page} page
* @param {string} newNameForTimer New Name for object
*/
async function renameObjectFrom3DotMenu(page, newNameForTimer) {
// Click on 3 Dot Menu
await page.locator('button[title="More options"]').click();
// Click text=Edit Properties...
await page.locator('text=Edit Properties...').click();
// Rename the object with newNameForTimer variable which is passed into this function
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(newNameForTimer);
// Click Ok button to Save
await page.locator('text=OK').click();
}

View File

@@ -93,70 +93,6 @@ test.describe('Testing Display Layout @unstable', () => {
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);
});
});
/**

View File

@@ -1,237 +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.
*****************************************************************************/
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,7 +25,7 @@ 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');
@@ -77,8 +77,7 @@ test.describe('Example Imagery Object', () => {
});
test('Can adjust image brightness/contrast by dragging the sliders', async ({ page, browserName }) => {
// eslint-disable-next-line playwright/no-skipped-test
test.skip(browserName === 'firefox', 'This test needs to be updated to work with firefox');
test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox');
// Open the image filter menu
await page.locator('[role=toolbar] button[title="Brightness and contrast"]').click();
@@ -423,12 +422,16 @@ test.describe('Example imagery thumbnails resize in display layouts', () => {
});
});
// test.fixme('Can use Mouse Wheel to zoom in and out of previous image');
// test.fixme('Can use alt+drag to move around image once zoomed in');
// test.fixme('Clicking on the left arrow should pause the imagery and go to previous image');
// test.fixme('If the imagery view is in pause mode, images still come in');
// test.fixme('If the imagery view is not in pause mode, it should be updated when new images come in');
test.describe('Example Imagery in Flexible layout', () => {
test('Example Imagery in Flexible layout @unstable', async ({ page, browserName, openmctConfig }) => {
const { myItemsFolderName } = openmctConfig;
// eslint-disable-next-line playwright/no-skipped-test
test.skip(browserName === 'firefox', 'This test needs to be updated to work with firefox');
test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox');
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5326'
@@ -570,40 +573,6 @@ 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

@@ -86,23 +86,6 @@ test.describe('Notebook section tests', () => {
//Delete 3rd section
//1st is selected and there is no default notebook
});
test.fixme('Section rename operations', async ({ page }) => {
// Create a new notebook
// Add a section
// Rename the section but do not confirm
// Keyboard press 'Escape'
// Verify that the section name reverts to the default name
// Rename the section but do not confirm
// Keyboard press 'Enter'
// Verify that the section name is updated
// Rename the section to "" (empty string)
// Keyboard press 'Enter' to confirm
// Verify that the section name reverts to the default name
// Rename the section to something long that overflows the text box
// Verify that the section name is not truncated while input is active
// Confirm the section name edit
// Verify that the section name is truncated now that input is not active
});
});
test.describe('Notebook page tests', () => {
@@ -124,23 +107,6 @@ test.describe('Notebook page tests', () => {
//Delete 3rd page
//First is now selected and there is no default notebook
});
test.fixme('Page rename operations', async ({ page }) => {
// Create a new notebook
// Add a page
// Rename the page but do not confirm
// Keyboard press 'Escape'
// Verify that the page name reverts to the default name
// Rename the page but do not confirm
// Keyboard press 'Enter'
// Verify that the page name is updated
// Rename the page to "" (empty string)
// Keyboard press 'Enter' to confirm
// Verify that the page name reverts to the default name
// Rename the page to something long that overflows the text box
// Verify that the page name is not truncated while input is active
// Confirm the page name edit
// Verify that the page name is truncated now that input is not active
});
});
test.describe('Notebook search tests', () => {

View File

@@ -21,7 +21,7 @@
*****************************************************************************/
const { test, expect } = require('../../../../pluginFixtures');
const { openObjectTreeContextMenu, createDomainObjectWithDefaults } = require('../../../../appActions');
const { openObjectTreeContextMenu } = require('../../../../appActions');
const path = require('path');
const TEST_TEXT = 'Testing text for entries.';
@@ -30,9 +30,8 @@ const CUSTOM_NAME = 'CUSTOM_NAME';
const NOTEBOOK_DROP_AREA = '.c-notebook__drag-area';
test.describe('Restricted Notebook', () => {
let notebook;
test.beforeEach(async ({ page }) => {
notebook = await startAndAddRestrictedNotebookObject(page);
await startAndAddRestrictedNotebookObject(page);
});
test('Can be renamed @addInit', async ({ page }) => {
@@ -40,7 +39,9 @@ test.describe('Restricted Notebook', () => {
});
test('Can be deleted if there are no locked pages @addInit', async ({ page, openmctConfig }) => {
await openObjectTreeContextMenu(page, notebook.url);
const { myItemsFolderName } = openmctConfig;
await openObjectTreeContextMenu(page, myItemsFolderName, `Unnamed ${CUSTOM_NAME}`);
const menuOptions = page.locator('.c-menu ul');
await expect.soft(menuOptions).toContainText('Remove');
@@ -75,9 +76,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 }) => {
notebook = await startAndAddRestrictedNotebookObject(page);
await startAndAddRestrictedNotebookObject(page);
await enterTextEntry(page);
await lockPage(page);
@@ -85,9 +86,9 @@ test.describe('Restricted Notebook with at least one entry and with the page loc
await page.locator('button.c-notebook__toggle-nav-button').click();
});
test('Locked page should now be in a locked state @addInit @unstable', async ({ page }, testInfo) => {
// eslint-disable-next-line playwright/no-skipped-test
test.skip(testInfo.project === 'chrome-beta', "Test is unreliable on chrome-beta");
test('Locked page should now be in a locked state @addInit @unstable', async ({ page, openmctConfig }, testInfo) => {
test.fixme(testInfo.project === 'chrome-beta', "Test is unreliable on chrome-beta");
const { myItemsFolderName } = openmctConfig;
// 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);
@@ -97,7 +98,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, notebook.url);
await openObjectTreeContextMenu(page, myItemsFolderName, `Unnamed ${CUSTOM_NAME}`);
const menuOptions = page.locator('.c-menu ul');
await expect(menuOptions).not.toContainText('Remove');
@@ -177,8 +178,13 @@ 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' });
return createDomainObjectWithDefaults(page, { type: CUSTOM_NAME });
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')
]);
}
/**

View File

@@ -56,23 +56,19 @@ async function createNotebookEntryAndTags(page, iterations = 1) {
await createNotebookAndEntry(page, iterations);
for (let iteration = 0; iteration < iterations; iteration++) {
// 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}`);
// Click text=To start a new entry, click here or drag and drop any object
await page.locator(`button:has-text("Add Tag") >> nth = ${iteration}`).click();
// Click inside the tag search input
// Click [placeholder="Type to select tag"]
await page.locator('[placeholder="Type to select tag"]').click();
// Select the "Driving" tag
// Click text=Driving
await page.locator('[aria-label="Autocomplete Options"] >> text=Driving').click();
// 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}`);
// Click button:has-text("Add Tag")
await page.locator(`button:has-text("Add Tag") >> nth = ${iteration}`).click();
// Click inside the tag search input
// Click [placeholder="Type to select tag"]
await page.locator('[placeholder="Type to select tag"]').click();
// Select the "Science" tag
// Click text=Science
await page.locator('[aria-label="Autocomplete Options"] >> text=Science').click();
}
}
@@ -126,16 +122,15 @@ test.describe('Tagging in Notebooks @addInit', () => {
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
// Fill [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Xq');
await expect(page.locator('[aria-label="Search Result"]')).toBeHidden();
await expect(page.locator('[aria-label="Search Result"]')).toBeHidden();
await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible();
await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible();
});
test('Can delete tags', async ({ page }) => {
await createNotebookEntryAndTags(page);
await page.locator('[aria-label="Notebook Entries"]').click();
// Delete Driving
await page.hover('.c-tag__label:has-text("Driving")');
await page.locator('.c-tag__label:has-text("Driving") ~ .c-completed-tag-deletion').click();
await page.locator('text=Science Driving Add Tag >> button').nth(1).click();
await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Science");
await expect(page.locator('[aria-label="Notebook Entry"]')).not.toContainText("Driving");

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -28,10 +28,9 @@ const { test, expect } = require('../../../../pluginFixtures');
test.describe('Handle missing object for plots', () => {
test('Displays empty div for missing stacked plot item @unstable', async ({ page, browserName, openmctConfig }) => {
// eslint-disable-next-line playwright/no-skipped-test
test.skip(browserName === 'firefox', 'Firefox failing due to console events being missed');
const { myItemsFolderName } = openmctConfig;
test.fixme(browserName === 'firefox', 'Firefox failing due to console events being missed');
const errorLogs = [];
page.on("console", (message) => {

View File

@@ -147,24 +147,4 @@ test.describe('Time conductor input fields real-time mode', () => {
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
});
});

View File

@@ -24,10 +24,9 @@ const { test, expect } = require('../../../../pluginFixtures');
const { openObjectTreeContextMenu, createDomainObjectWithDefaults } = require('../../../../appActions');
test.describe('Timer', () => {
let timer;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
timer = await createDomainObjectWithDefaults(page, { type: 'timer' });
await createDomainObjectWithDefaults(page, { type: 'timer' });
});
test('Can perform actions on the Timer', async ({ page, openmctConfig }) => {
@@ -36,13 +35,13 @@ test.describe('Timer', () => {
description: 'https://github.com/nasa/openmct/issues/4313'
});
const timerUrl = timer.url;
const { myItemsFolderName } = await openmctConfig;
await test.step("From the tree context menu", async () => {
await triggerTimerContextMenuAction(page, timerUrl, 'Start');
await triggerTimerContextMenuAction(page, timerUrl, 'Pause');
await triggerTimerContextMenuAction(page, timerUrl, 'Restart at 0');
await triggerTimerContextMenuAction(page, timerUrl, 'Stop');
await triggerTimerContextMenuAction(page, myItemsFolderName, 'Start');
await triggerTimerContextMenuAction(page, myItemsFolderName, 'Pause');
await triggerTimerContextMenuAction(page, myItemsFolderName, 'Restart at 0');
await triggerTimerContextMenuAction(page, myItemsFolderName, 'Stop');
});
await test.step("From the 3dot menu", async () => {
@@ -75,9 +74,9 @@ test.describe('Timer', () => {
* @param {import('@playwright/test').Page} page
* @param {TimerAction} action
*/
async function triggerTimerContextMenuAction(page, timerUrl, action) {
async function triggerTimerContextMenuAction(page, myItemsFolderName, action) {
const menuAction = `.c-menu ul li >> text="${action}"`;
await openObjectTreeContextMenu(page, timerUrl);
await openObjectTreeContextMenu(page, myItemsFolderName, "Unnamed Timer");
await page.locator(menuAction).click();
assertTimerStateAfterAction(page, action);
}

View File

@@ -24,8 +24,6 @@
*/
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 }) => {
@@ -43,7 +41,7 @@ test.describe('Grand Search', () => {
await expect(page.locator('[aria-label="Search Result"] >> nth=3')).toContainText(`Clock D ${myItemsFolderName} Red Folder Blue Folder`);
// Click text=Elements >> nth=0
await page.locator('text=Elements').first().click();
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeHidden();
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).not.toBeVisible();
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click();
await page.locator('[aria-label="Clock A clock result"] >> text=Clock A').click();
@@ -56,11 +54,11 @@ test.describe('Grand Search', () => {
// Click [aria-label="OpenMCT Search"] a >> nth=0
await page.locator('[aria-label="OpenMCT Search"] a').first().click();
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeHidden();
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).not.toBeVisible();
// Fill [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('foo');
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeHidden();
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).not.toBeVisible();
// Click text=Snapshot Save and Finish Editing Save and Continue Editing >> button >> nth=1
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
@@ -114,16 +112,13 @@ test.describe("Search Tests @unstable", () => {
await expect(page.locator('text=No matching results.')).toBeVisible();
});
test('Validate single object in search result @couchdb', async ({ page }) => {
test('Validate single object in search result', async ({ page }) => {
//Go to baseURL
await page.goto("./", { waitUntil: "networkidle" });
// Create a folder object
const folderName = uuid();
await createDomainObjectWithDefaults(page, {
type: 'folder',
name: folderName
});
const folderName = 'testFolder';
await createFolderObject(page, folderName);
// Full search for object
await page.type("input[type=search]", folderName);
@@ -132,7 +127,7 @@ test.describe("Search Tests @unstable", () => {
await waitForSearchCompletion(page);
// Get the search results
const searchResults = page.locator(searchResultSelector);
const searchResults = await page.locator(searchResultSelector);
// Verify that one result is found
expect(await searchResults.count()).toBe(1);

View File

@@ -1,138 +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.
*****************************************************************************/
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

@@ -1,101 +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.
*****************************************************************************/
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

@@ -67,7 +67,7 @@ test.describe('Visual - Default', () => {
await percySnapshot(page, `About (theme: '${theme}')`);
});
test('Visual - Default Condition Set @unstable', async ({ page, theme }) => {
test.fixme('Visual - Default Condition Set', async ({ page, theme }) => {
await createDomainObjectWithDefaults(page, { type: 'Condition Set' });
@@ -75,7 +75,7 @@ test.describe('Visual - Default', () => {
await percySnapshot(page, `Default Condition Set (theme: '${theme}')`);
});
test('Visual - Default Condition Widget @unstable', async ({ page, theme }) => {
test.fixme('Visual - Default Condition Widget', async ({ page, theme }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5349'
@@ -137,7 +137,7 @@ test.describe('Visual - Default', () => {
await percySnapshot(page, `removed amplitude property value (theme: '${theme}')`);
});
test('Visual - Save Successful Banner @unstable', async ({ page, theme }) => {
test.fixme('Visual - Save Successful Banner', async ({ page, theme }) => {
await createDomainObjectWithDefaults(page, { type: 'Timer' });
await page.locator('.c-message-banner__message').hover({ trial: true });
@@ -159,7 +159,7 @@ test.describe('Visual - Default', () => {
});
test('Visual - Default Gauge is correct @unstable', async ({ page, theme }) => {
test.fixme('Visual - Default Gauge is correct', async ({ page, theme }) => {
await createDomainObjectWithDefaults(page, { type: 'Gauge' });
// Take a snapshot of the newly created Gauge object

View File

@@ -1,78 +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.
*****************************************************************************/
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

@@ -1,76 +0,0 @@
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

@@ -20,36 +20,59 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import utils from './utils';
export default function (staticFaults = false) {
export default function () {
return function install(openmct) {
openmct.install(openmct.plugins.FaultManagement());
const faultsData = utils.randomFaults(staticFaults);
openmct.faults.addProvider({
request(domainObject, options) {
return Promise.resolve(faultsData);
const faults = JSON.parse(localStorage.getItem('faults'));
return Promise.resolve(faults.alarms);
},
subscribe(domainObject, callback) {
return () => {};
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);
};
},
supportsRequest(domainObject) {
return domainObject.type === 'faultManagement';
const faults = localStorage.getItem('faults');
return faults && domainObject.type === 'faultManagement';
},
supportsSubscribe(domainObject) {
return domainObject.type === 'faultManagement';
const faults = localStorage.getItem('faults');
return faults && domainObject.type === 'faultManagement';
},
acknowledgeFault(fault, { comment = '' }) {
utils.acknowledgeFault(fault);
console.log('acknowledgeFault', fault);
console.log('comment', comment);
return Promise.resolve({
success: true
});
},
shelveFault(fault, duration) {
utils.shelveFault(fault, duration);
shelveFault(fault, shelveData) {
console.log('shelveFault', fault);
console.log('shelveData', shelveData);
return Promise.resolve({
success: true

View File

@@ -5,7 +5,7 @@
"devDependencies": {
"@babel/eslint-parser": "7.18.9",
"@braintree/sanitize-url": "6.0.0",
"@percy/cli": "1.10.0",
"@percy/cli": "1.7.2",
"@percy/playwright": "1.0.4",
"@playwright/test": "1.23.0",
"@types/eventemitter3": "^1.0.0",
@@ -23,9 +23,9 @@
"d3-axis": "3.0.0",
"d3-scale": "3.3.0",
"d3-selection": "3.0.0",
"eslint": "8.22.0",
"eslint": "8.18.0",
"eslint-plugin-compat": "4.0.2",
"eslint-plugin-playwright": "0.11.1",
"eslint-plugin-playwright": "0.10.0",
"eslint-plugin-vue": "9.3.0",
"eslint-plugin-you-dont-need-lodash-underscore": "6.12.0",
"eventemitter3": "1.2.0",
@@ -33,8 +33,9 @@
"file-saver": "2.0.5",
"git-rev-sync": "3.0.2",
"html2canvas": "1.4.1",
"imports-loader": "4.0.1",
"imports-loader": "0.8.0",
"jasmine-core": "4.3.0",
"jsdoc": "3.6.11",
"karma": "6.3.20",
"karma-chrome-launcher": "3.1.1",
"karma-cli": "2.0.0",
@@ -45,20 +46,22 @@
"karma-sourcemap-loader": "0.3.8",
"karma-spec-reporter": "0.0.34",
"karma-webpack": "5.0.0",
"lighthouse": "9.6.1",
"location-bar": "3.0.1",
"lodash": "4.17.21",
"mini-css-extract-plugin": "2.6.1",
"moment": "2.29.4",
"moment-duration-format": "2.3.2",
"moment-timezone": "0.5.34",
"node-bourbon": "4.2.3",
"nyc":"15.1.0",
"painterro": "1.2.78",
"plotly.js-basic-dist": "2.14.0",
"plotly.js-gl2d-dist": "2.14.0",
"plotly.js-basic-dist": "2.12.0",
"plotly.js-gl2d-dist": "2.12.0",
"printj": "1.3.1",
"request": "2.88.2",
"resolve-url-loader": "5.0.0",
"sass": "1.54.4",
"sass": "1.52.2",
"sass-loader": "13.0.2",
"sinon": "14.0.0",
"style-loader": "^1.0.1",
@@ -87,8 +90,7 @@
"test": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run",
"test:debug": "cross-env NODE_ENV=debug karma start --no-single-run",
"test:e2e": "npx playwright test",
"test:e2e:couchdb": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @couchdb",
"test:e2e:stable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep-invert \"@unstable|@couchdb\"",
"test:e2e:stable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep-invert @unstable",
"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",
@@ -96,8 +98,11 @@
"test:e2e:full": "npx playwright test --config=e2e/playwright-ci.config.js",
"test:perf": "npx playwright test --config=e2e/playwright-performance.config.js",
"test:watch": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --no-single-run",
"jsdoc": "jsdoc -c jsdoc.json -R API.md -r -d dist/docs/api",
"update-about-dialog-copyright": "perl -pi -e 's/20\\d\\d\\-202\\d/2014\\-2022/gm' ./src/ui/layout/AboutDialog.vue",
"update-copyright-date": "npm run update-about-dialog-copyright && grep -lr --null --include=*.{js,scss,vue,ts,sh,html,md,frag} 'Copyright (c) 20' . | xargs -r0 perl -pi -e 's/Copyright\\s\\(c\\)\\s20\\d\\d\\-20\\d\\d/Copyright \\(c\\)\\ 2014\\-2022/gm'",
"otherdoc": "node docs/gendocs.js --in docs/src --out dist/docs --suppress-toc 'docs/src/index.md|docs/src/process/index.md'",
"docs": "npm run jsdoc ; npm run otherdoc",
"cov:e2e:report":"nyc report --reporter=lcovonly --report-dir=./coverage/e2e",
"cov:e2e:full:publish":"codecov --disable=gcov -f ./coverage/e2e/lcov.info -F e2e-full",
"cov:e2e:stable:publish":"codecov --disable=gcov -f ./coverage/e2e/lcov.info -F e2e-stable",

View File

@@ -63,8 +63,6 @@ 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);
@@ -77,12 +75,6 @@ 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);
});
@@ -267,6 +259,7 @@ class InMemorySearchProvider {
}
onAnnotationCreation(annotationObject) {
const objectProvider = this.openmct.objects.getProvider(annotationObject.identifier);
if (objectProvider === undefined || objectProvider.search === undefined) {
const provider = this;
@@ -288,34 +281,17 @@ class InMemorySearchProvider {
provider.index(domainObject);
}
onCompositionAdded(newDomainObjectToIndex) {
onCompositionMutation(domainObject, composition) {
const provider = this;
// 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));
const indexedComposition = domainObject.composition;
const identifiersToIndex = composition
.filter(identifier => !indexedComposition
.some(indexedIdentifier => this.openmct.objects
.areIdsEqual([identifier, indexedIdentifier])));
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];
}
identifiersToIndex.forEach(identifier => {
this.openmct.objects.get(identifier).then(objectToIndex => provider.index(objectToIndex));
});
}
/**
@@ -329,7 +305,6 @@ 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(
@@ -337,12 +312,11 @@ class InMemorySearchProvider {
'name',
this.onNameMutation.bind(this, domainObject)
);
if (composition) {
composition.on('add', this.onCompositionAdded);
composition.on('remove', this.onCompositionRemoved);
this.indexedCompositions[keyString] = composition;
}
this.indexedCompositions[keyString] = this.openmct.objects.observe(
domainObject,
'composition',
this.onCompositionMutation.bind(this, domainObject)
);
if (domainObject.type === 'annotation') {
this.indexedTags[keyString] = this.openmct.objects.observe(
domainObject,
@@ -364,6 +338,8 @@ class InMemorySearchProvider {
}
}
const composition = this.openmct.composition.get(domainObject);
if (composition !== undefined) {
const children = await composition.load();

View File

@@ -230,10 +230,15 @@ export default class ObjectAPI {
return result;
}).catch((result) => {
console.warn(`Failed to retrieve ${keystring}:`, result);
this.openmct.notifications.error(`Failed to retrieve object ${keystring}`);
delete this.cache[keystring];
result = this.applyGetInterceptors(identifier);
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);
}
return result;
});

View File

@@ -97,11 +97,11 @@ export default {
},
followTimeContext() {
this.timeContext.on('bounds', this.reloadTelemetryOnBoundsChange);
this.timeContext.on('bounds', this.reloadTelemetry);
},
stopFollowingTimeContext() {
if (this.timeContext) {
this.timeContext.off('bounds', this.reloadTelemetryOnBoundsChange);
this.timeContext.off('bounds', this.reloadTelemetry);
}
},
addToComposition(telemetryObject) {
@@ -181,11 +181,6 @@ export default {
this.composition.on('remove', this.removeTelemetryObject);
this.composition.load();
},
reloadTelemetryOnBoundsChange(bounds, isTick) {
if (!isTick) {
this.reloadTelemetry();
}
},
reloadTelemetry() {
this.valuesByTimestamp = {};

View File

@@ -51,11 +51,7 @@ export default class TelemetryCriterion extends EventEmitter {
}
initialize() {
this.telemetryObjectIdAsString = "";
if (![undefined, null, ""].includes(this.telemetryDomainObjectDefinition?.telemetry)) {
this.telemetryObjectIdAsString = this.openmct.objects.makeKeyString(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,19 +517,7 @@ export default {
initializeItems() {
this.telemetryViewMap = {};
this.objectViewMap = {};
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);
this.layoutItems.forEach(this.trackItem);
},
isItemAlreadyTracked(child) {
let found = false;

View File

@@ -232,12 +232,10 @@ export default {
this.removeSelectable();
}
if (this.telemetryCollection) {
this.telemetryCollection.off('add', this.setLatestValues);
this.telemetryCollection.off('clear', this.refreshData);
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,7 +21,6 @@
*****************************************************************************/
import { createOpenMct, resetApplicationState } from 'utils/testing';
import Vue from 'vue';
import DisplayLayoutPlugin from './plugin';
describe('the plugin', function () {
@@ -118,59 +117,6 @@ 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,8 +71,6 @@ 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,
@@ -127,19 +125,27 @@ export default {
},
methods: {
filterUsingSearchTerm(fault) {
if (!fault) {
return false;
if (fault?.id?.toString().toLowerCase().includes(this.searchTerm)) {
return true;
}
let match = false;
if (fault?.triggerValueInfo?.toString().toLowerCase().includes(this.searchTerm)) {
return true;
}
SEARCH_KEYS.forEach((key) => {
if (fault[key]?.toString().toLowerCase().includes(this.searchTerm)) {
match = true;
}
});
if (fault?.currentValueInfo?.toString().toLowerCase().includes(this.searchTerm)) {
return true;
}
return match;
if (fault?.triggerTime.toString().toLowerCase().includes(this.searchTerm)) {
return true;
}
if (fault?.severity.toString().toLowerCase().includes(this.searchTerm)) {
return true;
}
return false;
},
isSelected(fault) {
return Boolean(this.selectedFaults[fault.id]);

View File

@@ -24,22 +24,10 @@ import {
createOpenMct,
resetApplicationState
} from '../../utils/testing';
import {
FAULT_MANAGEMENT_TYPE,
FAULT_MANAGEMENT_VIEW,
FAULT_MANAGEMENT_NAMESPACE
} from './constants';
import { FAULT_MANAGEMENT_TYPE } 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();
@@ -50,54 +38,15 @@ describe("The Fault Management Plugin", () => {
});
it('is not installed by default', () => {
const typeDef = openmct.types.get(FAULT_MANAGEMENT_TYPE).definition;
let typeDef = openmct.types.get(FAULT_MANAGEMENT_TYPE).definition;
expect(typeDef.name).toBe('Unknown Type');
});
it('can be installed', () => {
openmct.install(openmct.plugins.FaultManagement());
const typeDef = openmct.types.get(FAULT_MANAGEMENT_TYPE).definition;
let 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

@@ -519,17 +519,20 @@ export default {
},
watch: {
imageHistory: {
handler(newHistory, _oldHistory) {
handler(newHistory, oldHistory) {
const newSize = newHistory.length;
let imageIndex = newSize > 0 ? newSize - 1 : undefined;
let imageIndex;
if (this.focusedImageTimestamp !== undefined) {
const foundImageIndex = newHistory.findIndex(img => img.time === this.focusedImageTimestamp);
if (foundImageIndex > -1) {
imageIndex = foundImageIndex;
}
imageIndex = foundImageIndex > -1
? foundImageIndex
: newSize - 1;
} else {
imageIndex = newSize > 0
? newSize - 1
: undefined;
}
this.setFocusedImage(imageIndex);
this.nextImageIndex = imageIndex;
if (this.previousFocusedImage && newHistory.length) {

View File

@@ -27,13 +27,10 @@ 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: ' + keyString
name: 'Missing: ' + openmct.objects.makeKeyString(identifier)
};
}

View File

@@ -69,27 +69,27 @@ describe("the plugin", () => {
});
describe('adds an interceptor that returns a "My Items" model for', () => {
let myItemsObject;
let mockNotFoundProvider;
let myItemsMissing;
let mockMissingProvider;
let activeProvider;
beforeEach(async () => {
mockNotFoundProvider = {
get: () => Promise.reject(new Error('Not found')),
mockMissingProvider = {
get: () => Promise.resolve(missingObj),
create: () => Promise.resolve(missingObj),
update: () => Promise.resolve(missingObj)
};
activeProvider = mockNotFoundProvider;
activeProvider = mockMissingProvider;
spyOn(openmct.objects, 'getProvider').and.returnValue(activeProvider);
myItemsObject = await openmct.objects.get(myItemsIdentifier);
myItemsMissing = await openmct.objects.get(myItemsIdentifier);
});
it('missing objects', () => {
let idsMatch = openmct.objects.areIdsEqual(myItemsObject.identifier, myItemsIdentifier);
let idsMatchMissing = openmct.objects.areIdsEqual(myItemsMissing.identifier, myItemsIdentifier);
expect(myItemsObject).toBeDefined();
expect(idsMatch).toBeTrue();
expect(myItemsMissing).toBeDefined();
expect(idsMatchMissing).toBeTrue();
});
});

View File

@@ -12,10 +12,8 @@
<template v-if="!page.isLocked">
<div
class="c-list__item__name js-list__item__name"
:class="[{ 'c-input-inline': isSelected }]"
:data-id="page.id"
:contenteditable="isSelected"
@keydown.escape="updateName"
:contenteditable="true"
@keydown.enter="updateName"
@blur="updateName"
>{{ pageName }}</div>
@@ -34,9 +32,8 @@
</template>
<script>
import { KEY_ENTER, KEY_ESCAPE } from '../utils/notebook-key-code';
import RemoveDialog from '../utils/removeDialog';
import PopupMenu from './PopupMenu.vue';
import RemoveDialog from '../utils/removeDialog';
export default {
components: {
@@ -110,39 +107,36 @@ export default {
removeDialog.show();
},
selectPage(event) {
const { target: { dataset: { id } } } = event;
const target = event.target;
const id = target.dataset.id;
if (this.isSelected || !id) {
if (!this.page.isLocked) {
const page = target.closest('.js-list__item');
const input = page.querySelector('.js-list__item__name');
if (page.className.indexOf('is-selected') > -1) {
input.classList.add('c-input-inline');
return;
}
}
if (!id) {
return;
}
this.$emit('selectPage', id);
},
renamePage(target) {
if (!target) {
return;
}
target.textContent = target.textContent ? target.textContent.trim() : `Unnamed ${this.pageTitle}`;
if (this.page.name === target.textContent) {
return;
}
this.$emit('renamePage', Object.assign(this.page, { name: target.textContent }));
},
updateName(event) {
const { target, keyCode, type } = event;
const target = event.target;
const name = target.textContent.toString();
target.classList.remove('c-input-inline');
if (keyCode === KEY_ESCAPE) {
target.textContent = this.page.name;
} else if (keyCode === KEY_ENTER || type === 'blur') {
this.renamePage(target);
if (name === '' || this.page.name === name) {
return;
}
target.scrollLeft = '0';
target.blur();
this.$emit('renamePage', Object.assign(this.page, { name }));
}
}
};

View File

@@ -7,10 +7,8 @@
>
<span
class="c-list__item__name js-list__item__name"
:class="[{ 'c-input-inline': isSelected && !section.isLocked }]"
:data-id="section.id"
:contenteditable="isSelected && !section.isLocked"
@keydown.escape="updateName"
contenteditable="true"
@keydown.enter="updateName"
@blur="updateName"
>{{ sectionName }}</span>
@@ -22,9 +20,8 @@
</template>
<script>
import { KEY_ENTER, KEY_ESCAPE } from '../utils/notebook-key-code';
import RemoveDialog from '../utils/removeDialog';
import PopupMenu from './PopupMenu.vue';
import RemoveDialog from '../utils/removeDialog';
export default {
components: {
@@ -99,39 +96,36 @@ export default {
removeDialog.show();
},
selectSection(event) {
const { target: { dataset: { id } } } = event;
const target = event.target;
const id = target.dataset.id;
if (this.isSelected || !id) {
if (!this.section.isLocked) {
const section = target.closest('.js-list__item');
const input = section.querySelector('.js-list__item__name');
if (section.className.indexOf('is-selected') > -1) {
input.classList.add('c-input-inline');
return;
}
}
if (!id) {
return;
}
this.$emit('selectSection', id);
},
renameSection(target) {
if (!target) {
return;
}
target.textContent = target.textContent ? target.textContent.trim() : `Unnamed ${this.sectionTitle}`;
if (this.section.name === target.textContent) {
return;
}
this.$emit('renameSection', Object.assign(this.section, { name: target.textContent }));
},
updateName(event) {
const { target, keyCode, type } = event;
const target = event.target;
target.classList.remove('c-input-inline');
const name = target.textContent.trim();
if (keyCode === KEY_ESCAPE) {
target.textContent = this.section.name;
} else if (keyCode === KEY_ENTER || type === 'blur') {
this.renameSection(target);
if (name === '' || this.section.name === name) {
return;
}
target.scrollLeft = '0';
target.blur();
this.$emit('renameSection', Object.assign(this.section, { name }));
}
}
};

View File

@@ -88,7 +88,7 @@
}
&__name {
flex: 1 1 auto;
flex: 0 1 auto;
}
&__menu-indicator {

View File

@@ -1,3 +0,0 @@
// Key codes for `KeyboardEvent.keyCode`.
export const KEY_ENTER = 13;
export const KEY_ESCAPE = 27;

View File

@@ -1,5 +0,0 @@
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

@@ -1,145 +1,52 @@
# 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
## 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
## macOS
### Installing with admin privileges to your computer
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 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>.
### Installing without admin privileges to your computer
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 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
# Configuring Open MCT
1. Edit `openmct/index.html` comment out the following line:
```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! 🏆
```
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! 🏆

View File

@@ -1,14 +0,0 @@
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

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

View File

@@ -1,137 +0,0 @@
#!/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"\'
}
enable_cors () {
curl -su "${CURL_USERPASS_ARG}" -o /dev/null -X PUT $COUCH_BASE_LOCAL/_node/$COUCH_NODE_NAME/_config/httpd/enable_cors -d '"true"'
curl -su "${CURL_USERPASS_ARG}" -o /dev/null -X PUT $COUCH_BASE_LOCAL/_node/$COUCH_NODE_NAME/_config/cors/origins -d '"http://localhost:8080"'
curl -su "${CURL_USERPASS_ARG}" -o /dev/null -X PUT $COUCH_BASE_LOCAL/_node/$COUCH_NODE_NAME/_config/cors/credentials -d '"true"'
curl -su "${CURL_USERPASS_ARG}" -o /dev/null -X PUT $COUCH_BASE_LOCAL/_node/$COUCH_NODE_NAME/_config/cors/methods -d '"GET, PUT, POST, HEAD, DELETE"'
curl -su "${CURL_USERPASS_ARG}" -o /dev/null -X PUT $COUCH_BASE_LOCAL/_node/$COUCH_NODE_NAME/_config/cors/headers -d '"accept, authorization, content-type, origin, referer, x-csrf-token"'
}
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
echo "Enabling CORS"
enable_cors
echo "CORS successfully enabled"

View File

@@ -136,7 +136,7 @@
<span v-if="showValueWhenExpanded">Value</span>
<span v-if="showMinimumWhenExpanded">Min</span>
<span v-if="showMaximumWhenExpanded">Max</span>
<span v-if="showUnitsWhenExpanded">Unit</span>
<span v-if="showUnitsWhenExpanded">Units</span>
</div>
</li>
</ul>

View File

@@ -54,7 +54,7 @@
<option value="nearestValue">Nearest value</option>
<option value="min">Minimum value</option>
<option value="max">Maximum value</option>
<option value="unit">Unit</option>
<option value="units">Units</option>
</select>
</div>
</li>
@@ -89,7 +89,7 @@
v-model="showUnitsWhenExpanded"
type="checkbox"
@change="updateForm('showUnitsWhenExpanded')"
> Unit</li>
> Units</li>
</ul>
</div>

View File

@@ -41,7 +41,7 @@
<span class="plot-series-name">{{ nameWithUnit }}</span>
</div>
<div
v-show="!!highlights.length && (valueToShowWhenCollapsed !== 'none' && valueToShowWhenCollapsed !== 'unit')"
v-show="!!highlights.length && (valueToShowWhenCollapsed !== 'none' && valueToShowWhenCollapsed !== 'units')"
class="plot-series-value hover-value-enabled"
:class="[{ 'cursor-hover': notNearest }, valueToDisplayWhenCollapsedClass, mctLimitStateClass]"
>

View File

@@ -32,7 +32,7 @@ define([
'./autoflow/AutoflowTabularPlugin',
'./timeConductor/plugin',
'../../example/imagery/plugin',
'../../example/faultManagement/exampleFaultSource',
'../../example/faultManagment/exampleFaultSource',
'./imagery/plugin',
'./summaryWidget/plugin',
'./URLIndicatorPlugin/URLIndicatorPlugin',
@@ -58,6 +58,7 @@ define([
'./condition/plugin',
'./conditionWidget/plugin',
'./themes/espresso',
'./themes/maelstrom',
'./themes/snow',
'./URLTimeSettingsSynchronizer/plugin',
'./notificationIndicator/plugin',
@@ -121,6 +122,7 @@ define([
ConditionPlugin,
ConditionWidgetPlugin,
Espresso,
Maelstrom,
Snow,
URLTimeSettingsSynchronizer,
NotificationIndicator,
@@ -205,6 +207,7 @@ define([
plugins.ClearData = ClearData;
plugins.WebPage = WebPagePlugin.default;
plugins.Espresso = Espresso.default;
plugins.Maelstrom = Maelstrom.default;
plugins.Snow = Snow.default;
plugins.Condition = ConditionPlugin.default;
plugins.ConditionWidget = ConditionWidgetPlugin.default;

View File

@@ -0,0 +1,22 @@
@import "../../styles/vendor/normalize-min";
@import "../../styles/constants";
@import "../../styles/constants-mobile.scss";
@import "../../styles/constants-maelstrom";
@import "../../styles/mixins";
@import "../../styles/animations";
@import "../../styles/about";
@import "../../styles/glyphs";
@import "../../styles/global";
@import "../../styles/status";
@import "../../styles/limits";
@import "../../styles/controls";
@import "../../styles/forms";
@import "../../styles/table";
@import "../../styles/legacy";
@import "../../styles/legacy-plots";
@import "../../styles/plotly";
@import "../../styles/legacy-messages";
@import "../../styles/vue-styles.scss";

View File

@@ -0,0 +1,7 @@
import { installTheme } from './installTheme';
export default function plugin() {
return function install(openmct) {
installTheme(openmct, 'maelstrom');
};
}

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_LENGTH = 10;
const DEFAULT_RECORDS = 10;
import { millisecondsToDHMS } from "utils/duration";
import UTCTimeFormat from "../utcTimeSystem/UTCTimeFormat.js";
@@ -79,14 +79,16 @@ export default {
* @timespans {start, end} number representing timestamp
*/
fixedHistory: {},
presets: [],
isFixed: this.openmct.time.clock() === undefined
presets: []
};
},
computed: {
currentHistory() {
return this.mode + 'History';
},
isFixed() {
return this.openmct.time.clock() === undefined;
},
historyForCurrentTimeSystem() {
const history = this[this.currentHistory][this.timeSystem.key];
@@ -94,7 +96,7 @@ export default {
},
storageKey() {
let key = LOCAL_STORAGE_HISTORY_KEY_FIXED;
if (!this.isFixed) {
if (this.mode !== 'fixed') {
key = LOCAL_STORAGE_HISTORY_KEY_REALTIME;
}
@@ -106,7 +108,6 @@ export default {
handler() {
// only for fixed time since we track offsets for realtime
if (this.isFixed) {
this.updateMode();
this.addTimespan();
}
},
@@ -114,35 +115,28 @@ export default {
},
offsets: {
handler() {
this.updateMode();
this.addTimespan();
},
deep: true
},
timeSystem: {
handler(ts) {
this.updateMode();
this.loadConfiguration();
this.addTimespan();
},
deep: true
},
mode: function () {
this.updateMode();
this.getHistoryFromLocalStorage();
this.initializeHistoryIfNoHistory();
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;
@@ -209,8 +203,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.MAX_RECORDS_LENGTH) {
currentHistory.length = this.MAX_RECORDS_LENGTH;
if (currentHistory.length > this.records) {
currentHistory.length = this.records;
}
this.$set(this[this.currentHistory], key, currentHistory);
@@ -237,7 +231,7 @@ export default {
.filter(option => option.timeSystem === this.timeSystem.key);
this.presets = this.loadPresets(configurations);
this.MAX_RECORDS_LENGTH = this.loadRecords(configurations);
this.records = this.loadRecords(configurations);
},
loadPresets(configurations) {
const configuration = configurations.find(option => {
@@ -249,9 +243,9 @@ export default {
},
loadRecords(configurations) {
const configuration = configurations.find(option => option.records);
const maxRecordsLength = configuration ? configuration.records : DEFAULT_RECORDS_LENGTH;
const records = configuration ? configuration.records : DEFAULT_RECORDS;
return maxRecordsLength;
return records;
},
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), millisecondsToDHMS(213927028)];
const validResults = [' ', '+ 1d', '+ 1d 12h', '+ 7d 15h 50m 24s', '+ 2d 11h 25m 27s 28ms'];
millisecondsToDHMS(129600000), millisecondsToDHMS(661824000)];
const validResults = [' ', '+ 1d', '+ 1d 12h', '+ 7d 15h 50m 24s'];
expect(validResults).toEqual(functionResults);
});
it('should get precise duration', () => {
const functionResults = [getPreciseDuration(0), getPreciseDuration(643680000),
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'];
getPreciseDuration(1605312000)];
const validResults = ['00:00:00:00', '07:10:48:00', '18:13:55:12'];
expect(validResults).toEqual(functionResults);
});
});

View File

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

View File

@@ -33,16 +33,28 @@ 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: 1,
futureEventsIndex: 0,
futureEventsDurationIndex: 0,
futureEventsDuration: 20,
currentEventsIndex: 1,
currentEventsDurationIndex: 0,
currentEventsDuration: 20,
pastEventsIndex: 1,
pastEventsIndex: 0,
pastEventsDurationIndex: 0,
pastEventsDuration: 20,
filter: ''

View File

@@ -95,12 +95,14 @@ describe('the plugin', function () {
originalRouterPath = openmct.router.path;
mockComposition = new EventEmitter();
// eslint-disable-next-line require-await
mockComposition.load = async () => {
return [planObject];
mockComposition.load = () => {
mockComposition.emit('add', planObject);
return Promise.resolve([planObject]);
};
spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
openmct.on('start', done);
openmct.start(appHolder);
});
@@ -266,8 +268,6 @@ 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,8 +319,6 @@ 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);
@@ -372,8 +370,6 @@ 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,12 +32,6 @@
.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

@@ -744,9 +744,3 @@ body.mobile {
}
}
}
.c-list__item {
&__name:focus {
text-overflow: clip;
}
}

View File

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

View File

@@ -41,8 +41,6 @@
<div
ref="mainTree"
class="c-tree-and-search__tree c-tree"
role="tree"
aria-expanded="true"
>
<div>
@@ -469,7 +467,7 @@ export default {
}
},
scrollEndEvent() {
if (!this.$refs.scrollable) {
if (!this.$refs.srcrollable) {
return;
}
@@ -578,17 +576,14 @@ export default {
};
},
addTreeItemObserver(domainObject, parentObjectPath) {
const objectPath = [domainObject].concat(parentObjectPath);
const navigationPath = this.buildNavigationPath(objectPath);
if (this.observers[navigationPath]) {
this.observers[navigationPath]();
if (this.observers[domainObject.identifier.key]) {
this.observers[domainObject.identifier.key]();
}
this.observers[navigationPath] = this.openmct.objects.observe(
this.observers[domainObject.identifier.key] = this.openmct.objects.observe(
domainObject,
'name',
this.sortTreeItems.bind(this, parentObjectPath)
this.updateTreeItems.bind(this, parentObjectPath)
);
},
async updateTreeItems(parentObjectPath) {
@@ -615,44 +610,6 @@ 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

@@ -42,8 +42,6 @@ describe("GrandSearch", () => {
let mockAnotherFolderObject;
let mockTopObject;
let originalRouterPath;
let mockNewObject;
let mockObjectProvider;
beforeEach((done) => {
openmct = createOpenMct();
@@ -57,7 +55,6 @@ describe("GrandSearch", () => {
mockDomainObject = {
type: 'notebook',
name: 'fooRabbitNotebook',
location: 'fooNameSpace:topObject',
identifier: {
key: 'some-object',
namespace: 'fooNameSpace'
@@ -78,7 +75,6 @@ describe("GrandSearch", () => {
mockTopObject = {
type: 'root',
name: 'Top Folder',
composition: [],
identifier: {
key: 'topObject',
namespace: 'fooNameSpace'
@@ -87,7 +83,6 @@ describe("GrandSearch", () => {
mockAnotherFolderObject = {
type: 'folder',
name: 'Another Test Folder',
composition: [],
location: 'fooNameSpace:topObject',
identifier: {
key: 'someParent',
@@ -97,7 +92,6 @@ describe("GrandSearch", () => {
mockFolderObject = {
type: 'folder',
name: 'Test Folder',
composition: [],
location: 'fooNameSpace:someParent',
identifier: {
key: 'someFolder',
@@ -107,7 +101,6 @@ describe("GrandSearch", () => {
mockDisplayLayout = {
type: 'layout',
name: 'Bar Layout',
composition: [],
identifier: {
key: 'some-layout',
namespace: 'fooNameSpace'
@@ -132,19 +125,9 @@ 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);
mockObjectProvider = jasmine.createSpyObj("mock object provider", [
const mockObjectProvider = jasmine.createSpyObj("mock object provider", [
"create",
"update",
"get"
@@ -163,8 +146,6 @@ describe("GrandSearch", () => {
return mockAnotherFolderObject;
} else if (identifier.key === mockTopObject.identifier.key) {
return mockTopObject;
} else if (identifier.key === mockNewObject.identifier.key) {
return mockNewObject;
} else {
return null;
}
@@ -187,7 +168,6 @@ 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);
@@ -216,7 +196,6 @@ describe("GrandSearch", () => {
openmct.objects.inMemorySearchProvider.worker = sharedWorkerToRestore;
openmct.router.path = originalRouterPath;
grandSearchComponent.$destroy();
document.body.removeChild(parent);
return resetApplicationState(openmct);
});
@@ -224,62 +203,25 @@ describe("GrandSearch", () => {
it("should render an object search result", async () => {
await grandSearchComponent.$children[0].searchEverything('foo');
await Vue.nextTick();
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');
const searchResult = document.querySelector('[aria-label="fooRabbitNotebook notebook result"]');
expect(searchResult).toBeDefined();
});
it("should render an annotation search result", async () => {
await grandSearchComponent.$children[0].searchEverything('S');
await Vue.nextTick();
const annotationResults = document.querySelectorAll('[aria-label="Search Result"]');
expect(annotationResults.length).toBe(2);
expect(annotationResults[1].innerText).toContain('Driving');
const annotationResult = document.querySelector('[aria-label="Search Result"]');
expect(annotationResult).toBeDefined();
});
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 searchResults = document.querySelectorAll('[name="Test Folder"]');
expect(searchResults.length).toBe(1);
expect(searchResults[0].innerText).toContain('Folder');
searchResults[0].click();
const searchResult = document.querySelector('[name="Test Folder"]');
expect(searchResult).toBeDefined();
searchResult.click();
const previewWindow = document.querySelector('.js-preview-window');
expect(previewWindow.innerText).toContain('Snapshot');
expect(previewWindow).toBeDefined();
});
});

View File

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

View File

@@ -32,16 +32,8 @@ function normalizeAge(num) {
return isWhole ? hundredtized / 100 : num;
}
function padLeadingZeros(num, numOfLeadingZeros) {
return num.toString().padStart(numOfLeadingZeros, '0');
}
function toDoubleDigits(num) {
return padLeadingZeros(num, 2);
}
function toTripleDigits(num) {
return padLeadingZeros(num, 3);
return num >= 10 ? num : `0${num}`;
}
function addTimeSuffix(value, suffix) {
@@ -54,8 +46,7 @@ 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_SECOND)), "ms")
addTimeSuffix(Math.floor(normalizeAge((ms % ONE_MINUTE) / ONE_SECOND)), 's')
].filter(Boolean).join(' ');
return `${ dhms ? '+' : ''} ${dhms}`;
@@ -68,8 +59,7 @@ 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))),
toTripleDigits(Math.floor(normalizeAge(ms % ONE_SECOND)))
toDoubleDigits(Math.floor(normalizeAge((ms % ONE_MINUTE) / ONE_SECOND)))
].join(":");
}

View File

@@ -30,6 +30,7 @@ const config = {
inMemorySearchWorker: './src/api/objects/InMemorySearchWorker.js',
espressoTheme: './src/plugins/themes/espresso-theme.scss',
snowTheme: './src/plugins/themes/snow-theme.scss',
maelstromTheme: './src/plugins/themes/maelstrom-theme.scss'
},
output: {
globalObject: 'this',