Skip to content

Commit 1148644

Browse files
authored
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
1 parent 4cf6306 commit 1148644

22 files changed

+202
-158
lines changed

e2e/tests/functional/plugins/plot/overlayPlot.e2e.spec.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -260,9 +260,9 @@ async function assertLimitLinesExistAndAreVisible(page) {
260260
await waitForPlotsToRender(page);
261261
// Wait for limit lines to be created
262262
await page.waitForSelector('.js-limit-area', { state: 'attached' });
263-
const limitLineCount = await page.locator('.c-plot-limit-line').count();
264263
// There should be 10 limit lines created by default
265-
expect(await page.locator('.c-plot-limit-line').count()).toBe(10);
264+
await expect(page.locator('.c-plot-limit-line')).toHaveCount(10);
265+
const limitLineCount = await page.locator('.c-plot-limit-line').count();
266266
for (let i = 0; i < limitLineCount; i++) {
267267
await expect(page.locator('.c-plot-limit-line').nth(i)).toBeVisible();
268268
}

e2e/tests/functional/plugins/tabs/tabs.e2e.spec.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ test.describe('Tabs View', () => {
5555
await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible();
5656

5757
// no canvas (i.e., sine wave generator) in the document should be visible
58-
await expect(page.locator('canvas')).toBeHidden();
58+
await expect(page.locator('canvas[id=webglContext]')).toBeHidden();
5959

6060
// select second tab
6161
await page.getByLabel(`${notebook.name} tab`).click();
@@ -64,7 +64,7 @@ test.describe('Tabs View', () => {
6464
await expect(page.locator('.c-notebook__drag-area')).toBeVisible();
6565

6666
// no canvas (i.e., sine wave generator) in the document should be visible
67-
await expect(page.locator('canvas')).toBeHidden();
67+
await expect(page.locator('canvas[id=webglContext]')).toBeHidden();
6868

6969
// select third tab
7070
await page.getByLabel(`${sineWaveGenerator.name} tab`).click();
@@ -83,6 +83,6 @@ test.describe('Tabs View', () => {
8383
await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible();
8484

8585
// no canvas (i.e., sine wave generator) in the document should be visible
86-
await expect(page.locator('canvas')).toBeHidden();
86+
await expect(page.locator('canvas[id=webglContext]')).toBeHidden();
8787
});
8888
});

e2e/tests/functional/search.e2e.spec.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ test.describe('Grand Search', () => {
9999
page.waitForNavigation(),
100100
page.getByLabel('OpenMCT Search').getByText('Clock A').click()
101101
]);
102-
await expect(page.getByRole('status', { name: 'Clock' })).toBeVisible();
102+
await expect(page.getByRole('status', { name: 'Clock', exact: true })).toBeVisible();
103103

104104
await grandSearchInput.fill('Disp');
105105
await expect(page.getByLabel('Object Search Result').first()).toContainText(

e2e/tests/visual-a11y/components/header.visual.spec.js

+16
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,22 @@ test.describe('Visual - Header @a11y', () => {
3636
test.beforeEach(async ({ page }) => {
3737
//Go to baseURL and Hide Tree
3838
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
39+
// Wait for status bar to load
40+
await expect(
41+
page.getByRole('status', {
42+
name: 'Clock Indicator'
43+
})
44+
).toBeInViewport();
45+
await expect(
46+
page.getByRole('status', {
47+
name: 'Global Clear Indicator'
48+
})
49+
).toBeInViewport();
50+
await expect(
51+
page.getByRole('status', {
52+
name: 'Snapshot Indicator'
53+
})
54+
).toBeInViewport();
3955
});
4056

4157
test('header sizing', async ({ page, theme }) => {

e2e/tests/visual-a11y/faultManagement.visual.spec.js

+18-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import percySnapshot from '@percy/playwright';
2323
import { fileURLToPath } from 'url';
2424

2525
import * as utils from '../../helper/faultUtils.js';
26-
import { test } from '../../pluginFixtures.js';
26+
import { expect, test } from '../../pluginFixtures.js';
2727

2828
test.describe('Fault Management Visual Tests', () => {
2929
test('icon test', async ({ page, theme }) => {
@@ -32,6 +32,23 @@ test.describe('Fault Management Visual Tests', () => {
3232
});
3333
await page.goto('./', { waitUntil: 'domcontentloaded' });
3434

35+
// Wait for status bar to load
36+
await expect(
37+
page.getByRole('status', {
38+
name: 'Clock Indicator'
39+
})
40+
).toBeInViewport();
41+
await expect(
42+
page.getByRole('status', {
43+
name: 'Global Clear Indicator'
44+
})
45+
).toBeInViewport();
46+
await expect(
47+
page.getByRole('status', {
48+
name: 'Snapshot Indicator'
49+
})
50+
).toBeInViewport();
51+
3552
await percySnapshot(page, `Fault Management icon appears in tree (theme: '${theme}')`);
3653
});
3754

src/api/indicators/IndicatorAPI.js

+24
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,12 @@
2222

2323
import EventEmitter from 'EventEmitter';
2424

25+
import vueWrapHtmlElement from '../../utils/vueWrapHtmlElement.js';
2526
import SimpleIndicator from './SimpleIndicator.js';
2627

2728
class IndicatorAPI extends EventEmitter {
29+
/** @type {import('../../../openmct.js').OpenMCT} */
30+
openmct;
2831
constructor(openmct) {
2932
super();
3033

@@ -42,6 +45,18 @@ class IndicatorAPI extends EventEmitter {
4245
return new SimpleIndicator(this.openmct);
4346
}
4447

48+
/**
49+
* @typedef {import('vue').Component} VueComponent
50+
*/
51+
52+
/**
53+
* @typedef {Object} Indicator
54+
* @property {HTMLElement} [element]
55+
* @property {VueComponent|Promise<VueComponent>} [vueComponent]
56+
* @property {string} key
57+
* @property {number} priority
58+
*/
59+
4560
/**
4661
* Accepts an indicator object, which is a simple object
4762
* with a two attributes: 'element' which has an HTMLElement
@@ -62,11 +77,20 @@ class IndicatorAPI extends EventEmitter {
6277
* myIndicator.text("Hello World!");
6378
* myIndicator.iconClass("icon-info");
6479
*
80+
* If you would like to use a Vue component, you can pass it in
81+
* directly as the 'vueComponent' attribute of the indicator object.
82+
* This accepts a Vue component or a promise that resolves to a Vue component (for asynchronous
83+
* rendering).
84+
*
85+
* @param {Indicator} indicator
6586
*/
6687
add(indicator) {
6788
if (!indicator.priority) {
6889
indicator.priority = this.openmct.priority.DEFAULT;
6990
}
91+
if (!indicator.vueComponent) {
92+
indicator.vueComponent = vueWrapHtmlElement(indicator.element);
93+
}
7094

7195
this.indicatorObjects.push(indicator);
7296

src/api/indicators/IndicatorAPISpec.js

+31-9
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
* this source code distribution or the Licensing information page available
2020
* at runtime from the About dialog for additional information.
2121
*****************************************************************************/
22+
import { defineComponent } from 'vue';
23+
2224
import { createOpenMct, resetApplicationState } from '../../utils/testing.js';
2325
import SimpleIndicator from './SimpleIndicator.js';
2426

@@ -33,7 +35,7 @@ describe('The Indicator API', () => {
3335
return resetApplicationState(openmct);
3436
});
3537

36-
function generateIndicator(className, label, priority) {
38+
function generateHTMLIndicator(className, label, priority) {
3739
const element = document.createElement('div');
3840
element.classList.add(className);
3941
const textNode = document.createTextNode(label);
@@ -46,46 +48,66 @@ describe('The Indicator API', () => {
4648
return testIndicator;
4749
}
4850

49-
it('can register an indicator', () => {
50-
const testIndicator = generateIndicator('test-indicator', 'This is a test indicator', 2);
51+
function generateVueIndicator(priority) {
52+
return {
53+
vueComponent: defineComponent({
54+
template: '<div class="test-indicator">This is a test indicator</div>'
55+
}),
56+
priority
57+
};
58+
}
59+
60+
it('can register an HTML indicator', () => {
61+
const testIndicator = generateHTMLIndicator('test-indicator', 'This is a test indicator', 2);
62+
openmct.indicators.add(testIndicator);
63+
expect(openmct.indicators.indicatorObjects).toBeDefined();
64+
// notifier indicator is installed by default
65+
expect(openmct.indicators.indicatorObjects.length).toBe(2);
66+
});
67+
68+
it('can register a Vue indicator', () => {
69+
const testIndicator = generateVueIndicator(2);
5170
openmct.indicators.add(testIndicator);
5271
expect(openmct.indicators.indicatorObjects).toBeDefined();
5372
// notifier indicator is installed by default
5473
expect(openmct.indicators.indicatorObjects.length).toBe(2);
5574
});
5675

5776
it('can order indicators based on priority', () => {
58-
const testIndicator1 = generateIndicator(
77+
const testIndicator1 = generateHTMLIndicator(
5978
'test-indicator-1',
6079
'This is a test indicator',
6180
openmct.priority.LOW
6281
);
6382
openmct.indicators.add(testIndicator1);
6483

65-
const testIndicator2 = generateIndicator(
84+
const testIndicator2 = generateHTMLIndicator(
6685
'test-indicator-2',
6786
'This is another test indicator',
6887
openmct.priority.DEFAULT
6988
);
7089
openmct.indicators.add(testIndicator2);
7190

72-
const testIndicator3 = generateIndicator(
91+
const testIndicator3 = generateHTMLIndicator(
7392
'test-indicator-3',
7493
'This is yet another test indicator',
7594
openmct.priority.LOW
7695
);
7796
openmct.indicators.add(testIndicator3);
7897

79-
const testIndicator4 = generateIndicator(
98+
const testIndicator4 = generateHTMLIndicator(
8099
'test-indicator-4',
81100
'This is yet another test indicator',
82101
openmct.priority.HIGH
83102
);
84103
openmct.indicators.add(testIndicator4);
85104

86-
expect(openmct.indicators.indicatorObjects.length).toBe(5);
105+
const testIndicator5 = generateVueIndicator(openmct.priority.DEFAULT);
106+
openmct.indicators.add(testIndicator5);
107+
108+
expect(openmct.indicators.indicatorObjects.length).toBe(6);
87109
const indicatorObjectsByPriority = openmct.indicators.getIndicatorObjectsByPriority();
88-
expect(indicatorObjectsByPriority.length).toBe(5);
110+
expect(indicatorObjectsByPriority.length).toBe(6);
89111
expect(indicatorObjectsByPriority[2].priority).toBe(openmct.priority.DEFAULT);
90112
});
91113

src/plugins/clearData/components/GlobalClearIndicator.vue

+4-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@
2020
at runtime from the About dialog for additional information.
2121
-->
2222
<template>
23-
<div class="c-indicator c-indicator--clickable icon-clear-data s-status-caution">
23+
<div
24+
aria-label="Global Clear Indicator"
25+
class="c-indicator c-indicator--clickable icon-clear-data s-status-caution"
26+
>
2427
<span class="label c-indicator__label">
2528
<button @click="globalClearEmit">Clear Data</button>
2629
</span>

src/plugins/clearData/plugin.js

+2-21
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@
1919
* this source code distribution or the Licensing information page available
2020
* at runtime from the About dialog for additional information.
2121
*****************************************************************************/
22-
import mount from 'utils/mount';
23-
2422
import ClearDataAction from './ClearDataAction.js';
2523
import GlobalClearIndicator from './components/GlobalClearIndicator.vue';
2624

@@ -31,27 +29,10 @@ export default function plugin(appliesToObjects, options = { indicator: true })
3129

3230
return function install(openmct) {
3331
if (installIndicator) {
34-
const { vNode, destroy } = mount(
35-
{
36-
components: {
37-
GlobalClearIndicator
38-
},
39-
provide: {
40-
openmct
41-
},
42-
template: '<GlobalClearIndicator></GlobalClearIndicator>'
43-
},
44-
{
45-
app: openmct.app,
46-
element: document.createElement('div')
47-
}
48-
);
49-
5032
let indicator = {
51-
element: vNode.el,
33+
vueComponent: GlobalClearIndicator,
5234
key: 'global-clear-indicator',
53-
priority: openmct.priority.DEFAULT,
54-
destroy: destroy
35+
priority: openmct.priority.DEFAULT
5536
};
5637

5738
openmct.indicators.add(indicator);

src/plugins/clearData/pluginSpec.js

+3-8
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
*****************************************************************************/
2222

2323
import { createMouseEvent, createOpenMct, resetApplicationState } from 'utils/testing';
24-
import { nextTick } from 'vue';
2524

2625
import ClearDataPlugin from './plugin.js';
2726

@@ -208,12 +207,11 @@ describe('The Clear Data Plugin:', () => {
208207
it('installs', () => {
209208
const globalClearIndicator = openmct.indicators.indicatorObjects.find(
210209
(indicator) => indicator.key === 'global-clear-indicator'
211-
).element;
210+
).vueComponent;
212211
expect(globalClearIndicator).toBeDefined();
213212
});
214213

215-
it('renders its major elements', async () => {
216-
await nextTick();
214+
it('renders its major elements', () => {
217215
const indicatorClass = appHolder.querySelector('.c-indicator');
218216
const iconClass = appHolder.querySelector('.icon-clear-data');
219217
const indicatorLabel = appHolder.querySelector('.c-indicator__label');
@@ -228,10 +226,7 @@ describe('The Clear Data Plugin:', () => {
228226
const indicatorLabel = appHolder.querySelector('.c-indicator__label');
229227
const buttonElement = indicatorLabel.querySelector('button');
230228
const clickEvent = createMouseEvent('click');
231-
openmct.objectViews.on('clearData', () => {
232-
// when we click the button, this event should fire
233-
done();
234-
});
229+
openmct.objectViews.on('clearData', done);
235230
buttonElement.dispatchEvent(clickEvent);
236231
});
237232
});

src/plugins/clock/components/ClockIndicator.vue

+12-6
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
<template>
2424
<div
25+
aria-label="Clock Indicator"
2526
class="c-indicator t-indicator-clock icon-clock no-minify c-indicator--not-clickable"
2627
role="complementary"
2728
>
@@ -40,27 +41,32 @@ export default {
4041
props: {
4142
indicatorFormat: {
4243
type: String,
43-
required: true
44+
default: 'YYYY/MM/DD HH:mm:ss'
4445
}
4546
},
4647
data() {
4748
return {
48-
timeTextValue: this.openmct.time.getClock() ? this.openmct.time.now() : undefined
49+
timestamp: this.openmct.time.getClock() ? this.openmct.time.now() : undefined
4950
};
5051
},
52+
computed: {
53+
timeTextValue() {
54+
return `${moment.utc(this.timestamp).format(this.indicatorFormat)} ${
55+
this.openmct.time.getTimeSystem().name
56+
}`;
57+
}
58+
},
5159
mounted() {
5260
this.tick = raf(this.tick);
5361
this.openmct.time.on('tick', this.tick);
54-
this.tick(this.timeTextValue);
62+
this.tick(this.timestamp);
5563
},
5664
beforeUnmount() {
5765
this.openmct.time.off('tick', this.tick);
5866
},
5967
methods: {
6068
tick(timestamp) {
61-
this.timeTextValue = `${moment.utc(timestamp).format(this.indicatorFormat)} ${
62-
this.openmct.time.getTimeSystem().name
63-
}`;
69+
this.timestamp = timestamp;
6470
}
6571
}
6672
};

0 commit comments

Comments
 (0)