From 114864429a616616e22dafe4d24760bfc1ef1da1 Mon Sep 17 00:00:00 2001 From: Jesse Mazzella Date: Tue, 23 Jan 2024 15:15:22 -0800 Subject: [PATCH] feat(#7394): Incorporate Status Indicators into the main Vue app (#7395) * feat(IndicatorAPI): accept Vue components - Adds a new property to Indicators, `component`, which is a synchronous or asynchronous Vue component. - Adds `wrapHtmlElement` utility function to create anonymous Vue components out of `HTMLElement`s (for backwards compatibility) - Refactors StatusIndicators.vue to use dynamic components, allowing us to dynamically render indicators (and keep it all within Vue's ecosystem). * refactor(indicators): use dynamic Vue components instead of `mount()` - Refactors some indicators to use Vue components directly as async components * refactor: use Vue reactivity for timestamps in clock indicator * fix(test): fix unit tests and remove some console logs * test(e2e): stabilize ladSet e2e test * test: mix in some Vue indicators in indicatorSpec * refactor: cleanup variable names * docs: update IndicatorAPI docs * fix(e2e): wait for async status bar components to load before snapshot * a11y(e2e): add aria-labels and wait for status bar to load * test(e2e): add exact: true * fix: initializing indicators * fix(typo): uhhh.. how did that get there? O_o * fix: use synchronous components for default indicators * test: clean up, remove unnecessary `nextTick()`s * test: remove more `nextTick()`s * refactor: lint:fix * fix: `on` -> `off` * test(e2e): stabilize tabs test * test(e2e): attempt to stabilize limit lines tests with `toHaveCount()` assertion --- .../plugins/plot/overlayPlot.e2e.spec.js | 4 +- .../functional/plugins/tabs/tabs.e2e.spec.js | 6 +-- e2e/tests/functional/search.e2e.spec.js | 2 +- .../components/header.visual.spec.js | 16 ++++++++ .../faultManagement.visual.spec.js | 19 ++++++++- src/api/indicators/IndicatorAPI.js | 24 +++++++++++ src/api/indicators/IndicatorAPISpec.js | 40 ++++++++++++++----- .../components/GlobalClearIndicator.vue | 5 ++- src/plugins/clearData/plugin.js | 23 +---------- src/plugins/clearData/pluginSpec.js | 11 ++--- .../clock/components/ClockIndicator.vue | 18 ++++++--- src/plugins/clock/plugin.js | 28 +------------ src/plugins/clock/pluginSpec.js | 16 ++++---- src/plugins/flexibleLayout/pluginSpec.js | 1 - .../components/NotebookSnapshotIndicator.vue | 6 ++- src/plugins/notebook/plugin.js | 26 ++---------- src/plugins/notebook/pluginSpec.js | 34 ++++++++-------- .../components/NotificationIndicator.vue | 4 +- src/plugins/notificationIndicator/plugin.js | 22 +--------- src/ui/layout/AppLayout.vue | 2 +- src/ui/layout/status-bar/StatusIndicators.vue | 29 +++++++++++--- src/utils/vueWrapHtmlElement.js | 24 +++++++++++ 22 files changed, 202 insertions(+), 158 deletions(-) create mode 100644 src/utils/vueWrapHtmlElement.js diff --git a/e2e/tests/functional/plugins/plot/overlayPlot.e2e.spec.js b/e2e/tests/functional/plugins/plot/overlayPlot.e2e.spec.js index 84b86d4c5c..a80f15be0a 100644 --- a/e2e/tests/functional/plugins/plot/overlayPlot.e2e.spec.js +++ b/e2e/tests/functional/plugins/plot/overlayPlot.e2e.spec.js @@ -260,9 +260,9 @@ async function assertLimitLinesExistAndAreVisible(page) { await waitForPlotsToRender(page); // Wait for limit lines to be created await page.waitForSelector('.js-limit-area', { state: 'attached' }); - const limitLineCount = await page.locator('.c-plot-limit-line').count(); // There should be 10 limit lines created by default - expect(await page.locator('.c-plot-limit-line').count()).toBe(10); + await expect(page.locator('.c-plot-limit-line')).toHaveCount(10); + const limitLineCount = await page.locator('.c-plot-limit-line').count(); for (let i = 0; i < limitLineCount; i++) { await expect(page.locator('.c-plot-limit-line').nth(i)).toBeVisible(); } diff --git a/e2e/tests/functional/plugins/tabs/tabs.e2e.spec.js b/e2e/tests/functional/plugins/tabs/tabs.e2e.spec.js index 46e33330fe..7d5df80fb8 100644 --- a/e2e/tests/functional/plugins/tabs/tabs.e2e.spec.js +++ b/e2e/tests/functional/plugins/tabs/tabs.e2e.spec.js @@ -55,7 +55,7 @@ test.describe('Tabs View', () => { await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible(); // no canvas (i.e., sine wave generator) in the document should be visible - await expect(page.locator('canvas')).toBeHidden(); + await expect(page.locator('canvas[id=webglContext]')).toBeHidden(); // select second tab await page.getByLabel(`${notebook.name} tab`).click(); @@ -64,7 +64,7 @@ test.describe('Tabs View', () => { await expect(page.locator('.c-notebook__drag-area')).toBeVisible(); // no canvas (i.e., sine wave generator) in the document should be visible - await expect(page.locator('canvas')).toBeHidden(); + await expect(page.locator('canvas[id=webglContext]')).toBeHidden(); // select third tab await page.getByLabel(`${sineWaveGenerator.name} tab`).click(); @@ -83,6 +83,6 @@ test.describe('Tabs View', () => { await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible(); // no canvas (i.e., sine wave generator) in the document should be visible - await expect(page.locator('canvas')).toBeHidden(); + await expect(page.locator('canvas[id=webglContext]')).toBeHidden(); }); }); diff --git a/e2e/tests/functional/search.e2e.spec.js b/e2e/tests/functional/search.e2e.spec.js index 5b8b8833cf..ccc0163b18 100644 --- a/e2e/tests/functional/search.e2e.spec.js +++ b/e2e/tests/functional/search.e2e.spec.js @@ -99,7 +99,7 @@ test.describe('Grand Search', () => { page.waitForNavigation(), page.getByLabel('OpenMCT Search').getByText('Clock A').click() ]); - await expect(page.getByRole('status', { name: 'Clock' })).toBeVisible(); + await expect(page.getByRole('status', { name: 'Clock', exact: true })).toBeVisible(); await grandSearchInput.fill('Disp'); await expect(page.getByLabel('Object Search Result').first()).toContainText( diff --git a/e2e/tests/visual-a11y/components/header.visual.spec.js b/e2e/tests/visual-a11y/components/header.visual.spec.js index 70e7f53480..438a0269df 100644 --- a/e2e/tests/visual-a11y/components/header.visual.spec.js +++ b/e2e/tests/visual-a11y/components/header.visual.spec.js @@ -36,6 +36,22 @@ test.describe('Visual - Header @a11y', () => { test.beforeEach(async ({ page }) => { //Go to baseURL and Hide Tree await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' }); + // Wait for status bar to load + await expect( + page.getByRole('status', { + name: 'Clock Indicator' + }) + ).toBeInViewport(); + await expect( + page.getByRole('status', { + name: 'Global Clear Indicator' + }) + ).toBeInViewport(); + await expect( + page.getByRole('status', { + name: 'Snapshot Indicator' + }) + ).toBeInViewport(); }); test('header sizing', async ({ page, theme }) => { diff --git a/e2e/tests/visual-a11y/faultManagement.visual.spec.js b/e2e/tests/visual-a11y/faultManagement.visual.spec.js index a9f9d80ca5..f038e6d37d 100644 --- a/e2e/tests/visual-a11y/faultManagement.visual.spec.js +++ b/e2e/tests/visual-a11y/faultManagement.visual.spec.js @@ -23,7 +23,7 @@ import percySnapshot from '@percy/playwright'; import { fileURLToPath } from 'url'; import * as utils from '../../helper/faultUtils.js'; -import { test } from '../../pluginFixtures.js'; +import { expect, test } from '../../pluginFixtures.js'; test.describe('Fault Management Visual Tests', () => { test('icon test', async ({ page, theme }) => { @@ -32,6 +32,23 @@ test.describe('Fault Management Visual Tests', () => { }); await page.goto('./', { waitUntil: 'domcontentloaded' }); + // Wait for status bar to load + await expect( + page.getByRole('status', { + name: 'Clock Indicator' + }) + ).toBeInViewport(); + await expect( + page.getByRole('status', { + name: 'Global Clear Indicator' + }) + ).toBeInViewport(); + await expect( + page.getByRole('status', { + name: 'Snapshot Indicator' + }) + ).toBeInViewport(); + await percySnapshot(page, `Fault Management icon appears in tree (theme: '${theme}')`); }); diff --git a/src/api/indicators/IndicatorAPI.js b/src/api/indicators/IndicatorAPI.js index 1b81e4328c..e5a3e2f30e 100644 --- a/src/api/indicators/IndicatorAPI.js +++ b/src/api/indicators/IndicatorAPI.js @@ -22,9 +22,12 @@ import EventEmitter from 'EventEmitter'; +import vueWrapHtmlElement from '../../utils/vueWrapHtmlElement.js'; import SimpleIndicator from './SimpleIndicator.js'; class IndicatorAPI extends EventEmitter { + /** @type {import('../../../openmct.js').OpenMCT} */ + openmct; constructor(openmct) { super(); @@ -42,6 +45,18 @@ class IndicatorAPI extends EventEmitter { return new SimpleIndicator(this.openmct); } + /** + * @typedef {import('vue').Component} VueComponent + */ + + /** + * @typedef {Object} Indicator + * @property {HTMLElement} [element] + * @property {VueComponent|Promise} [vueComponent] + * @property {string} key + * @property {number} priority + */ + /** * Accepts an indicator object, which is a simple object * with a two attributes: 'element' which has an HTMLElement @@ -62,11 +77,20 @@ class IndicatorAPI extends EventEmitter { * myIndicator.text("Hello World!"); * myIndicator.iconClass("icon-info"); * + * If you would like to use a Vue component, you can pass it in + * directly as the 'vueComponent' attribute of the indicator object. + * This accepts a Vue component or a promise that resolves to a Vue component (for asynchronous + * rendering). + * + * @param {Indicator} indicator */ add(indicator) { if (!indicator.priority) { indicator.priority = this.openmct.priority.DEFAULT; } + if (!indicator.vueComponent) { + indicator.vueComponent = vueWrapHtmlElement(indicator.element); + } this.indicatorObjects.push(indicator); diff --git a/src/api/indicators/IndicatorAPISpec.js b/src/api/indicators/IndicatorAPISpec.js index 03d42f9c95..a41e08e680 100644 --- a/src/api/indicators/IndicatorAPISpec.js +++ b/src/api/indicators/IndicatorAPISpec.js @@ -19,6 +19,8 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ +import { defineComponent } from 'vue'; + import { createOpenMct, resetApplicationState } from '../../utils/testing.js'; import SimpleIndicator from './SimpleIndicator.js'; @@ -33,7 +35,7 @@ describe('The Indicator API', () => { return resetApplicationState(openmct); }); - function generateIndicator(className, label, priority) { + function generateHTMLIndicator(className, label, priority) { const element = document.createElement('div'); element.classList.add(className); const textNode = document.createTextNode(label); @@ -46,8 +48,25 @@ describe('The Indicator API', () => { return testIndicator; } - it('can register an indicator', () => { - const testIndicator = generateIndicator('test-indicator', 'This is a test indicator', 2); + function generateVueIndicator(priority) { + return { + vueComponent: defineComponent({ + template: '
This is a test indicator
' + }), + priority + }; + } + + it('can register an HTML indicator', () => { + const testIndicator = generateHTMLIndicator('test-indicator', 'This is a test indicator', 2); + openmct.indicators.add(testIndicator); + expect(openmct.indicators.indicatorObjects).toBeDefined(); + // notifier indicator is installed by default + expect(openmct.indicators.indicatorObjects.length).toBe(2); + }); + + it('can register a Vue indicator', () => { + const testIndicator = generateVueIndicator(2); openmct.indicators.add(testIndicator); expect(openmct.indicators.indicatorObjects).toBeDefined(); // notifier indicator is installed by default @@ -55,37 +74,40 @@ describe('The Indicator API', () => { }); it('can order indicators based on priority', () => { - const testIndicator1 = generateIndicator( + const testIndicator1 = generateHTMLIndicator( 'test-indicator-1', 'This is a test indicator', openmct.priority.LOW ); openmct.indicators.add(testIndicator1); - const testIndicator2 = generateIndicator( + const testIndicator2 = generateHTMLIndicator( 'test-indicator-2', 'This is another test indicator', openmct.priority.DEFAULT ); openmct.indicators.add(testIndicator2); - const testIndicator3 = generateIndicator( + const testIndicator3 = generateHTMLIndicator( 'test-indicator-3', 'This is yet another test indicator', openmct.priority.LOW ); openmct.indicators.add(testIndicator3); - const testIndicator4 = generateIndicator( + const testIndicator4 = generateHTMLIndicator( 'test-indicator-4', 'This is yet another test indicator', openmct.priority.HIGH ); openmct.indicators.add(testIndicator4); - expect(openmct.indicators.indicatorObjects.length).toBe(5); + const testIndicator5 = generateVueIndicator(openmct.priority.DEFAULT); + openmct.indicators.add(testIndicator5); + + expect(openmct.indicators.indicatorObjects.length).toBe(6); const indicatorObjectsByPriority = openmct.indicators.getIndicatorObjectsByPriority(); - expect(indicatorObjectsByPriority.length).toBe(5); + expect(indicatorObjectsByPriority.length).toBe(6); expect(indicatorObjectsByPriority[2].priority).toBe(openmct.priority.DEFAULT); }); diff --git a/src/plugins/clearData/components/GlobalClearIndicator.vue b/src/plugins/clearData/components/GlobalClearIndicator.vue index 8ca800f88f..71b73ddaeb 100644 --- a/src/plugins/clearData/components/GlobalClearIndicator.vue +++ b/src/plugins/clearData/components/GlobalClearIndicator.vue @@ -20,7 +20,10 @@ at runtime from the About dialog for additional information. -->