diff --git a/.github/workflows/e2e-pr.yml b/.github/workflows/e2e-pr.yml index d90f31c361..b21bf8ce79 100644 --- a/.github/workflows/e2e-pr.yml +++ b/.github/workflows/e2e-pr.yml @@ -30,7 +30,8 @@ jobs: - uses: actions/setup-node@v3 with: node-version: '16' - - run: npx playwright@1.21.1 install + - run: npx playwright@1.23.0 install + - run: npx playwright install chrome-beta - run: npm install - run: npm run test:e2e:full - name: Archive test results diff --git a/.github/workflows/e2e-visual.yml b/.github/workflows/e2e-visual.yml index bd0ec056f5..11c8da3caf 100644 --- a/.github/workflows/e2e-visual.yml +++ b/.github/workflows/e2e-visual.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/setup-node@v3 with: node-version: '16' - - run: npx playwright@1.21.1 install + - run: npx playwright@1.23.0 install - run: npm install - name: Run the e2e visual tests run: npm run test:e2e:visual diff --git a/app.js b/app.js index a1a30ef839..baa951e129 100644 --- a/app.js +++ b/app.js @@ -12,6 +12,7 @@ const express = require('express'); const app = express(); const fs = require('fs'); const request = require('request'); +const __DEV__ = process.env.NODE_ENV === 'development'; // Defaults options.port = options.port || options.p || 8080; @@ -49,14 +50,18 @@ class WatchRunPlugin { } const webpack = require('webpack'); -const webpackConfig = process.env.CI ? require('./webpack.coverage.js') : require('./webpack.dev.js'); -webpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin()); -webpackConfig.plugins.push(new WatchRunPlugin()); - -webpackConfig.entry.openmct = [ - 'webpack-hot-middleware/client?reload=true', - webpackConfig.entry.openmct -]; +let webpackConfig; +if (__DEV__) { + webpackConfig = require('./webpack.dev'); + webpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin()); + webpackConfig.entry.openmct = [ + 'webpack-hot-middleware/client?reload=true', + webpackConfig.entry.openmct + ]; + webpackConfig.plugins.push(new WatchRunPlugin()); +} else { + webpackConfig = require('./webpack.coverage'); +} const compiler = webpack(webpackConfig); @@ -68,10 +73,12 @@ app.use(require('webpack-dev-middleware')( } )); -app.use(require('webpack-hot-middleware')( - compiler, - {} -)); +if (__DEV__) { + app.use(require('webpack-hot-middleware')( + compiler, + {} + )); +} // Expose index.html for development users. app.get('/', function (req, res) { diff --git a/e2e/playwright-ci.config.js b/e2e/playwright-ci.config.js index 4364d11bd4..a0139f56bf 100644 --- a/e2e/playwright-ci.config.js +++ b/e2e/playwright-ci.config.js @@ -4,6 +4,8 @@ // eslint-disable-next-line no-unused-vars const { devices } = require('@playwright/test'); +const MAX_FAILURES = 5; +const NUM_WORKERS = 2; /** @type {import('@playwright/test').PlaywrightTestConfig} */ const config = { @@ -12,20 +14,20 @@ const config = { testIgnore: '**/*.perf.spec.js', //Ignore performance tests and define in playwright-perfromance.config.js timeout: 60 * 1000, webServer: { - command: 'npm run start', + command: 'cross-env NODE_ENV=test npm run start', url: 'http://localhost:8080/#', timeout: 200 * 1000, - reuseExistingServer: !process.env.CI + reuseExistingServer: false }, - maxFailures: process.env.CI ? 5 : undefined, //Limits failures to 5 to reduce CI Waste - workers: 2, //Limit to 2 for CircleCI Agent + maxFailures: MAX_FAILURES, //Limits failures to 5 to reduce CI Waste + workers: NUM_WORKERS, //Limit to 2 for CircleCI Agent use: { baseURL: 'http://localhost:8080/', headless: true, ignoreHTTPSErrors: true, screenshot: 'only-on-failure', trace: 'on-first-retry', - video: 'on-first-retry' + video: 'off' }, projects: [ { diff --git a/e2e/playwright-local.config.js b/e2e/playwright-local.config.js index 54f59b303e..d79c702b19 100644 --- a/e2e/playwright-local.config.js +++ b/e2e/playwright-local.config.js @@ -12,10 +12,10 @@ const config = { testIgnore: '**/*.perf.spec.js', timeout: 30 * 1000, webServer: { - command: 'npm run start', + command: 'cross-env NODE_ENV=test npm run start', url: 'http://localhost:8080/#', timeout: 120 * 1000, - reuseExistingServer: !process.env.CI + reuseExistingServer: true }, workers: 1, use: { @@ -25,7 +25,7 @@ const config = { ignoreHTTPSErrors: true, screenshot: 'only-on-failure', trace: 'retain-on-failure', - video: 'retain-on-failure' + video: 'off' }, projects: [ { diff --git a/e2e/playwright-performance.config.js b/e2e/playwright-performance.config.js index e9b7e38449..de79304f11 100644 --- a/e2e/playwright-performance.config.js +++ b/e2e/playwright-performance.config.js @@ -2,6 +2,8 @@ // playwright.config.js // @ts-check +const CI = process.env.CI === 'true'; + /** @type {import('@playwright/test').PlaywrightTestConfig} */ const config = { retries: 1, //Only for debugging purposes because trace is enabled only on first retry @@ -9,15 +11,15 @@ const config = { timeout: 60 * 1000, workers: 1, //Only run in serial with 1 worker webServer: { - command: 'npm run start', + command: 'cross-env NODE_ENV=test npm run start', url: 'http://localhost:8080/#', timeout: 200 * 1000, - reuseExistingServer: !process.env.CI + reuseExistingServer: !CI }, use: { browserName: "chromium", baseURL: 'http://localhost:8080/', - headless: Boolean(process.env.CI), //Only if running locally + headless: CI, //Only if running locally ignoreHTTPSErrors: true, screenshot: 'off', trace: 'on-first-retry', diff --git a/e2e/playwright-visual.config.js b/e2e/playwright-visual.config.js index 07758121bd..55570b0493 100644 --- a/e2e/playwright-visual.config.js +++ b/e2e/playwright-visual.config.js @@ -9,7 +9,7 @@ const config = { timeout: 90 * 1000, workers: 1, // visual tests should never run in parallel due to test pollution webServer: { - command: 'npm run start', + command: 'cross-env NODE_ENV=test npm run start', url: 'http://localhost:8080/#', timeout: 200 * 1000, reuseExistingServer: !process.env.CI @@ -21,7 +21,7 @@ const config = { ignoreHTTPSErrors: true, screenshot: 'on', trace: 'off', - video: 'on' + video: 'off' }, reporter: [ ['list'], diff --git a/e2e/test-data/VisualTestData_storage.json b/e2e/test-data/VisualTestData_storage.json new file mode 100644 index 0000000000..d059303264 --- /dev/null +++ b/e2e/test-data/VisualTestData_storage.json @@ -0,0 +1,22 @@ +{ + "cookies": [], + "origins": [ + { + "origin": "http://localhost:8080", + "localStorage": [ + { + "name": "tcHistory", + "value": "{\"utc\":[{\"start\":1654548551471,\"end\":1654550351471}]}" + }, + { + "name": "mct", + "value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"527856c0-cced-4b64-bb19-f943432326d0\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1654550352296,\"modified\":1654550352296},\"527856c0-cced-4b64-bb19-f943432326d0\":{\"identifier\":{\"key\":\"527856c0-cced-4b64-bb19-f943432326d0\",\"namespace\":\"\"},\"name\":\"Unnamed Overlay Plot\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"ce88ce37-8bb9-45e1-a85b-bb7e3c8453b9\",\"namespace\":\"\"}],\"configuration\":{\"series\":[{\"identifier\":{\"key\":\"ce88ce37-8bb9-45e1-a85b-bb7e3c8453b9\",\"namespace\":\"\"}}],\"yAxis\":{},\"xAxis\":{}},\"modified\":1654550353356,\"location\":\"mine\",\"persisted\":1654550353357},\"ce88ce37-8bb9-45e1-a85b-bb7e3c8453b9\":{\"name\":\"Unnamed Sine Wave Generator\",\"type\":\"generator\",\"identifier\":{\"key\":\"ce88ce37-8bb9-45e1-a85b-bb7e3c8453b9\",\"namespace\":\"\"},\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":\"5000\"},\"modified\":1654550353350,\"location\":\"527856c0-cced-4b64-bb19-f943432326d0\",\"persisted\":1654550353350}}" + }, + { + "name": "mct-tree-expanded", + "value": "[\"/browse/mine\"]" + } + ] + } + ] +} \ No newline at end of file diff --git a/e2e/test-data/recycled_local_storage.json b/e2e/test-data/recycled_local_storage.json new file mode 100644 index 0000000000..5026bca3bb --- /dev/null +++ b/e2e/test-data/recycled_local_storage.json @@ -0,0 +1,22 @@ +{ + "cookies": [], + "origins": [ + { + "origin": "http://localhost:8080", + "localStorage": [ + { + "name": "tcHistory", + "value": "{\"utc\":[{\"start\":1654537164464,\"end\":1654538964464},{\"start\":1652301954635,\"end\":1652303754635}]}" + }, + { + "name": "mct", + "value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1654538965703,\"modified\":1654538965703},\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"73f2d9ae-d1f3-4561-b7fc-ecd5df557249\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1652303755999,\"location\":\"mine\",\"persisted\":1652303756002},\"2d02a680-eb7e-4645-bba2-dd298f76efb8\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"4291d80c-303c-4d8d-85e1-10f012b864fb\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1654538965702,\"location\":\"mine\",\"persisted\":1654538965702}}" + }, + { + "name": "mct-tree-expanded", + "value": "[]" + } + ] + } + ] +} \ No newline at end of file diff --git a/e2e/tests/branding.e2e.spec.js b/e2e/tests/branding.e2e.spec.js index a37101c8a0..b543abf864 100644 --- a/e2e/tests/branding.e2e.spec.js +++ b/e2e/tests/branding.e2e.spec.js @@ -36,7 +36,7 @@ test.describe('Branding tests', () => { await page.click('.l-shell__app-logo'); // Verify that the NASA Logo Appears - await expect(await page.locator('.c-about__image')).toBeVisible(); + await expect(page.locator('.c-about__image')).toBeVisible(); // Modify the Build information in 'about' Modal const versionInformationLocator = page.locator('ul.t-info.l-info.s-info'); diff --git a/e2e/tests/framework.e2e.spec.js b/e2e/tests/framework.e2e.spec.js new file mode 100644 index 0000000000..b262ddc316 --- /dev/null +++ b/e2e/tests/framework.e2e.spec.js @@ -0,0 +1,55 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2022, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +/* +This test suite is dedicated to testing our use of the playwright framework as it +relates to how we've extended it (i.e. ./e2e/fixtures.js) and assumptions made in our dev environment +(app.js and ./e2e/webpack-dev-middleware.js) +*/ + +const { test } = require('../fixtures.js'); + +test.describe('fixtures.js tests', () => { + test('Verify that tests fail if console.error is thrown', async ({ page }) => { + test.fail(); + //Go to baseURL + await page.goto('/', { waitUntil: 'networkidle' }); + + //Verify that ../fixtures.js detects console log errors + await Promise.all([ + page.evaluate(() => console.error('This should result in a failure')), + page.waitForEvent('console') // always wait for the event to happen while triggering it! + ]); + + }); + test('Verify that tests pass if console.warn is thrown', async ({ page }) => { + //Go to baseURL + await page.goto('/', { waitUntil: 'networkidle' }); + + //Verify that ../fixtures.js detects console log errors + await Promise.all([ + page.evaluate(() => console.warn('This should result in a pass')), + page.waitForEvent('console') // always wait for the event to happen while triggering it! + ]); + + }); +}); diff --git a/e2e/tests/moveObjects.e2e.spec.js b/e2e/tests/moveObjects.e2e.spec.js index de196bde50..e7afc2f68c 100644 --- a/e2e/tests/moveObjects.e2e.spec.js +++ b/e2e/tests/moveObjects.e2e.spec.js @@ -40,9 +40,6 @@ test.describe('Move item tests', () => { await page.locator('text=Properties Title Notes >> input[type="text"]').click(); await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder1); - // Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184 - await page.click('form[name="mctForm"] a:has-text("My Items")'); - await Promise.all([ page.waitForNavigation(), page.locator('text=OK').click(), @@ -94,9 +91,6 @@ test.describe('Move item tests', () => { await page.locator('text=Properties Title Notes >> input[type="text"]').click(); await page.locator('text=Properties Title Notes >> input[type="text"]').fill(telemetryTable); - // Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184 - await page.click('form[name="mctForm"] a:has-text("My Items")'); - await page.locator('text=OK').click(); // Finish editing and save Telemetry Table diff --git a/e2e/tests/persistence/persistability.e2e.spec.js b/e2e/tests/persistence/persistability.e2e.spec.js index a9bec6a18a..aa7c0b9d73 100644 --- a/e2e/tests/persistence/persistability.e2e.spec.js +++ b/e2e/tests/persistence/persistability.e2e.spec.js @@ -28,9 +28,7 @@ const { test } = require('../../fixtures.js'); const { expect } = require('@playwright/test'); const path = require('path'); -// https://github.com/nasa/openmct/issues/4323#issuecomment-1067282651 - -test.describe('Persistence operations', () => { +test.describe('Persistence operations @addInit', () => { // add non persistable root item test.beforeEach(async ({ page }) => { // eslint-disable-next-line no-undef @@ -38,6 +36,10 @@ test.describe('Persistence operations', () => { }); test('Persistability should be respected in the create form location field', async ({ page }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/nasa/openmct/issues/4323' + }); // Go to baseURL await page.goto('/', { waitUntil: 'networkidle' }); diff --git a/e2e/tests/plugins/condition/condition.e2e.spec.js b/e2e/tests/plugins/condition/condition.e2e.spec.js index 3f3144c441..3f2558b8ec 100644 --- a/e2e/tests/plugins/condition/condition.e2e.spec.js +++ b/e2e/tests/plugins/condition/condition.e2e.spec.js @@ -51,20 +51,20 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => { page.click('text=OK') ]); - //Save localStorage for future test execution. - await context.storageState({ path: './e2e/tests/recycled_storage.json' }); + //Save localStorage for future test execution + await context.storageState({ path: './e2e/test-data/recycled_local_storage.json' }); //Set object identifier from url - conditionSetUrl = await page.url(); + conditionSetUrl = page.url(); console.log('conditionSetUrl ' + conditionSetUrl); - getConditionSetIdentifierFromUrl = await conditionSetUrl.split('/').pop().split('?')[0]; + getConditionSetIdentifierFromUrl = conditionSetUrl.split('/').pop().split('?')[0]; console.debug('getConditionSetIdentifierFromUrl ' + getConditionSetIdentifierFromUrl); await page.close(); }); - //Load localStorage for subsequent tests. Note: this requires a file already in place -- even if blank. - test.use({ storageState: './e2e/tests/recycled_storage.json' }); + //Load localStorage for subsequent tests + test.use({ storageState: './e2e/test-data/recycled_local_storage.json' }); //Begin suite of tests again localStorage test('Condition set object properties persist in main view and inspector @localStorage', async ({ page }) => { //Navigate to baseURL with injected localStorage @@ -74,7 +74,7 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => { await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set'); //Assertions on loaded Condition Set in Inspector - await expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy; + expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy(); //Reload Page await Promise.all([ @@ -85,7 +85,7 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => { //Re-verify after reload await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set'); //Assertions on loaded Condition Set in Inspector - await expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy; + expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy(); }); test('condition set object can be modified on @localStorage', async ({ page }) => { @@ -111,18 +111,18 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => { // Verify Inspector properties // Verify Inspector has updated Name property - await expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy(); + expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy(); // Verify Inspector Details has updated Name property - await expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy(); + expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy(); // Verify Tree reflects updated Name proprety // Expand Tree await page.locator('text=Open MCT My Items >> span >> nth=3').click(); // Verify Condition Set Object is renamed in Tree - await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); + expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); // Verify Search Tree reflects renamed Name property await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Renamed'); - await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); + expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); //Reload Page await Promise.all([ @@ -135,18 +135,18 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => { // Verify Inspector properties // Verify Inspector has updated Name property - await expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy(); + expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy(); // Verify Inspector Details has updated Name property - await expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy(); + expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy(); // Verify Tree reflects updated Name proprety // Expand Tree await page.locator('text=Open MCT My Items >> span >> nth=3').click(); // Verify Condition Set Object is renamed in Tree - await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); + expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); // Verify Search Tree reflects renamed Name property await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Renamed'); - await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); + expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); }); test('condition set object can be deleted by Search Tree Actions menu on @localStorage', async ({ page }) => { //Navigate to baseURL diff --git a/e2e/tests/plugins/imagery/exampleImagery.e2e.spec.js b/e2e/tests/plugins/imagery/exampleImagery.e2e.spec.js index 1c5790340f..9777695f5c 100644 --- a/e2e/tests/plugins/imagery/exampleImagery.e2e.spec.js +++ b/e2e/tests/plugins/imagery/exampleImagery.e2e.spec.js @@ -31,6 +31,7 @@ const { expect } = require('@playwright/test'); const backgroundImageSelector = '.c-imagery__main-image__background-image'; +//The following block of tests verifies the basic functionality of example imagery and serves as a template for Imagery objects embedded in other objects. test.describe('Example Imagery Object', () => { test.beforeEach(async ({ page }) => { @@ -43,10 +44,7 @@ test.describe('Example Imagery Object', () => { // Click text=Example Imagery await page.click('text=Example Imagery'); - // Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184 - await page.click('form[name="mctForm"] a:has-text("My Items")'); - - // Click text=OK and wait for save banner to appear + // Click text=OK await Promise.all([ page.waitForNavigation({waitUntil: 'networkidle'}), page.click('text=OK'), @@ -183,7 +181,8 @@ test.describe('Example Imagery Object', () => { }); - test('Can use the reset button to reset the image', async ({ page }) => { + test('Can use the reset button to reset the image', async ({ page }, testInfo) => { + test.slow(testInfo.project === 'chrome-beta', "This test is slow in chrome-beta"); // wait for zoom animation to finish await page.locator(backgroundImageSelector).hover({trial: true}); @@ -202,16 +201,17 @@ test.describe('Example Imagery Object', () => { expect.soft(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height); expect.soft(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width); - await zoomResetBtn.click(); // wait for zoom animation to finish - await page.locator(backgroundImageSelector).hover({trial: true}); + // FIXME: The zoom is flakey, sometimes not returning to original dimensions + // https://github.com/nasa/openmct/issues/5491 + await expect.poll(async () => { + await zoomResetBtn.click(); + const boundingBox = await page.locator(backgroundImageSelector).boundingBox(); - const resetBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); - expect.soft(resetBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height); - expect.soft(resetBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width); - - expect.soft(resetBoundingBox.height).toEqual(initialBoundingBox.height); - expect(resetBoundingBox.width).toEqual(initialBoundingBox.width); + return boundingBox; + }, { + timeout: 10 * 1000 + }).toEqual(initialBoundingBox); }); test('Using the zoom features does not pause telemetry', async ({ page }) => { @@ -316,7 +316,14 @@ test('Example Imagery in Display layout', async ({ page, browserName }) => { await page.locator('[data-testid=conductor-modeOption-realtime]').click(); // Zoom in on next image - await mouseZoomIn(page); + await page.locator(backgroundImageSelector).hover({trial: true}); + await page.mouse.wheel(0, deltaYStep * 2); + + // Wait for zoom animation to finish + await page.locator(backgroundImageSelector).hover({trial: true}); + const imageNextMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox(); + expect(imageNextMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height); + expect(imageNextMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width); // Click previous image button await previousImageButton.click(); @@ -330,9 +337,9 @@ test('Example Imagery in Display layout', async ({ page, browserName }) => { return newImageCount; }, { - message: "verify that new images still stream in", + message: "verify that old images are discarded", timeout: 6 * 1000 - }).toBeGreaterThan(imageCount); + }).toBe(imageCount); // Verify selected image is still displayed await expect(selectedImage).toBeVisible(); @@ -352,7 +359,6 @@ test('Example Imagery in Display layout', async ({ page, browserName }) => { }); test.describe('Example imagery thumbnails resize in display layouts', () => { - test('Resizing the layout changes thumbnail visibility and size', async ({ page }) => { await page.goto('/', { waitUntil: 'networkidle' }); @@ -557,9 +563,9 @@ test.describe('Example Imagery in Flexible layout', () => { return newImageCount; }, { - message: "verify that new images still stream in", + message: "verify that old images are discarded", timeout: 6 * 1000 - }).toBeGreaterThan(imageCount); + }).toBe(imageCount); // Verify selected image is still displayed await expect(selectedImage).toBeVisible(); @@ -579,6 +585,16 @@ test.describe('Example Imagery in Flexible layout', () => { }); }); +test.describe('Example Imagery in Tabs view', () => { + 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('Can zoom into the latest image and the real-time/fixed-time imagery will pause'); + test.fixme('Can zoom into a previous image from thumbstrip in real-time or fixed-time'); + 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, it should not be updated when new images come in'); + test.fixme('If the imagery view is not in pause mode, it should be updated when new images come in'); +}); + /** * @param {import('@playwright/test').Page} page */ @@ -645,22 +661,6 @@ async function assertBackgroundImageBrightness(page, expected) { expect(actual).toBe(expected); } -/** - * Gets the filter:contrast value of the current background-image and - * asserts against an expected value - * @param {import('@playwright/test').Page} page - * @param {String} expected The expected contrast value - */ -async function assertBackgroundImageContrast(page, expected) { - const backgroundImage = page.locator('.c-imagery__main-image__background-image'); - - // Get the contrast filter value (i.e: filter: contrast(500%) => "500") - const actual = await backgroundImage.evaluate((el) => { - return el.style.filter.match(/contrast\((\d{1,3})%\)/)[1]; - }); - expect(actual).toBe(expected); -} - /** * @param {import('@playwright/test').Page} page */ @@ -761,12 +761,19 @@ async function mouseZoomIn(page) { expect(imageMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width); } -test.describe('Example Imagery in Tabs view', () => { - 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('Can zoom into the latest image and the real-time/fixed-time imagery will pause'); - test.fixme('Can zoom into a previous image from thumbstrip in real-time or fixed-time'); - 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, it should not be updated when new images come in'); - test.fixme('If the imagery view is not in pause mode, it should be updated when new images come in'); -}); +/** + * Gets the filter:contrast value of the current background-image and + * asserts against an expected value + * @param {import('@playwright/test').Page} page + * @param {String} expected The expected contrast value + */ +async function assertBackgroundImageContrast(page, expected) { + const backgroundImage = page.locator('.c-imagery__main-image__background-image'); + + // Get the contrast filter value (i.e: filter: contrast(500%) => "500") + const actual = await backgroundImage.evaluate((el) => { + return el.style.filter.match(/contrast\((\d{1,3})%\)/)[1]; + }); + expect(actual).toBe(expected); +} + diff --git a/e2e/tests/plugins/notebook/restrictedNotebook.e2e.spec.js b/e2e/tests/plugins/notebook/restrictedNotebook.e2e.spec.js index 8aafc66336..56ce843921 100644 --- a/e2e/tests/plugins/notebook/restrictedNotebook.e2e.spec.js +++ b/e2e/tests/plugins/notebook/restrictedNotebook.e2e.spec.js @@ -34,11 +34,11 @@ test.describe('Restricted Notebook', () => { await startAndAddRestrictedNotebookObject(page); }); - test('Can be renamed', async ({ page }) => { - await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText(`Unnamed ${CUSTOM_NAME}`); + test('Can be renamed @addInit', async ({ page }) => { + await expect(page.locator('.l-browse-bar__object-name')).toContainText(`Unnamed ${CUSTOM_NAME}`); }); - test('Can be deleted if there are no locked pages', async ({ page }) => { + test('Can be deleted if there are no locked pages @addInit', async ({ page }) => { await openContextMenuRestrictedNotebook(page); const menuOptions = page.locator('.c-menu ul'); @@ -52,7 +52,7 @@ test.describe('Restricted Notebook', () => { // Click Remove Text await page.locator('text=Remove').click(); - //Wait until Save Banner is gone + // Click 'OK' on confirmation window and wait for save banner to appear await Promise.all([ page.waitForNavigation(), page.locator('text=OK').click(), @@ -61,31 +61,37 @@ test.describe('Restricted Notebook', () => { await page.locator('.c-message-banner__close-button').click(); // has been deleted - expect.soft(await restrictedNotebookTreeObject.count()).toEqual(0); + expect(await restrictedNotebookTreeObject.count()).toEqual(0); }); - test('Can be locked if at least one page has one entry', async ({ page }) => { + test('Can be locked if at least one page has one entry @addInit', async ({ page }) => { await enterTextEntry(page); const commitButton = page.locator('button:has-text("Commit Entries")'); - expect.soft(await commitButton.count()).toEqual(1); + expect(await commitButton.count()).toEqual(1); }); }); -test.describe('Restricted Notebook with at least one entry and with the page locked', () => { +test.describe('Restricted Notebook with at least one entry and with the page locked @addInit', () => { test.beforeEach(async ({ page }) => { await startAndAddRestrictedNotebookObject(page); await enterTextEntry(page); await lockPage(page); + // FIXME: Give ample time for the mutation to happen + // https://github.com/nasa/openmct/issues/5409 + // eslint-disable-next-line playwright/no-wait-for-timeout + await page.waitForTimeout(1 * 1000); + // open sidebar await page.locator('button.c-notebook__toggle-nav-button').click(); }); - test.fixme('Locked page should now be in a locked state', async ({ page }) => { + test('Locked page should now be in a locked state @addInit', async ({ page }, testInfo) => { + test.fixme(testInfo.project === 'chrome-beta', "Test is unreliable on chrome-beta"); // main lock message on page const lockMessage = page.locator('text=This page has been committed and cannot be modified or removed'); expect.soft(await lockMessage.count()).toEqual(1); @@ -96,14 +102,12 @@ test.describe('Restricted Notebook with at least one entry and with the page loc // no way to remove a restricted notebook with a locked page await openContextMenuRestrictedNotebook(page); - const menuOptions = page.locator('.c-menu ul'); - await expect.soft(menuOptions).not.toContainText('Remove'); - + await expect(menuOptions).not.toContainText('Remove'); }); - test('Can still: add page, rename, add entry, delete unlocked pages', async ({ page }) => { + test('Can still: add page, rename, add entry, delete unlocked pages @addInit', async ({ page }) => { // Click text=Page Add >> button await Promise.all([ page.waitForNavigation(), @@ -139,32 +143,32 @@ test.describe('Restricted Notebook with at least one entry and with the page loc // deleted page, should no longer exist const deletedPageElement = page.locator(`text=${TEST_TEXT_NAME}`); - expect.soft(await deletedPageElement.count()).toEqual(0); + expect(await deletedPageElement.count()).toEqual(0); }); }); -test.describe('Restricted Notebook with a page locked and with an embed', () => { +test.describe('Restricted Notebook with a page locked and with an embed @addInit', () => { test.beforeEach(async ({ page }) => { await startAndAddRestrictedNotebookObject(page); await dragAndDropEmbed(page); }); - test('Allows embeds to be deleted if page unlocked', async ({ page }) => { + test('Allows embeds to be deleted if page unlocked @addInit', async ({ page }) => { // Click .c-ne__embed__name .c-popup-menu-button await page.locator('.c-ne__embed__name .c-popup-menu-button').click(); // embed popup menu const embedMenu = page.locator('body >> .c-menu'); - await expect.soft(embedMenu).toContainText('Remove This Embed'); + await expect(embedMenu).toContainText('Remove This Embed'); }); - test('Disallows embeds to be deleted if page locked', async ({ page }) => { + test('Disallows embeds to be deleted if page locked @addInit', async ({ page }) => { await lockPage(page); // Click .c-ne__embed__name .c-popup-menu-button await page.locator('.c-ne__embed__name .c-popup-menu-button').click(); // embed popup menu const embedMenu = page.locator('body >> .c-menu'); - await expect.soft(embedMenu).not.toContainText('Remove This Embed'); + await expect(embedMenu).not.toContainText('Remove This Embed'); }); }); @@ -232,28 +236,18 @@ async function lockPage(page) { await commitButton.click(); //Wait until Lock Banner is visible - await Promise.all([ - page.locator('text=Lock Page').click(), - page.waitForSelector('.c-message-banner__message') - ]); - // Close Lock Banner - await page.locator('.c-message-banner__close-button').click(); - - //artifically wait to avoid mutation delay TODO: https://github.com/nasa/openmct/issues/5409 - // eslint-disable-next-line playwright/no-wait-for-timeout - await page.waitForTimeout(1 * 1000); + await page.locator('text=Lock Page').click(); } /** * @param {import('@playwright/test').Page} page */ async function openContextMenuRestrictedNotebook(page) { - // Click text=Open MCT My Items (This expands the My Items folder to show it's chilren in the tree) - await page.locator('text=Open MCT My Items >> span').nth(3).click(); - - //artifically wait to avoid mutation delay TODO: https://github.com/nasa/openmct/issues/5409 - // eslint-disable-next-line playwright/no-wait-for-timeout - await page.waitForTimeout(1 * 1000); + const myItemsFolder = page.locator('text=Open MCT My Items >> span').nth(3); + const className = await myItemsFolder.getAttribute('class'); + if (!className.includes('c-disclosure-triangle--expanded')) { + await myItemsFolder.click(); + } // Click a:has-text("Unnamed CUSTOM_NAME") await page.locator(`a:has-text("Unnamed ${CUSTOM_NAME}")`).click({ diff --git a/e2e/tests/plugins/notebook/tags.e2e.spec.js b/e2e/tests/plugins/notebook/tags.e2e.spec.js new file mode 100644 index 0000000000..c7f145c2ee --- /dev/null +++ b/e2e/tests/plugins/notebook/tags.e2e.spec.js @@ -0,0 +1,205 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2022, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +/* +This test suite is dedicated to tests which verify form functionality. +*/ + +const { expect } = require('@playwright/test'); +const { test } = require('../../../fixtures'); + +/** + * Creates a notebook object and adds an entry. + * @param {import('@playwright/test').Page} - page to load + * @param {number} [iterations = 1] - the number of entries to create + */ +async function createNotebookAndEntry(page, iterations = 1) { + //Go to baseURL + await page.goto('/', { waitUntil: 'networkidle' }); + + // Click button:has-text("Create") + await page.locator('button:has-text("Create")').click(); + await page.locator('[title="Create and save timestamped notes with embedded object snapshots."]').click(); + // Click button:has-text("OK") + await Promise.all([ + page.waitForNavigation(), + page.locator('[name="mctForm"] >> text=My Items').click(), + page.locator('button:has-text("OK")').click() + ]); + + for (let iteration = 0; iteration < iterations; iteration++) { + // Click text=To start a new entry, click here or drag and drop any object + await page.locator('text=To start a new entry, click here or drag and drop any object').click(); + const entryLocator = `[aria-label="Notebook Entry Input"] >> nth = ${iteration}`; + await page.locator(entryLocator).click(); + await page.locator(entryLocator).fill(`Entry ${iteration}`); + } + +} + +/** + * Creates a notebook object, adds an entry, and adds a tag. + * @param {import('@playwright/test').Page} page + * @param {number} [iterations = 1] - the number of entries (and tags) to create + */ +async function createNotebookEntryAndTags(page, iterations = 1) { + await createNotebookAndEntry(page, iterations); + + for (let iteration = 0; iteration < iterations; iteration++) { + // Click text=To start a new entry, click here or drag and drop any object + await page.locator(`button:has-text("Add Tag") >> nth = ${iteration}`).click(); + + // Click [placeholder="Type to select tag"] + await page.locator('[placeholder="Type to select tag"]').click(); + // Click text=Driving + await page.locator('[aria-label="Autocomplete Options"] >> text=Driving').click(); + + // Click button:has-text("Add Tag") + await page.locator(`button:has-text("Add Tag") >> nth = ${iteration}`).click(); + // Click [placeholder="Type to select tag"] + await page.locator('[placeholder="Type to select tag"]').click(); + // Click text=Science + await page.locator('[aria-label="Autocomplete Options"] >> text=Science').click(); + } +} + +test.describe('Tagging in Notebooks', () => { + test('Can load tags', async ({ page }) => { + await createNotebookAndEntry(page); + // Click text=To start a new entry, click here or drag and drop any object + await page.locator('button:has-text("Add Tag")').click(); + + // Click [placeholder="Type to select tag"] + await page.locator('[placeholder="Type to select tag"]').click(); + + await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Science"); + await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Drilling"); + await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Driving"); + }); + test('Can add tags', async ({ page }) => { + await createNotebookEntryAndTags(page); + + await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Science"); + await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Driving"); + + // Click button:has-text("Add Tag") + await page.locator('button:has-text("Add Tag")').click(); + // Click [placeholder="Type to select tag"] + await page.locator('[placeholder="Type to select tag"]').click(); + + await expect(page.locator('[aria-label="Autocomplete Options"]')).not.toContainText("Science"); + await expect(page.locator('[aria-label="Autocomplete Options"]')).not.toContainText("Driving"); + await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Drilling"); + }); + test('Can search for tags', async ({ page }) => { + await createNotebookEntryAndTags(page); + // Click [aria-label="OpenMCT Search"] input[type="search"] + await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); + // Fill [aria-label="OpenMCT Search"] input[type="search"] + await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc'); + await expect(page.locator('[aria-label="Search Result"]')).toContainText("Science"); + await expect(page.locator('[aria-label="Search Result"]')).toContainText("Driving"); + + // Click [aria-label="OpenMCT Search"] input[type="search"] + await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); + // Fill [aria-label="OpenMCT Search"] input[type="search"] + await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Sc'); + await expect(page.locator('[aria-label="Search Result"]')).toContainText("Science"); + await expect(page.locator('[aria-label="Search Result"]')).toContainText("Driving"); + + // Click [aria-label="OpenMCT Search"] input[type="search"] + await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); + // Fill [aria-label="OpenMCT Search"] input[type="search"] + await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Xq'); + await expect(page.locator('[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.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"); + + // Fill [aria-label="OpenMCT Search"] input[type="search"] + await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc'); + await expect(page.locator('[aria-label="Search Result"]')).not.toContainText("Driving"); + }); + test('Tags persist across reload', async ({ page }) => { + //Go to baseURL + await page.goto('/', { waitUntil: 'networkidle' }); + + // Create a clock object we can navigate to + await page.click('button:has-text("Create")'); + + // Click Clock + await page.click('text=Clock'); + // Click button:has-text("OK") + await Promise.all([ + page.waitForNavigation(), + page.locator('[name="mctForm"] >> text=My Items').click(), + page.locator('button:has-text("OK")').click() + ]); + + await page.click('.c-disclosure-triangle'); + + const ITERATIONS = 4; + await createNotebookEntryAndTags(page, ITERATIONS); + + for (let iteration = 0; iteration < ITERATIONS; iteration++) { + const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`; + await expect(page.locator(entryLocator)).toContainText("Science"); + await expect(page.locator(entryLocator)).toContainText("Driving"); + } + + // Click Unnamed Clock + await page.click('text="Unnamed Clock"'); + + // Click Unnamed Notebook + await page.click('text="Unnamed Notebook"'); + + for (let iteration = 0; iteration < ITERATIONS; iteration++) { + const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`; + await expect(page.locator(entryLocator)).toContainText("Science"); + await expect(page.locator(entryLocator)).toContainText("Driving"); + } + + //Reload Page + await Promise.all([ + page.reload(), + page.waitForLoadState('networkidle') + ]); + + // Click Unnamed Notebook + await page.click('text="Unnamed Notebook"'); + + for (let iteration = 0; iteration < ITERATIONS; iteration++) { + const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`; + await expect(page.locator(entryLocator)).toContainText("Science"); + await expect(page.locator(entryLocator)).toContainText("Driving"); + } + + }); +}); diff --git a/e2e/tests/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-panned-chrome-darwin b/e2e/tests/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-panned-chrome-darwin index 01850a3bc4..bea4d7c408 100644 Binary files a/e2e/tests/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-panned-chrome-darwin and b/e2e/tests/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-panned-chrome-darwin differ diff --git a/e2e/tests/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-panned-chrome-linux b/e2e/tests/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-panned-chrome-linux index 7fb1ec390d..345901fcce 100644 Binary files a/e2e/tests/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-panned-chrome-linux and b/e2e/tests/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-panned-chrome-linux differ diff --git a/e2e/tests/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-prepan-chrome-darwin b/e2e/tests/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-prepan-chrome-darwin index 75b1c4d953..fc3db7c140 100644 Binary files a/e2e/tests/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-prepan-chrome-darwin and b/e2e/tests/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-prepan-chrome-darwin differ diff --git a/e2e/tests/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-prepan-chrome-linux b/e2e/tests/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-prepan-chrome-linux index 031025d8e0..88e71e7895 100644 Binary files a/e2e/tests/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-prepan-chrome-linux and b/e2e/tests/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-prepan-chrome-linux differ diff --git a/e2e/tests/plugins/plot/missingPlotObj.e2e.spec.js b/e2e/tests/plugins/plot/missingPlotObj.e2e.spec.js new file mode 100644 index 0000000000..a10f990c53 --- /dev/null +++ b/e2e/tests/plugins/plot/missingPlotObj.e2e.spec.js @@ -0,0 +1,155 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2022, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +/* +Tests to verify log plot functionality when objects are missing +*/ + +const { test } = require('../../../fixtures.js'); +const { expect } = require('@playwright/test'); + +test.describe('Handle missing object for plots', () => { + test('Displays empty div for missing stacked plot item', async ({ page, browserName }) => { + test.fixme(browserName === 'firefox', 'Firefox failing due to console events being missed'); + const errorLogs = []; + + page.on("console", (message) => { + if (message.type() === 'warning' && message.text().includes('Missing domain object')) { + errorLogs.push(message.text()); + } + }); + + //Make stacked plot + await makeStackedPlot(page); + + //Gets local storage and deletes the last sine wave generator in the stacked plot + const localStorage = await page.evaluate(() => window.localStorage); + const parsedData = JSON.parse(localStorage.mct); + const keys = Object.keys(parsedData); + const lastKey = keys[keys.length - 1]; + + delete parsedData[lastKey]; + + //Sets local storage with missing object + await page.evaluate( + `window.localStorage.setItem('mct', '${JSON.stringify(parsedData)}')` + ); + + //Reloads page and clicks on stacked plot + await Promise.all([ + page.reload(), + page.waitForLoadState('networkidle') + ]); + + //Verify Main section is there on load + await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Stacked Plot'); + + await page.locator('text=Open MCT My Items >> span').nth(3).click(); + await Promise.all([ + page.waitForNavigation(), + page.locator('text=Unnamed Stacked Plot').first().click() + ]); + + //Check that there is only one stacked item plot with a plot, the missing one will be empty + await expect(page.locator(".c-plot--stacked-container:has(.gl-plot)")).toHaveCount(1); + //Verify that console.warn is thrown + expect(errorLogs).toHaveLength(1); + }); +}); + +/** + * This is used the create a stacked plot object + * @private + */ +async function makeStackedPlot(page) { + // fresh page with time range from 2022-03-29 22:00:00.000Z to 2022-03-29 22:00:30.000Z + await page.goto('/', { waitUntil: 'networkidle' }); + + // create stacked plot + await page.locator('button.c-create-button').click(); + await page.locator('li:has-text("Stacked Plot")').click(); + + await Promise.all([ + page.waitForNavigation({ waitUntil: 'networkidle'}), + page.locator('text=OK').click(), + //Wait for Save Banner to appear + page.waitForSelector('.c-message-banner__message') + ]); + + // save the stacked plot + await saveStackedPlot(page); + + // create a sinewave generator + await createSineWaveGenerator(page); + + // click on stacked plot + await page.locator('text=Open MCT My Items >> span').nth(3).click(); + await Promise.all([ + page.waitForNavigation(), + page.locator('text=Unnamed Stacked Plot').first().click() + ]); + + // create a second sinewave generator + await createSineWaveGenerator(page); + + // click on stacked plot + await page.locator('text=Open MCT My Items >> span').nth(3).click(); + await Promise.all([ + page.waitForNavigation(), + page.locator('text=Unnamed Stacked Plot').first().click() + ]); +} + +/** + * This is used to save a stacked plot object + * @private + */ +async function saveStackedPlot(page) { + // save stacked plot + await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click(); + + await Promise.all([ + page.locator('text=Save and Finish Editing').click(), + //Wait for Save Banner to appear + page.waitForSelector('.c-message-banner__message') + ]); + //Wait until Save Banner is gone + await page.locator('.c-message-banner__close-button').click(); + await page.waitForSelector('.c-message-banner__message', { state: 'detached' }); +} + +/** + * This is used to create a sine wave generator object + * @private + */ +async function createSineWaveGenerator(page) { + //Create sine wave generator + await page.locator('button.c-create-button').click(); + await page.locator('li:has-text("Sine Wave Generator")').click(); + + await Promise.all([ + page.waitForNavigation({ waitUntil: 'networkidle'}), + page.locator('text=OK').click(), + //Wait for Save Banner to appear + page.waitForSelector('.c-message-banner__message') + ]); +} diff --git a/e2e/tests/plugins/remoteClock/remoteClock.e2e.spec.js b/e2e/tests/plugins/remoteClock/remoteClock.e2e.spec.js new file mode 100644 index 0000000000..f9167d9e4e --- /dev/null +++ b/e2e/tests/plugins/remoteClock/remoteClock.e2e.spec.js @@ -0,0 +1,41 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2022, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +const { test } = require('../../../fixtures.js'); +// eslint-disable-next-line no-unused-vars +const { expect } = require('@playwright/test'); + +test.describe('Remote Clock', () => { + // eslint-disable-next-line require-await + test.fixme('blocks historical requests until first tick is received', async ({ page }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/nasa/openmct/issues/5221' + }); + // addInitScript to with remote clock + // Switch time conductor mode to 'remote clock' + // Navigate to telemetry + // Verify that the plot renders historical data within the correct bounds + // Refresh the page + // Verify again that the plot renders historical data within the correct bounds + }); +}); diff --git a/e2e/tests/plugins/telemetryTable/telemetryTable.e2e.spec.js b/e2e/tests/plugins/telemetryTable/telemetryTable.e2e.spec.js index ceffa3ae5f..192c116283 100644 --- a/e2e/tests/plugins/telemetryTable/telemetryTable.e2e.spec.js +++ b/e2e/tests/plugins/telemetryTable/telemetryTable.e2e.spec.js @@ -24,7 +24,7 @@ const { test } = require('../../../fixtures'); const { expect } = require('@playwright/test'); test.describe('Telemetry Table', () => { - test('unpauses when paused by button and user changes bounds', async ({ page }) => { + test('unpauses and filters data when paused by button and user changes bounds', async ({ page }) => { test.info().annotations.push({ type: 'issue', description: 'https://github.com/nasa/openmct/issues/5113' @@ -39,9 +39,6 @@ test.describe('Telemetry Table', () => { await page.locator(createButton).click(); await page.locator('li:has-text("Telemetry Table")').click(); - // Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184 - await page.click('form[name="mctForm"] a:has-text("My Items")'); - await Promise.all([ page.waitForNavigation(), page.locator('text=OK').click(), @@ -59,9 +56,6 @@ test.describe('Telemetry Table', () => { // add Sine Wave Generator with defaults await page.locator('li:has-text("Sine Wave Generator")').click(); - // Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184 - await page.click('form[name="mctForm"] a:has-text("My Items")'); - await Promise.all([ page.waitForNavigation(), page.locator('text=OK').click(), @@ -77,25 +71,34 @@ test.describe('Telemetry Table', () => { ]); // Click pause button - const pauseButton = await page.locator('button.c-button.icon-pause'); + const pauseButton = page.locator('button.c-button.icon-pause'); await pauseButton.click(); - const tableWrapper = await page.locator('div.c-table-wrapper'); + const tableWrapper = page.locator('div.c-table-wrapper'); await expect(tableWrapper).toHaveClass(/is-paused/); - // Arbitrarily change end date to some time in the future + // Subtract 5 minutes from the current end bound datetime and set it const endTimeInput = page.locator('input[type="text"].c-input--datetime').nth(1); await endTimeInput.click(); let endDate = await endTimeInput.inputValue(); endDate = new Date(endDate); - endDate.setUTCDate(endDate.getUTCDate() + 1); - endDate = endDate.toISOString().replace(/T.*/, ''); + + endDate.setUTCMinutes(endDate.getUTCMinutes() - 5); + endDate = endDate.toISOString().replace(/T/, ' '); await endTimeInput.fill(''); await endTimeInput.fill(endDate); await page.keyboard.press('Enter'); await expect(tableWrapper).not.toHaveClass(/is-paused/); + + // Get the most recent telemetry date + const latestTelemetryDate = await page.locator('table.c-telemetry-table__body > tbody > tr').last().locator('td').nth(1).getAttribute('title'); + + // Verify that it is <= our new end bound + const latestMilliseconds = Date.parse(latestTelemetryDate); + const endBoundMilliseconds = Date.parse(endDate); + expect(latestMilliseconds).toBeLessThanOrEqual(endBoundMilliseconds); }); }); diff --git a/e2e/tests/plugins/timer/timer.e2e.spec.js b/e2e/tests/plugins/timer/timer.e2e.spec.js new file mode 100644 index 0000000000..3c8a051a90 --- /dev/null +++ b/e2e/tests/plugins/timer/timer.e2e.spec.js @@ -0,0 +1,185 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2022, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +const { test } = require('../../../fixtures.js'); +const { expect } = require('@playwright/test'); + +test.describe('Timer', () => { + + test.beforeEach(async ({ page }) => { + //Go to baseURL + await page.goto('/', { waitUntil: 'networkidle' }); + + //Click the Create button + await page.click('button:has-text("Create")'); + + // Click 'Timer' + await page.click('text=Timer'); + + // Click text=OK + await Promise.all([ + page.waitForNavigation({waitUntil: 'networkidle'}), + page.click('text=OK') + ]); + + await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Timer'); + }); + + test('Can perform actions on the Timer', async ({ page }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/nasa/openmct/issues/4313' + }); + + await test.step("From the tree context menu", async () => { + await triggerTimerContextMenuAction(page, 'Start'); + await triggerTimerContextMenuAction(page, 'Pause'); + await triggerTimerContextMenuAction(page, 'Restart at 0'); + await triggerTimerContextMenuAction(page, 'Stop'); + }); + + await test.step("From the 3dot menu", async () => { + await triggerTimer3dotMenuAction(page, 'Start'); + await triggerTimer3dotMenuAction(page, 'Pause'); + await triggerTimer3dotMenuAction(page, 'Restart at 0'); + await triggerTimer3dotMenuAction(page, 'Stop'); + }); + + await test.step("From the object view", async () => { + await triggerTimerViewAction(page, 'Start'); + await triggerTimerViewAction(page, 'Pause'); + await triggerTimerViewAction(page, 'Restart at 0'); + }); + }); +}); + +/** + * Actions that can be performed on a timer from context menus. + * @typedef {'Start' | 'Stop' | 'Pause' | 'Restart at 0'} TimerAction + */ + +/** + * Actions that can be performed on a timer from the object view. + * @typedef {'Start' | 'Pause' | 'Restart at 0'} TimerViewAction + */ + +/** + * Open the timer context menu from the object tree. + * Expands the 'My Items' folder if it is not already expanded. + * @param {import('@playwright/test').Page} page + */ +async function openTimerContextMenu(page) { + const myItemsFolder = page.locator('text=Open MCT My Items >> span').nth(3); + const className = await myItemsFolder.getAttribute('class'); + if (!className.includes('c-disclosure-triangle--expanded')) { + await myItemsFolder.click(); + } + + await page.locator(`a:has-text("Unnamed Timer")`).click({ + button: 'right' + }); +} + +/** + * Trigger a timer action from the tree context menu + * @param {import('@playwright/test').Page} page + * @param {TimerAction} action + */ +async function triggerTimerContextMenuAction(page, action) { + const menuAction = `.c-menu ul li >> text="${action}"`; + await openTimerContextMenu(page); + await page.locator(menuAction).click(); + assertTimerStateAfterAction(page, action); +} + +/** + * Trigger a timer action from the 3dot menu + * @param {import('@playwright/test').Page} page + * @param {TimerAction} action + */ +async function triggerTimer3dotMenuAction(page, action) { + const menuAction = `.c-menu ul li >> text="${action}"`; + const threeDotMenuButton = 'button[title="More options"]'; + let isActionAvailable = false; + let iterations = 0; + // Dismiss/open the 3dot menu until the action is available + // or a maxiumum number of iterations is reached + while (!isActionAvailable && iterations <= 20) { + await page.click('.c-object-view'); + await page.click(threeDotMenuButton); + isActionAvailable = await page.locator(menuAction).isVisible(); + iterations++; + } + + await page.locator(menuAction).click(); + assertTimerStateAfterAction(page, action); +} + +/** + * Trigger a timer action from the object view + * @param {import('@playwright/test').Page} page + * @param {TimerViewAction} action + */ +async function triggerTimerViewAction(page, action) { + await page.locator('.c-timer').hover({trial: true}); + const buttonTitle = buttonTitleFromAction(action); + await page.click(`button[title="${buttonTitle}"]`); + assertTimerStateAfterAction(page, action); +} + +/** + * Takes in a TimerViewAction and returns the button title + * @param {TimerViewAction} action + */ +function buttonTitleFromAction(action) { + switch (action) { + case 'Start': + return 'Start'; + case 'Pause': + return 'Pause'; + case 'Restart at 0': + return 'Reset'; + } +} + +/** + * Verify the timer state after a timer action has been performed. + * @param {import('@playwright/test').Page} page + * @param {TimerAction} action + */ +async function assertTimerStateAfterAction(page, action) { + let timerStateClass; + switch (action) { + case 'Start': + case 'Restart at 0': + timerStateClass = "is-started"; + break; + case 'Stop': + timerStateClass = 'is-stopped'; + break; + case 'Pause': + timerStateClass = 'is-paused'; + break; + } + + await expect.soft(page.locator('.c-timer')).toHaveClass(new RegExp(timerStateClass)); +} diff --git a/e2e/tests/recycled_storage.json b/e2e/tests/recycled_storage.json deleted file mode 100644 index 8d59148b23..0000000000 --- a/e2e/tests/recycled_storage.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "cookies": [], - "origins": [ - { - "origin": "http://localhost:8080", - "localStorage": [ - { - "name": "mct-tree-expanded", - "value": "[]" - }, - { - "name": "tcHistory", - "value": "{\"utc\":[{\"start\":1656473493306,\"end\":1656475293306},{\"start\":1655769110258,\"end\":1655770910258},{\"start\":1652301954635,\"end\":1652303754635}]}" - }, - { - "name": "mct", - "value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},{\"key\":\"18ba28bf-152e-4e0f-9b9c-638fb2ade0c3\",\"namespace\":\"\"},{\"key\":\"fa64bd6c-9351-4d94-a54e-e062a93be3b6\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1656475294042,\"modified\":1656475294042},\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"73f2d9ae-d1f3-4561-b7fc-ecd5df557249\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1652303755999,\"location\":\"mine\",\"persisted\":1652303756002},\"18ba28bf-152e-4e0f-9b9c-638fb2ade0c3\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"18ba28bf-152e-4e0f-9b9c-638fb2ade0c3\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"43cfb4b1-348c-43c0-a681-c4cf53b5335f\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1655770911020,\"location\":\"mine\",\"persisted\":1655770911020},\"fa64bd6c-9351-4d94-a54e-e062a93be3b6\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"fa64bd6c-9351-4d94-a54e-e062a93be3b6\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"26739ce0-9a56-466c-91dd-f08bd9bfc9d7\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1656475294040,\"location\":\"mine\",\"persisted\":1656475294040}}" - } - ] - } - ] -} \ No newline at end of file diff --git a/e2e/tests/ui/layout/search/grandsearch.e2e.spec.js b/e2e/tests/ui/layout/search/grandsearch.e2e.spec.js new file mode 100644 index 0000000000..f67b336731 --- /dev/null +++ b/e2e/tests/ui/layout/search/grandsearch.e2e.spec.js @@ -0,0 +1,111 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2022, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +/* +This test suite is dedicated to tests which verify search functionality. +*/ + +const { expect } = require('@playwright/test'); +const { test } = require('../../../../fixtures'); + +/** + * Creates a notebook object and adds an entry. + * @param {import('@playwright/test').Page} page + */ +async function createClockAndDisplayLayout(page) { + //Go to baseURL + await page.goto('/', { waitUntil: 'networkidle' }); + + // Click button:has-text("Create") + await page.locator('button:has-text("Create")').click(); + // Click li:has-text("Notebook") + await page.locator('li:has-text("Clock")').click(); + // Click button:has-text("OK") + await Promise.all([ + page.waitForNavigation(), + page.locator('button:has-text("OK")').click() + ]); + + // Click a:has-text("My Items") + await Promise.all([ + page.waitForNavigation(), + page.locator('a:has-text("My Items") >> nth=0').click() + ]); + // Click button:has-text("Create") + await page.locator('button:has-text("Create")').click(); + // Click li:has-text("Notebook") + await page.locator('li:has-text("Display Layout")').click(); + // Click button:has-text("OK") + await Promise.all([ + page.waitForNavigation(), + page.locator('button:has-text("OK")').click() + ]); +} + +test.describe('Grand Search', () => { + test('Can search for objects, and subsequent search dropdown behaves properly', async ({ page }) => { + await createClockAndDisplayLayout(page); + + // Click [aria-label="OpenMCT Search"] input[type="search"] + await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); + // Fill [aria-label="OpenMCT Search"] input[type="search"] + await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Cl'); + await expect(page.locator('[aria-label="Search Result"]')).toContainText('Clock'); + // Click text=Elements >> nth=0 + await page.locator('text=Elements').first().click(); + await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible(); + + // Click [aria-label="OpenMCT Search"] [aria-label="Search Input"] + await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click(); + // Click [aria-label="Unnamed Clock clock result"] >> text=Unnamed Clock + await page.locator('[aria-label="Unnamed Clock clock result"] >> text=Unnamed Clock').click(); + await expect(page.locator('.js-preview-window')).toBeVisible(); + + // Click [aria-label="Close"] + await page.locator('[aria-label="Close"]').click(); + await expect(page.locator('[aria-label="Search Result"]')).toBeVisible(); + await expect(page.locator('[aria-label="Search Result"]')).toContainText('Cloc'); + + // Click [aria-label="OpenMCT Search"] a >> nth=0 + await page.locator('[aria-label="OpenMCT Search"] a').first().click(); + await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible(); + + // Fill [aria-label="OpenMCT Search"] input[type="search"] + await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('foo'); + await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible(); + + // Click text=Snapshot Save and Finish Editing Save and Continue Editing >> button >> nth=1 + await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click(); + // Click text=Save and Finish Editing + await page.locator('text=Save and Finish Editing').click(); + // Click [aria-label="OpenMCT Search"] [aria-label="Search Input"] + await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click(); + // Fill [aria-label="OpenMCT Search"] [aria-label="Search Input"] + await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').fill('Cl'); + // Click text=Unnamed Clock + await Promise.all([ + page.waitForNavigation(), + page.locator('text=Unnamed Clock').click() + ]); + await expect(page.locator('.is-object-type-clock')).toBeVisible(); + }); +}); diff --git a/e2e/tests/visual/addInit.visual.spec.js b/e2e/tests/visual/addInit.visual.spec.js new file mode 100644 index 0000000000..c7f142c242 --- /dev/null +++ b/e2e/tests/visual/addInit.visual.spec.js @@ -0,0 +1,76 @@ +/* eslint-disable no-undef */ +/***************************************************************************** + * 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. + *****************************************************************************/ + +/* +Collection of Visual Tests set to run with modified init scripts to inject plugins not otherwise available in the default contexts. + +These should only use functional expect statements to verify assumptions about the state +in a test and not for functional verification of correctness. Visual tests are not supposed +to "fail" on assertions. Instead, they should be used to detect changes between builds or branches. + +Note: Larger testsuite sizes are OK due to the setup time associated with these tests. +*/ + +const { test } = require('@playwright/test'); +const percySnapshot = require('@percy/playwright'); +const path = require('path'); +const sinon = require('sinon'); + +const VISUAL_GRACE_PERIOD = 5 * 1000; //Lets the application "simmer" before the snapshot is taken + +const CUSTOM_NAME = 'CUSTOM_NAME'; + +// Snippet from https://github.com/microsoft/playwright/issues/6347#issuecomment-965887758 +// Will replace with cy.clock() equivalent +test.beforeEach(async ({ context }) => { + await context.addInitScript({ + path: path.join(__dirname, '../../..', './node_modules/sinon/pkg/sinon.js') + }); + await context.addInitScript(() => { + window.__clock = sinon.useFakeTimers({ + now: 0, + shouldAdvanceTime: true + }); //Set browser clock to UNIX Epoch + }); +}); + +test('Visual - Restricted Notebook is visually correct @addInit', async ({ page }) => { + // eslint-disable-next-line no-undef + await page.addInitScript({ path: path.join(__dirname, '../plugins/notebook', './addInitRestrictedNotebook.js') }); + //Go to baseURL + await page.goto('/', { waitUntil: 'networkidle' }); + //Click the Create button + await page.click('button:has-text("Create")'); + // Click text=CUSTOM_NAME + await page.click(`text=${CUSTOM_NAME}`); + // Click text=OK + await Promise.all([ + page.waitForNavigation({waitUntil: 'networkidle'}), + page.click('text=OK') + ]); + + // Take a snapshot of the newly created CUSTOM_NAME notebook + await page.waitForTimeout(VISUAL_GRACE_PERIOD); + await percySnapshot(page, 'Restricted Notebook with CUSTOM_NAME'); + +}); diff --git a/e2e/tests/visual/controlledClock.visual.spec.js b/e2e/tests/visual/controlledClock.visual.spec.js new file mode 100644 index 0000000000..7eb7f64c84 --- /dev/null +++ b/e2e/tests/visual/controlledClock.visual.spec.js @@ -0,0 +1,70 @@ +/***************************************************************************** + * 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. + *****************************************************************************/ + +/* +Collection of Visual Tests set to run in a default context. The tests within this suite +are only meant to run against openmct's app.js started by `npm run start` within the +`./e2e/playwright-visual.config.js` file. + +These should only use functional expect statements to verify assumptions about the state +in a test and not for functional verification of correctness. Visual tests are not supposed +to "fail" on assertions. Instead, they should be used to detect changes between builds or branches. + +Note: Larger testsuite sizes are OK due to the setup time associated with these tests. +*/ + +const { test, expect } = require('@playwright/test'); +const percySnapshot = require('@percy/playwright'); +const path = require('path'); +const sinon = require('sinon'); + +// Snippet from https://github.com/microsoft/playwright/issues/6347#issuecomment-965887758 +// Will replace with cy.clock() equivalent +test.beforeEach(async ({ context }) => { + await context.addInitScript({ + // eslint-disable-next-line no-undef + path: path.join(__dirname, '../../..', './node_modules/sinon/pkg/sinon.js') + }); + await context.addInitScript(() => { + window.__clock = sinon.useFakeTimers({ + now: 0, //Set browser clock to UNIX Epoch + shouldAdvanceTime: false, //Don't advance the clock + toFake: ["setTimeout", "nextTick"] + }); + }); +}); +test.use({ storageState: './e2e/test-data/VisualTestData_storage.json' }); + +test('Visual - Overlay Plot Loading Indicator @localstorage', async ({ page }) => { + // Go to baseURL + await page.goto('/', { waitUntil: 'networkidle' }); + + await page.locator('a:has-text("Unnamed Overlay Plot Overlay Plot")').click(); + //Ensure that we're on the Unnamed Overlay Plot object + await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Overlay Plot'); + + //Wait for canvas to be rendered and stop animating + await page.locator('canvas >> nth=1').hover({trial: true}); + + //Take snapshot of Sine Wave Generator within Overlay Plot + await percySnapshot(page, 'SineWaveInOverlayPlot'); +}); diff --git a/e2e/tests/visual/default.visual.spec.js b/e2e/tests/visual/default.visual.spec.js index e64de2925e..d5c2210ee3 100644 --- a/e2e/tests/visual/default.visual.spec.js +++ b/e2e/tests/visual/default.visual.spec.js @@ -211,3 +211,22 @@ test('Visual - Display Layout Icon is correct', async ({ page }) => { await percySnapshot(page, 'Display Layout Create Menu'); }); + +test('Visual - Default Gauge is correct', async ({ page }) => { + + //Go to baseURL + await page.goto('/', { waitUntil: 'networkidle' }); + + //Click the Create button + await page.click('button:has-text("Create")'); + + await page.click('text=Gauge'); + + await page.click('text=OK'); + + // Take a snapshot of the newly created Gauge object + await page.waitForTimeout(VISUAL_GRACE_PERIOD); + await percySnapshot(page, 'Default Gauge'); + +}); + diff --git a/e2e/tests/visual/generateVisualTestData.e2e.spec.js b/e2e/tests/visual/generateVisualTestData.e2e.spec.js new file mode 100644 index 0000000000..fc67dab723 --- /dev/null +++ b/e2e/tests/visual/generateVisualTestData.e2e.spec.js @@ -0,0 +1,86 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2022, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +/* +This test suite is dedicated to generating LocalStorage via Session Storage to be used +in some visual test suites like controlledClock.visual.spec.js. This suite should run to completion +and generate an artifact named ./e2e/test-data/VisualTestData_storage.json . This will run +on every Commit to ensure that this object still loads into tests correctly and will retain the +.e2e.spec.js suffix. + +TODO: Provide additional validation of object properties as it grows. + +*/ + +const { test } = require('../../fixtures.js'); +const { expect } = require('@playwright/test'); + +test('Generate Visual Test Data @localStorage', async ({ page, context }) => { + //Go to baseURL + await page.goto('/', { waitUntil: 'networkidle' }); + + await page.locator('button:has-text("Create")').click(); + + // add overlay plot with defaults + await page.locator('li:has-text("Overlay Plot")').click(); + + // Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184 + await page.click('form[name="mctForm"] a:has-text("My Items")'); + + await Promise.all([ + page.waitForNavigation(), + page.locator('text=OK').click(), + //Wait for Save Banner to appear1 + page.waitForSelector('.c-message-banner__message') + ]); + + // save (exit edit mode) + await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click(); + await page.locator('text=Save and Finish Editing').click(); + + // click create button + await page.locator('button:has-text("Create")').click(); + + // add sine wave generator with defaults + await page.locator('li:has-text("Sine Wave Generator")').click(); + + //Add a 5000 ms Delay + await page.locator('[aria-label="Loading Delay \\(ms\\)"]').fill('5000'); + + await Promise.all([ + page.waitForNavigation(), + page.locator('text=OK').click(), + //Wait for Save Banner to appear1 + page.waitForSelector('.c-message-banner__message') + ]); + + // focus the overlay plot + await page.locator('text=Open MCT My Items >> span').nth(3).click(); + await Promise.all([ + page.waitForNavigation(), + page.locator('text=Unnamed Overlay Plot').first().click() + ]); + + await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Overlay Plot'); + //Save localStorage for future test execution + await context.storageState({ path: './e2e/test-data/VisualTestData_storage.json' }); +}); diff --git a/e2e/tests/visual/search.visual.spec.js b/e2e/tests/visual/search.visual.spec.js new file mode 100644 index 0000000000..654a336dd8 --- /dev/null +++ b/e2e/tests/visual/search.visual.spec.js @@ -0,0 +1,104 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2022, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +/* +This test suite is dedicated to tests which verify search functionality. +*/ + +const { test, expect } = require('@playwright/test'); +const percySnapshot = require('@percy/playwright'); + +/** + * Creates a notebook object and adds an entry. + * @param {import('@playwright/test').Page} page + */ +async function createClockAndDisplayLayout(page) { + //Go to baseURL + await page.goto('/', { waitUntil: 'networkidle' }); + + // Click button:has-text("Create") + await page.locator('button:has-text("Create")').click(); + // Click li:has-text("Notebook") + await page.locator('li:has-text("Clock")').click(); + // Click button:has-text("OK") + await Promise.all([ + page.waitForNavigation(), + page.locator('button:has-text("OK")').click() + ]); + + // Click a:has-text("My Items") + await Promise.all([ + page.waitForNavigation(), + page.locator('a:has-text("My Items") >> nth=0').click() + ]); + // Click button:has-text("Create") + await page.locator('button:has-text("Create")').click(); + // Click li:has-text("Notebook") + await page.locator('li:has-text("Display Layout")').click(); + // Click button:has-text("OK") + await Promise.all([ + page.waitForNavigation(), + page.locator('button:has-text("OK")').click() + ]); +} + +test.describe('Grand Search', () => { + test('Can search for objects, and subsequent search dropdown behaves properly', async ({ page }) => { + await createClockAndDisplayLayout(page); + + // Click [aria-label="OpenMCT Search"] input[type="search"] + await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); + // Fill [aria-label="OpenMCT Search"] input[type="search"] + await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Cl'); + await expect(page.locator('[aria-label="Search Result"]')).toContainText('Clock'); + await percySnapshot(page, 'Searching for Clocks'); + // Click text=Elements >> nth=0 + await page.locator('text=Elements').first().click(); + await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible(); + + // Click [aria-label="OpenMCT Search"] [aria-label="Search Input"] + await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click(); + // Click [aria-label="Unnamed Clock clock result"] >> text=Unnamed Clock + await page.locator('[aria-label="Unnamed Clock clock result"] >> text=Unnamed Clock').click(); + await percySnapshot(page, 'Preview for clock should display when editing enabled and search item clicked'); + + // Click [aria-label="Close"] + await page.locator('[aria-label="Close"]').click(); + await percySnapshot(page, 'Search should still be showing after preview closed'); + + // Click text=Snapshot Save and Finish Editing Save and Continue Editing >> button >> nth=1 + await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click(); + // Click text=Save and Finish Editing + await page.locator('text=Save and Finish Editing').click(); + // Click [aria-label="OpenMCT Search"] [aria-label="Search Input"] + await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click(); + // Fill [aria-label="OpenMCT Search"] [aria-label="Search Input"] + await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').fill('Cl'); + // Click text=Unnamed Clock + await Promise.all([ + page.waitForNavigation(), + page.locator('text=Unnamed Clock').click() + ]); + await percySnapshot(page, 'Clicking on search results should navigate to them if not editing'); + + }); +}); diff --git a/example/exampleUser/ExampleUserProvider.js b/example/exampleUser/ExampleUserProvider.js index 2926f0ba92..8fdd029234 100644 --- a/example/exampleUser/ExampleUserProvider.js +++ b/example/exampleUser/ExampleUserProvider.js @@ -31,7 +31,7 @@ const STATUSES = [{ iconClassPoll: "icon-status-poll-question-mark" }, { key: "GO", - label: "GO", + label: "Go", iconClass: "icon-check", iconClassPoll: "icon-status-poll-question-mark", statusClass: "s-status-ok", @@ -39,7 +39,7 @@ const STATUSES = [{ statusFgColor: "#000" }, { key: "MAYBE", - label: "MAYBE", + label: "Maybe", iconClass: "icon-alert-triangle", iconClassPoll: "icon-status-poll-question-mark", statusClass: "s-status-warning", @@ -47,7 +47,7 @@ const STATUSES = [{ statusFgColor: "#000" }, { key: "NO_GO", - label: "NO GO", + label: "No go", iconClass: "icon-circle-slash", iconClassPoll: "icon-status-poll-question-mark", statusClass: "s-status-error", diff --git a/example/generator/GeneratorProvider.js b/example/generator/GeneratorProvider.js index 2b845a1aae..ee0bf98f91 100644 --- a/example/generator/GeneratorProvider.js +++ b/example/generator/GeneratorProvider.js @@ -32,7 +32,8 @@ define([ offset: 0, dataRateInHz: 1, randomness: 0, - phase: 0 + phase: 0, + loadDelay: 0 }; function GeneratorProvider(openmct) { @@ -53,8 +54,9 @@ define([ 'period', 'offset', 'dataRateInHz', + 'randomness', 'phase', - 'randomness' + 'loadDelay' ]; request = request || {}; diff --git a/example/generator/generatorWorker.js b/example/generator/generatorWorker.js index 02807e06f2..bc9083da3a 100644 --- a/example/generator/generatorWorker.js +++ b/example/generator/generatorWorker.js @@ -116,6 +116,7 @@ var dataRateInHz = request.dataRateInHz; var phase = request.phase; var randomness = request.randomness; + var loadDelay = Math.max(request.loadDelay, 0); var step = 1000 / dataRateInHz; var nextStep = start - (start % step) + step; @@ -133,6 +134,14 @@ }); } + if (loadDelay === 0) { + postOnRequest(message, request, data); + } else { + setTimeout(() => postOnRequest(message, request, data), loadDelay); + } + } + + function postOnRequest(message, request, data) { self.postMessage({ id: message.id, data: request.spectra ? { diff --git a/example/generator/plugin.js b/example/generator/plugin.js index 953383aad9..8f820e16fd 100644 --- a/example/generator/plugin.js +++ b/example/generator/plugin.js @@ -81,7 +81,7 @@ define([ { name: "Amplitude", control: "numberfield", - cssClass: "l-input-sm l-numeric", + cssClass: "l-numeric", key: "amplitude", required: true, property: [ @@ -92,7 +92,7 @@ define([ { name: "Offset", control: "numberfield", - cssClass: "l-input-sm l-numeric", + cssClass: "l-numeric", key: "offset", required: true, property: [ @@ -132,6 +132,17 @@ define([ "telemetry", "randomness" ] + }, + { + name: "Loading Delay (ms)", + control: "numberfield", + cssClass: "l-input-sm l-numeric", + key: "loadDelay", + required: true, + property: [ + "telemetry", + "loadDelay" + ] } ], initialize: function (object) { @@ -141,7 +152,8 @@ define([ offset: 0, dataRateInHz: 1, phase: 0, - randomness: 0 + randomness: 0, + loadDelay: 0 }; } }); diff --git a/example/imagery/plugin.js b/example/imagery/plugin.js index dfb77e4918..2f323356dd 100644 --- a/example/imagery/plugin.js +++ b/example/imagery/plugin.js @@ -190,7 +190,9 @@ function getRealtimeProvider() { subscribe: (domainObject, callback) => { const delay = getImageLoadDelay(domainObject); const interval = setInterval(() => { - callback(pointForTimestamp(Date.now(), domainObject.name, getImageSamples(domainObject.configuration), delay)); + const imageSamples = getImageSamples(domainObject.configuration); + const datum = pointForTimestamp(Date.now(), domainObject.name, imageSamples, delay); + callback(datum); }, delay); return () => { @@ -229,8 +231,9 @@ function getLadProvider() { }, request: (domainObject, options) => { const delay = getImageLoadDelay(domainObject); + const datum = pointForTimestamp(Date.now(), domainObject.name, getImageSamples(domainObject.configuration), delay); - return Promise.resolve([pointForTimestamp(Date.now(), domainObject.name, delay)]); + return Promise.resolve([datum]); } }; } diff --git a/package.json b/package.json index 5b6c600322..3b14f84a1f 100644 --- a/package.json +++ b/package.json @@ -88,17 +88,17 @@ "build:coverage": "webpack --config webpack.coverage.js", "build:watch": "webpack --config webpack.dev.js --watch", "info": "npx envinfo --system --browsers --npmPackages --binaries --languages --markdown", - "test": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run", - "test:firefox": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run --browsers=FirefoxHeadless", + "test": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run", + "test:firefox": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run --browsers=FirefoxHeadless", "test:debug": "cross-env NODE_ENV=debug karma start --no-single-run", "test:e2e": "npx playwright test", - "test:e2e:ci": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome smoke branding default condition timeConductor clock exampleImagery persistence performance", + "test:e2e:ci": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome smoke branding default condition timeConductor clock exampleImagery persistence performance grandsearch notebook/tags", "test:e2e:local": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome", "test:e2e:updatesnapshots": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @snapshot --update-snapshots", "test:e2e:visual": "percy exec --config ./e2e/.percy.yml -- npx playwright test --config=e2e/playwright-visual.config.js", "test:e2e: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_OPTIONS=\"--max_old_space_size=4096\" karma start --no-single-run", + "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'", diff --git a/src/MCT.js b/src/MCT.js index 7fa54e7ad1..595afa7b05 100644 --- a/src/MCT.js +++ b/src/MCT.js @@ -96,161 +96,167 @@ define([ }; this.destroy = this.destroy.bind(this); - /** - * Tracks current selection state of the application. - * @private - */ - this.selection = new Selection(this); + [ + /** + * Tracks current selection state of the application. + * @private + */ + ['selection', () => new Selection(this)], - /** - * MCT's time conductor, which may be used to synchronize view contents - * for telemetry- or time-based views. - * @type {module:openmct.TimeConductor} - * @memberof module:openmct.MCT# - * @name conductor - */ - this.time = new api.TimeAPI(this); + /** + * MCT's time conductor, which may be used to synchronize view contents + * for telemetry- or time-based views. + * @type {module:openmct.TimeConductor} + * @memberof module:openmct.MCT# + * @name conductor + */ + ['time', () => new api.TimeAPI(this)], - /** - * An interface for interacting with the composition of domain objects. - * The composition of a domain object is the list of other domain - * objects it "contains" (for instance, that should be displayed - * beneath it in the tree.) - * - * `composition` may be called as a function, in which case it acts - * as [`composition.get`]{@link module:openmct.CompositionAPI#get}. - * - * @type {module:openmct.CompositionAPI} - * @memberof module:openmct.MCT# - * @name composition - */ - this.composition = new api.CompositionAPI(this); + /** + * An interface for interacting with the composition of domain objects. + * The composition of a domain object is the list of other domain + * objects it "contains" (for instance, that should be displayed + * beneath it in the tree.) + * + * `composition` may be called as a function, in which case it acts + * as [`composition.get`]{@link module:openmct.CompositionAPI#get}. + * + * @type {module:openmct.CompositionAPI} + * @memberof module:openmct.MCT# + * @name composition + */ + ['composition', () => new api.CompositionAPI(this)], - /** - * Registry for views of domain objects which should appear in the - * main viewing area. - * - * @type {module:openmct.ViewRegistry} - * @memberof module:openmct.MCT# - * @name objectViews - */ - this.objectViews = new ViewRegistry(); + /** + * Registry for views of domain objects which should appear in the + * main viewing area. + * + * @type {module:openmct.ViewRegistry} + * @memberof module:openmct.MCT# + * @name objectViews + */ + ['objectViews', () => new ViewRegistry()], - /** - * Registry for views which should appear in the Inspector area. - * These views will be chosen based on the selection state. - * - * @type {module:openmct.InspectorViewRegistry} - * @memberof module:openmct.MCT# - * @name inspectorViews - */ - this.inspectorViews = new InspectorViewRegistry(); + /** + * Registry for views which should appear in the Inspector area. + * These views will be chosen based on the selection state. + * + * @type {module:openmct.InspectorViewRegistry} + * @memberof module:openmct.MCT# + * @name inspectorViews + */ + ['inspectorViews', () => new InspectorViewRegistry()], - /** - * Registry for views which should appear in Edit Properties - * dialogs, and similar user interface elements used for - * modifying domain objects external to its regular views. - * - * @type {module:openmct.ViewRegistry} - * @memberof module:openmct.MCT# - * @name propertyEditors - */ - this.propertyEditors = new ViewRegistry(); + /** + * Registry for views which should appear in Edit Properties + * dialogs, and similar user interface elements used for + * modifying domain objects external to its regular views. + * + * @type {module:openmct.ViewRegistry} + * @memberof module:openmct.MCT# + * @name propertyEditors + */ + ['propertyEditors', () => new ViewRegistry()], - /** - * Registry for views which should appear in the status indicator area. - * @type {module:openmct.ViewRegistry} - * @memberof module:openmct.MCT# - * @name indicators - */ - this.indicators = new ViewRegistry(); + /** + * Registry for views which should appear in the toolbar area while + * editing. These views will be chosen based on the selection state. + * + * @type {module:openmct.ToolbarRegistry} + * @memberof module:openmct.MCT# + * @name toolbars + */ + ['toolbars', () => new ToolbarRegistry()], - /** - * Registry for views which should appear in the toolbar area while - * editing. These views will be chosen based on the selection state. - * - * @type {module:openmct.ToolbarRegistry} - * @memberof module:openmct.MCT# - * @name toolbars - */ - this.toolbars = new ToolbarRegistry(); + /** + * Registry for domain object types which may exist within this + * instance of Open MCT. + * + * @type {module:openmct.TypeRegistry} + * @memberof module:openmct.MCT# + * @name types + */ + ['types', () => new api.TypeRegistry()], - /** - * Registry for domain object types which may exist within this - * instance of Open MCT. - * - * @type {module:openmct.TypeRegistry} - * @memberof module:openmct.MCT# - * @name types - */ - this.types = new api.TypeRegistry(); + /** + * An interface for interacting with domain objects and the domain + * object hierarchy. + * + * @type {module:openmct.ObjectAPI} + * @memberof module:openmct.MCT# + * @name objects + */ + ['objects', () => new api.ObjectAPI.default(this.types, this)], - /** - * An interface for interacting with domain objects and the domain - * object hierarchy. - * - * @type {module:openmct.ObjectAPI} - * @memberof module:openmct.MCT# - * @name objects - */ - this.objects = new api.ObjectAPI.default(this.types, this); + /** + * An interface for retrieving and interpreting telemetry data associated + * with a domain object. + * + * @type {module:openmct.TelemetryAPI} + * @memberof module:openmct.MCT# + * @name telemetry + */ + ['telemetry', () => new api.TelemetryAPI.default(this)], - /** - * An interface for retrieving and interpreting telemetry data associated - * with a domain object. - * - * @type {module:openmct.TelemetryAPI} - * @memberof module:openmct.MCT# - * @name telemetry - */ - this.telemetry = new api.TelemetryAPI(this); + /** + * An interface for creating new indicators and changing them dynamically. + * + * @type {module:openmct.IndicatorAPI} + * @memberof module:openmct.MCT# + * @name indicators + */ + ['indicators', () => new api.IndicatorAPI(this)], - /** - * An interface for creating new indicators and changing them dynamically. - * - * @type {module:openmct.IndicatorAPI} - * @memberof module:openmct.MCT# - * @name indicators - */ - this.indicators = new api.IndicatorAPI(this); + /** + * MCT's user awareness management, to enable user and + * role specific functionality. + * @type {module:openmct.UserAPI} + * @memberof module:openmct.MCT# + * @name user + */ + ['user', () => new api.UserAPI(this)], - /** - * MCT's user awareness management, to enable user and - * role specific functionality. - * @type {module:openmct.UserAPI} - * @memberof module:openmct.MCT# - * @name user - */ - this.user = new api.UserAPI(this); + ['notifications', () => new api.NotificationAPI()], - this.notifications = new api.NotificationAPI(); + ['editor', () => new api.EditorAPI.default(this)], - this.editor = new api.EditorAPI.default(this); + ['overlays', () => new OverlayAPI.default()], - this.overlays = new OverlayAPI.default(); + ['menus', () => new api.MenuAPI(this)], - this.menus = new api.MenuAPI(this); + ['actions', () => new api.ActionsAPI(this)], - this.actions = new api.ActionsAPI(this); + ['status', () => new api.StatusAPI(this)], - this.status = new api.StatusAPI(this); + ['priority', () => api.PriorityAPI], - this.priority = api.PriorityAPI; + ['router', () => new ApplicationRouter(this)], - this.router = new ApplicationRouter(this); - this.faults = new api.FaultManagementAPI.default(this); - this.forms = new api.FormsAPI.default(this); + ['faults', () => new api.FaultManagementAPI.default(this)], - this.branding = BrandingAPI.default; + ['forms', () => new api.FormsAPI.default(this)], - /** - * MCT's annotation API that enables - * human-created comments and categorization linked to data products - * @type {module:openmct.AnnotationAPI} - * @memberof module:openmct.MCT# - * @name annotation - */ - this.annotation = new api.AnnotationAPI(this); + ['branding', () => BrandingAPI.default], + + /** + * MCT's annotation API that enables + * human-created comments and categorization linked to data products + * @type {module:openmct.AnnotationAPI} + * @memberof module:openmct.MCT# + * @name annotation + */ + ['annotation', () => new api.AnnotationAPI(this)] + ].forEach(apiEntry => { + const apiName = apiEntry[0]; + const apiObject = apiEntry[1](); + + Object.defineProperty(this, apiName, { + value: apiObject, + enumerable: false, + configurable: false, + writable: true + }); + }); // Plugins that are installed by default this.install(this.plugins.Plot()); @@ -281,6 +287,7 @@ define([ this.install(this.plugins.ObjectInterceptors()); this.install(this.plugins.DeviceClassifier()); this.install(this.plugins.UserIndicator()); + this.install(this.plugins.Gauge()); } MCT.prototype = Object.create(EventEmitter.prototype); diff --git a/src/api/actions/ActionCollection.js b/src/api/actions/ActionCollection.js index eed8be625b..6606616b01 100644 --- a/src/api/actions/ActionCollection.js +++ b/src/api/actions/ActionCollection.js @@ -85,8 +85,6 @@ class ActionCollection extends EventEmitter { } destroy() { - super.removeAllListeners(); - if (!this.skipEnvironmentObservers) { this.objectUnsubscribes.forEach(unsubscribe => { unsubscribe(); @@ -96,6 +94,7 @@ class ActionCollection extends EventEmitter { } this.emit('destroy', this.view); + this.removeAllListeners(); } getVisibleActions() { diff --git a/src/api/annotation/AnnotationAPI.js b/src/api/annotation/AnnotationAPI.js index a856114957..a197620389 100644 --- a/src/api/annotation/AnnotationAPI.js +++ b/src/api/annotation/AnnotationAPI.js @@ -172,17 +172,19 @@ export default class AnnotationAPI extends EventEmitter { name: contentText, domainObject: targetDomainObject, annotationType, - tags: [], + tags: [tag], contentText, targets }; - existingAnnotation = await this.create(annotationCreationArguments); + const newAnnotation = await this.create(annotationCreationArguments); + + return newAnnotation; + } else { + const tagArray = [tag, ...existingAnnotation.tags]; + this.openmct.objects.mutate(existingAnnotation, 'tags', tagArray); + + return existingAnnotation; } - - const tagArray = [tag, ...existingAnnotation.tags]; - this.openmct.objects.mutate(existingAnnotation, 'tags', tagArray); - - return existingAnnotation; } removeAnnotationTag(existingAnnotation, tagToRemove) { diff --git a/src/api/forms/components/FormProperties.vue b/src/api/forms/components/FormProperties.vue index f6c5a62c8b..3c2af7b96d 100644 --- a/src/api/forms/components/FormProperties.vue +++ b/src/api/forms/components/FormProperties.vue @@ -60,6 +60,7 @@ tabindex="0" :disabled="isInvalid" class="c-button c-button--major" + aria-label="Save" @click="onSave" > {{ submitLabel }} @@ -67,6 +68,7 @@