diff --git a/.cspell.json b/.cspell.json index fb82973268..49dc94e839 100644 --- a/.cspell.json +++ b/.cspell.json @@ -479,7 +479,8 @@ "mediump", "sinonjs", "generatedata", - "grandsearch" + "grandsearch", + "websockets" ], "dictionaries": ["npm", "softwareTerms", "node", "html", "css", "bash", "en_US"], "ignorePaths": [ diff --git a/e2e/README.md b/e2e/README.md index aa8cb6d969..8b1c9db91b 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -82,13 +82,20 @@ To make this possible, we're leveraging a 3rd party service, [Percy](https://per At present, we are using percy with two configuration files: `./e2e/.percy.nightly.yml` and `./e2e/.percy.ci.yml`. This is mainly to reduce the number of snapshots. -### (Advanced) Snapshot Testing +### Advanced: Snapshot Testing (Not Recommended) -Snapshot testing is very similar to visual testing but allows us to be more precise in detecting change without relying on a 3rd party service. Unfortuantely, this precision requires advanced test setup and teardown and so we're using this pattern as a last resort. +While snapshot testing offers a precise way to detect changes in your application without relying on third-party services like Percy.io, we've found that it doesn't offer any advantages over visual testing in our use-cases. Therefore, snapshot testing is **not recommended** for further implementation. -To give an example, if a _single_ visual test assertion for an Overlay plot is run through multiple DOM rendering engines at various viewports to see how the Plot looks. If that same test were run as a snapshot test, it could only be executed against a single browser, on a single platform (ubuntu docker container). +#### CI vs Manual Checks +Snapshot tests can be reliably executed in Continuous Integration (CI) environments but lack the manual oversight provided by visual testing platforms like Percy.io. This means they may miss issues that a human reviewer could catch during manual checks. + +#### Example +A single visual test assertion in Percy.io can be executed across 10 different browser and resolution combinations without additional setup, providing comprehensive testing with minimal configuration. In contrast, a snapshot test is restricted to a single OS and browser resolution, requiring more effort to achieve the same level of coverage. + + +#### Further Reading +For those interested in the mechanics of snapshot testing with Playwright, you can refer to the [Playwright Snapshots Documentation](https://playwright.dev/docs/test-snapshots). However, keep in mind that we do not recommend using this approach. -Read more about [Playwright Snapshots](https://playwright.dev/docs/test-snapshots) #### Open MCT's implementation @@ -308,14 +315,27 @@ Skipping based on browser version (Rarely used): "Automated tests should verify that the application code works for the end users, and avoid relying on implementation details such as things which users will not typically use, see, or even know about such as the name of a function, whether something is an array, or the CSS class of some element. The end user will see or interact with what is rendered on the page, so your test should typically only see/interact with the same rendered output." + +By adhering to this principle, we can create tests that are both robust and reflective of actual user experiences. + +#### How to make tests robust to function in other contexts (VISTA, COUCHDB, YAMCS, VIPER, etc.) + 1. Leverage the use of `appActions.js` methods such as `createDomainObjectWithDefaults()`. This ensures that your tests will create unique instances of objects for your test to interact with. + 1. Do not assert on the order or structure of objects available unless you created them yourself. These tests may be used against a persistent datastore like couchdb with many objects in the tree. + 1. Do not search for your created objects. Open MCT does not performance uniqueness checks so it's possible that your tests will break when run twice. + 1. Avoid creating locator aliases. This likely means that you're compensating for a bad locator. Improve the application instead. + 1. Leverage `await page.goto('./', { waitUntil: 'domcontentloaded' });` instead of `{ waitUntil: 'networkidle' }`. Tests run against deployments with websockets often have issues with the networkidle detection. + +#### How to make tests faster and more resilient + 1. Avoid app interaction when possible. The best way of doing this is to navigate directly by URL: + + ```js // You can capture the CreatedObjectInfo returned from this appAction: const clock = await createDomainObjectWithDefaults(page, { type: 'Clock' }); @@ -323,12 +343,14 @@ Skipping based on browser version (Rarely used): { + + // All subsequent tests in this suite will override the clock + test.use({ + clockOptions: { + now: 1732413600000, // A timestamp given as milliseconds since the epoch + shouldAdvanceTime: true // Should the clock tick? + } + }); + + test('bar test', async ({ page }) => { + // ... + }); +}); + ``` + More info and options for `overrideClock` can be found in [baseFixtures.js](baseFixtures.js) + - Working with multiple pages -There are instances where multiple browser pages will need to be opened to verify multi-page or multi-tab application behavior. +There are instances where multiple browser pages will needed to verify multi-page or multi-tab application behavior. Make sure to use the `@2p` annotation as well as name each page appropriately: i.e. `page1` and `page2` or `tab1` and `tab2` depending on the intended use case. Generally pages should be used unless testing `sharedWorker` code, specifically. ### Reporting diff --git a/e2e/appActions.js b/e2e/appActions.js index 97e45bb83f..a308d13816 100644 --- a/e2e/appActions.js +++ b/e2e/appActions.js @@ -78,7 +78,7 @@ async function createDomainObjectWithDefaults( // Navigate to the parent object. This is necessary to create the object // in the correct location, such as a folder, layout, or plot. - await page.goto(`${parentUrl}?hideTree=true`); + await page.goto(`${parentUrl}`); //Click the Create button await page.click('button:has-text("Create")'); @@ -179,7 +179,7 @@ async function createPlanFromJSON(page, { name, json, parent = 'mine' }) { // Navigate to the parent object. This is necessary to create the object // in the correct location, such as a folder, layout, or plot. - await page.goto(`${parentUrl}?hideTree=true`); + await page.goto(`${parentUrl}`); // Click the Create button await page.click('button:has-text("Create")'); @@ -232,7 +232,7 @@ async function createExampleTelemetryObject(page, parent = 'mine') { const name = 'VIPER Rover Heading'; const nameInputLocator = page.getByRole('dialog').locator('input[type="text"]'); - await page.goto(`${parentUrl}?hideTree=true`); + await page.goto(`${parentUrl}`); await page.locator('button:has-text("Create")').click(); diff --git a/e2e/tests/functional/plugins/timer/timer.e2e.spec.js b/e2e/tests/functional/plugins/timer/timer.e2e.spec.js index 6a7d4ad3cf..bdab38997b 100644 --- a/e2e/tests/functional/plugins/timer/timer.e2e.spec.js +++ b/e2e/tests/functional/plugins/timer/timer.e2e.spec.js @@ -25,12 +25,15 @@ const { openObjectTreeContextMenu, createDomainObjectWithDefaults } = require('../../../../appActions'); +import { MISSION_TIME } from '../../../../constants'; test.describe('Timer', () => { let timer; + test.beforeEach(async ({ page }) => { await page.goto('./', { waitUntil: 'domcontentloaded' }); timer = await createDomainObjectWithDefaults(page, { type: 'timer' }); + await assertTimerElements(page, timer); }); test('Can perform actions on the Timer', async ({ page }) => { @@ -63,6 +66,70 @@ test.describe('Timer', () => { }); }); +test.describe('Timer with target date', () => { + let timer; + + test.beforeEach(async ({ page }) => { + await page.goto('./', { waitUntil: 'domcontentloaded' }); + timer = await createDomainObjectWithDefaults(page, { type: 'timer' }); + await assertTimerElements(page, timer); + }); + + // Override clock + test.use({ + clockOptions: { + now: MISSION_TIME, + shouldAdvanceTime: true + } + }); + + test('Can count down to a target date', async ({ page }) => { + // Set the target date to 2024-11-24 03:30:00 + await page.getByTitle('More options').click(); + await page.getByRole('menuitem', { name: /Edit Properties.../ }).click(); + await page.getByPlaceholder('YYYY-MM-DD').fill('2024-11-24'); + await page.locator('input[name="hour"]').fill('3'); + await page.locator('input[name="min"]').fill('30'); + await page.locator('input[name="sec"]').fill('00'); + await page.getByLabel('Save').click(); + + // Get the current timer seconds value + const timerSecValue = (await page.locator('.c-timer__value').innerText()).split(':').at(-1); + await expect(page.locator('.c-timer__direction')).toHaveClass(/icon-minus/); + + // Wait for the timer to count down and assert + await expect + .poll(async () => { + const newTimerValue = (await page.locator('.c-timer__value').innerText()).split(':').at(-1); + return Number(newTimerValue); + }) + .toBeLessThan(Number(timerSecValue)); + }); + + test('Can count up from a target date', async ({ page }) => { + // Set the target date to 2020-11-23 03:30:00 + await page.getByTitle('More options').click(); + await page.getByRole('menuitem', { name: /Edit Properties.../ }).click(); + await page.getByPlaceholder('YYYY-MM-DD').fill('2020-11-23'); + await page.locator('input[name="hour"]').fill('3'); + await page.locator('input[name="min"]').fill('30'); + await page.locator('input[name="sec"]').fill('00'); + await page.getByLabel('Save').click(); + + // Get the current timer seconds value + const timerSecValue = (await page.locator('.c-timer__value').innerText()).split(':').at(-1); + await expect(page.locator('.c-timer__direction')).toHaveClass(/icon-plus/); + + // Wait for the timer to count up and assert + await expect + .poll(async () => { + const newTimerValue = (await page.locator('.c-timer__value').innerText()).split(':').at(-1); + return Number(newTimerValue); + }) + .toBeGreaterThan(Number(timerSecValue)); + }); +}); + /** * Actions that can be performed on a timer from context menus. * @typedef {'Start' | 'Stop' | 'Pause' | 'Restart at 0'} TimerAction @@ -141,14 +208,17 @@ function buttonTitleFromAction(action) { * @param {TimerAction} action */ async function assertTimerStateAfterAction(page, action) { + const timerValue = page.locator('.c-timer__value'); let timerStateClass; switch (action) { case 'Start': case 'Restart at 0': timerStateClass = 'is-started'; + expect(await timerValue.innerText()).toBe('0D 00:00:00'); break; case 'Stop': timerStateClass = 'is-stopped'; + expect(await timerValue.innerText()).toBe('--:--:--'); break; case 'Pause': timerStateClass = 'is-paused'; @@ -157,3 +227,25 @@ async function assertTimerStateAfterAction(page, action) { await expect.soft(page.locator('.c-timer')).toHaveClass(new RegExp(timerStateClass)); } + +/** + * Assert that all the major components of a timer are present in the DOM. + * @param {import('@playwright/test').Page} page + * @param {import('../../../../appActions').CreatedObjectInfo} timer + */ +async function assertTimerElements(page, timer) { + const timerElement = page.locator('.c-timer'); + const resetButton = page.getByRole('button', { name: 'Reset' }); + const pausePlayButton = page + .getByRole('button', { name: 'Pause' }) + .or(page.getByRole('button', { name: 'Start' })); + const timerDirectionIcon = page.locator('.c-timer__direction'); + const timerValue = page.locator('.c-timer__value'); + + expect(await page.locator('.l-browse-bar__object-name').innerText()).toBe(timer.name); + expect(timerElement).toBeAttached(); + expect(resetButton).toBeAttached(); + expect(pausePlayButton).toBeAttached(); + expect(timerDirectionIcon).toBeAttached(); + expect(timerValue).toBeAttached(); +} diff --git a/src/api/forms/components/controls/Datetime.vue b/src/api/forms/components/controls/Datetime.vue index 921ec03774..017efc0058 100644 --- a/src/api/forms/components/controls/Datetime.vue +++ b/src/api/forms/components/controls/Datetime.vue @@ -123,7 +123,6 @@ export default { formatDatetime(timestamp = this.model.value) { if (!timestamp) { this.resetValues(); - return; } @@ -137,7 +136,7 @@ export default { const data = { model, - value: timestamp + value: new Date(timestamp).toISOString() }; this.$emit('onChange', data); diff --git a/src/plugins/charts/bar/BarGraphPlot.vue b/src/plugins/charts/bar/BarGraphPlot.vue index eae495ab8e..623a4e23ba 100644 --- a/src/plugins/charts/bar/BarGraphPlot.vue +++ b/src/plugins/charts/bar/BarGraphPlot.vue @@ -86,12 +86,15 @@ export default { handler: 'updateData' } }, + created() { + this.registerListeners(); + }, mounted() { + this.plotResizeObserver.observe(this.$refs.plotWrapper); Plotly.newPlot(this.$refs.plot, Array.from(this.data), this.getLayout(), { responsive: true, displayModeBar: false }); - this.registerListeners(); }, beforeUnmount() { if (this.plotResizeObserver) { @@ -226,7 +229,6 @@ export default { window.dispatchEvent(new Event('resize')); }, 250); }); - this.plotResizeObserver.observe(this.$refs.plotWrapper); } }, reset() { diff --git a/src/plugins/charts/bar/BarGraphViewProvider.js b/src/plugins/charts/bar/BarGraphViewProvider.js index 5c72b40154..e6518be3ed 100644 --- a/src/plugins/charts/bar/BarGraphViewProvider.js +++ b/src/plugins/charts/bar/BarGraphViewProvider.js @@ -43,17 +43,16 @@ export default function BarGraphViewProvider(openmct) { return domainObject && domainObject.type === BAR_GRAPH_KEY; }, - view: function (domainObject, objectPath) { + view(domainObject, objectPath) { let _destroy = null; let component = null; return { - show: function (element) { + show(element) { let isCompact = isCompactView(objectPath); const { vNode, destroy } = mount( { - el: element, components: { BarGraphView }, @@ -79,7 +78,7 @@ export default function BarGraphViewProvider(openmct) { _destroy = destroy; component = vNode.componentInstance; }, - destroy: function () { + destroy() { _destroy(); }, onClearData() { diff --git a/src/plugins/charts/bar/pluginSpec.js b/src/plugins/charts/bar/pluginSpec.js index 74b956285c..5400d0f07a 100644 --- a/src/plugins/charts/bar/pluginSpec.js +++ b/src/plugins/charts/bar/pluginSpec.js @@ -220,7 +220,7 @@ describe('the plugin', function () { }); }); - xdescribe('The spectral plot view for telemetry objects with array values', () => { + describe('The spectral plot view for telemetry objects with array values', () => { let barGraphObject; // eslint-disable-next-line no-unused-vars let mockComposition; @@ -256,7 +256,7 @@ describe('the plugin', function () { await Vue.nextTick(); }); - it('Renders spectral plots', () => { + it('Renders spectral plots', async () => { const dotFullTelemetryObject = { identifier: { namespace: 'someNamespace', @@ -304,11 +304,12 @@ describe('the plugin', function () { barGraphView.show(child, true); mockComposition.emit('add', dotFullTelemetryObject); - return Vue.nextTick().then(() => { - const plotElement = element.querySelector('.cartesianlayer .scatterlayer .trace .lines'); - expect(plotElement).not.toBeNull(); - barGraphView.destroy(); - }); + await Vue.nextTick(); + await Vue.nextTick(); + + const plotElement = element.querySelector('.cartesianlayer .scatterlayer .trace .lines'); + expect(plotElement).not.toBeNull(); + barGraphView.destroy(); }); }); diff --git a/src/plugins/plot/Plot.vue b/src/plugins/plot/Plot.vue index cda7177283..8901a78e59 100644 --- a/src/plugins/plot/Plot.vue +++ b/src/plugins/plot/Plot.vue @@ -172,11 +172,11 @@ export default { this.cursorGuide = newCursorGuide; } }, - mounted() { + created() { eventHelpers.extend(this); this.imageExporter = new ImageExporter(this.openmct); - this.loadComposition(); this.stalenessSubscription = {}; + this.loadComposition(); }, beforeUnmount() { this.destroy(); diff --git a/src/plugins/plot/inspector/PlotOptionsEdit.vue b/src/plugins/plot/inspector/PlotOptionsEdit.vue index c12185f573..8ce54adb1e 100644 --- a/src/plugins/plot/inspector/PlotOptionsEdit.vue +++ b/src/plugins/plot/inspector/PlotOptionsEdit.vue @@ -85,9 +85,11 @@ export default { return !this.isStackedPlotObject && this.yAxes.filter((yAxis) => yAxis.seriesCount > 0); } }, - mounted() { + created() { eventHelpers.extend(this); this.config = this.getConfig(); + }, + mounted() { if (!this.isStackedPlotObject) { this.yAxes = [ { diff --git a/src/plugins/plot/legend/PlotLegend.vue b/src/plugins/plot/legend/PlotLegend.vue index f74507053f..f1110a4abc 100644 --- a/src/plugins/plot/legend/PlotLegend.vue +++ b/src/plugins/plot/legend/PlotLegend.vue @@ -138,17 +138,18 @@ export default { return this.loaded && this.legend.get('valueToShowWhenCollapsed'); } }, - mounted() { - this.seriesModels = []; + created() { eventHelpers.extend(this); this.config = this.getConfig(); this.legend = this.config.legend; + this.seriesModels = []; + this.listenTo(this.config.legend, 'change:position', this.updatePosition, this); + this.initialize(); + }, + mounted() { this.loaded = true; this.isLegendExpanded = this.legend.get('expanded') === true; - this.listenTo(this.config.legend, 'change:position', this.updatePosition, this); this.updatePosition(); - - this.initialize(); }, beforeUnmount() { if (this.objectComposition) { diff --git a/src/plugins/plot/overlayPlot/pluginSpec.js b/src/plugins/plot/overlayPlot/pluginSpec.js index 4c2d081c77..8cb796458b 100644 --- a/src/plugins/plot/overlayPlot/pluginSpec.js +++ b/src/plugins/plot/overlayPlot/pluginSpec.js @@ -20,20 +20,22 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ +import mount from 'utils/mount'; import { createMouseEvent, createOpenMct, resetApplicationState, spyOnBuiltins } from 'utils/testing'; +import { nextTick } from 'vue'; + +import PlotOptions from '../inspector/PlotOptions.vue'; import PlotVuePlugin from '../plugin'; -import Vue from 'vue'; import Plot from '../Plot.vue'; import configStore from '../configuration/ConfigStore'; import EventEmitter from 'EventEmitter'; -import PlotOptions from '../inspector/PlotOptions.vue'; -xdescribe('the plugin', function () { +describe('the plugin', function () { let element; let child; let openmct; @@ -147,13 +149,13 @@ xdescribe('the plugin', function () { openmct.startHeadless(); }); - afterEach((done) => { + afterEach(async () => { openmct.time.timeSystem('utc', { start: 0, end: 1 }); configStore.deleteAll(); - resetApplicationState(openmct).then(done).catch(done); + await resetApplicationState(openmct); }); afterAll(() => { @@ -184,15 +186,15 @@ xdescribe('the plugin', function () { let testTelemetryObject; let testTelemetryObject2; let config; - let component; let mockComposition; + let destroyPlot; afterAll(() => { - component.$destroy(); + destroyPlot(); openmct.router.path = null; }); - beforeEach(() => { + beforeEach(async () => { testTelemetryObject = { identifier: { namespace: '', @@ -301,46 +303,53 @@ xdescribe('the plugin', function () { spyOn(openmct.composition, 'get').and.returnValue(mockComposition); let viewContainer = document.createElement('div'); - child.append(viewContainer); - component = new Vue({ - el: viewContainer, - components: { - Plot + child.appendChild(viewContainer); + const composition = openmct.composition.get(overlayPlotObject); + const { destroy } = mount( + { + components: { + Plot + }, + provide: { + openmct, + domainObject: overlayPlotObject, + composition, + path: [overlayPlotObject] + }, + template: '' }, - provide: { - openmct: openmct, - domainObject: overlayPlotObject, - composition: openmct.composition.get(overlayPlotObject), - path: [overlayPlotObject] - }, - template: '' - }); + { + element: child + } + ); - return telemetryPromise.then(Vue.nextTick()).then(() => { - const configId = openmct.objects.makeKeyString(overlayPlotObject.identifier); - config = configStore.get(configId); - }); + destroyPlot = destroy; + + await telemetryPromise; + await nextTick(); + + const configId = openmct.objects.makeKeyString(overlayPlotObject.identifier); + config = configStore.get(configId); }); - it('Renders multiple Y-axis for the telemetry objects', (done) => { + it('Renders multiple Y-axis for the telemetry objects', async () => { config.yAxis.set('displayRange', { min: 10, max: 20 }); - Vue.nextTick(() => { - let yAxisElement = element.querySelectorAll( - '.gl-plot-axis-area.gl-plot-y .gl-plot-tick-wrapper' - ); - expect(yAxisElement.length).toBe(2); - done(); - }); + await nextTick(); + let yAxisElement = element.querySelectorAll( + '.gl-plot-axis-area.gl-plot-y .gl-plot-tick-wrapper' + ); + expect(yAxisElement.length).toBe(2); }); describe('the inspector view', () => { let inspectorComponent; let viewComponentObject; let selection; - beforeEach((done) => { + let destroyPlotOptions; + beforeEach(async () => { selection = [ [ { @@ -357,40 +366,43 @@ xdescribe('the plugin', function () { ] ]; - let viewContainer = document.createElement('div'); - child.append(viewContainer); - inspectorComponent = new Vue({ - el: viewContainer, - components: { - PlotOptions + const viewContainer = document.createElement('div'); + child.appendChild(viewContainer); + const { vNode, destroy } = mount( + { + components: { + PlotOptions + }, + provide: { + openmct, + domainObject: selection[0][0].context.item, + path: [selection[0][0].context.item] + }, + template: '' }, - provide: { - openmct: openmct, - domainObject: selection[0][0].context.item, - path: [selection[0][0].context.item] - }, - template: '' - }); + { + element: viewContainer + } + ); + inspectorComponent = vNode.componentInstance; + destroyPlotOptions = destroy; - Vue.nextTick(() => { - viewComponentObject = inspectorComponent.$root.$children[0]; - done(); - }); + await nextTick(); + viewComponentObject = inspectorComponent.$refs.plotOptionsRef; }); afterEach(() => { + destroyPlotOptions(); openmct.router.path = null; }); describe('in edit mode', () => { let editOptionsEl; - beforeEach((done) => { + beforeEach(async () => { viewComponentObject.setEditState(true); - Vue.nextTick(() => { - editOptionsEl = viewComponentObject.$el.querySelector('.js-plot-options-edit'); - done(); - }); + await nextTick(); + editOptionsEl = viewComponentObject.$el.querySelector('.js-plot-options-edit'); }); it('shows multiple yAxis options', () => { @@ -419,15 +431,15 @@ xdescribe('the plugin', function () { describe('The overlay plot view with single axes', () => { let testTelemetryObject; let config; - let component; let mockComposition; + let destroyOverlayPlot; afterAll(() => { - component.$destroy(); + destroyOverlayPlot(); openmct.router.path = null; }); - beforeEach(() => { + beforeEach(async () => { testTelemetryObject = { identifier: { namespace: '', @@ -481,41 +493,45 @@ xdescribe('the plugin', function () { }; spyOn(openmct.composition, 'get').and.returnValue(mockComposition); - - let viewContainer = document.createElement('div'); - child.append(viewContainer); - component = new Vue({ - el: viewContainer, - components: { - Plot + const composition = openmct.composition.get(overlayPlotObject); + const viewContainer = document.createElement('div'); + child.appendChild(viewContainer); + const { destroy } = mount( + { + components: { + Plot + }, + provide: { + openmct: openmct, + domainObject: overlayPlotObject, + composition, + path: [overlayPlotObject] + }, + template: '' }, - provide: { - openmct: openmct, - domainObject: overlayPlotObject, - composition: openmct.composition.get(overlayPlotObject), - path: [overlayPlotObject] - }, - template: '' - }); + { + element: viewContainer + } + ); - return telemetryPromise.then(Vue.nextTick()).then(() => { - const configId = openmct.objects.makeKeyString(overlayPlotObject.identifier); - config = configStore.get(configId); - }); + destroyOverlayPlot = destroy; + + await telemetryPromise; + await nextTick(); + const configId = openmct.objects.makeKeyString(overlayPlotObject.identifier); + config = configStore.get(configId); }); - it('Renders single Y-axis for the telemetry object', (done) => { + it('Renders single Y-axis for the telemetry object', async () => { config.yAxis.set('displayRange', { min: 10, max: 20 }); - Vue.nextTick(() => { - let yAxisElement = element.querySelectorAll( - '.gl-plot-axis-area.gl-plot-y .gl-plot-tick-wrapper' - ); - expect(yAxisElement.length).toBe(1); - done(); - }); + await nextTick(); + let yAxisElement = element.querySelectorAll( + '.gl-plot-axis-area.gl-plot-y .gl-plot-tick-wrapper' + ); + expect(yAxisElement.length).toBe(1); }); }); }); diff --git a/src/plugins/plot/pluginSpec.js b/src/plugins/plot/pluginSpec.js index dd4eaa23c1..dbe9fcc291 100644 --- a/src/plugins/plot/pluginSpec.js +++ b/src/plugins/plot/pluginSpec.js @@ -20,6 +20,8 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ +import EventEmitter from 'EventEmitter'; +import mount from 'utils/mount'; import { createMouseEvent, createOpenMct, @@ -29,13 +31,12 @@ import { import PlotVuePlugin from './plugin'; import Vue from 'vue'; import configStore from './configuration/ConfigStore'; -import EventEmitter from 'EventEmitter'; import PlotOptions from './inspector/PlotOptions.vue'; import PlotConfigurationModel from './configuration/PlotConfigurationModel'; const TEST_KEY_ID = 'some-other-key'; -xdescribe('the plugin', function () { +describe('the plugin', function () { let element; let child; let openmct; @@ -164,14 +165,15 @@ xdescribe('the plugin', function () { openmct.startHeadless(); }); - afterEach((done) => { - openmct.time.timeSystem('utc', { + afterEach(async () => { + openmct.time.setTimeSystem('utc', { start: 0, end: 2 }); + await Vue.nextTick(); configStore.deleteAll(); - resetApplicationState(openmct).then(done).catch(done); + return resetApplicationState(openmct); }); describe('the plot views', () => { @@ -382,13 +384,15 @@ xdescribe('the plugin', function () { expect(openmct.telemetry.request).toHaveBeenCalledTimes(1); }); - it('Renders a collapsed legend for every telemetry', () => { + it('Renders a collapsed legend for every telemetry', async () => { + await Vue.nextTick(); let legend = element.querySelectorAll('.plot-wrapper-collapsed-legend .plot-series-name'); expect(legend.length).toBe(1); expect(legend[0].innerHTML).toEqual('Test Object'); }); - it('Renders an expanded legend for every telemetry', () => { + it('Renders an expanded legend for every telemetry', async () => { + await Vue.nextTick(); let legendControl = element.querySelector( '.c-plot-legend__view-control.gl-plot-legend__view-control.c-disclosure-triangle' ); @@ -421,7 +425,8 @@ xdescribe('the plugin', function () { }); }); - it('Renders Y-axis options for the telemetry object', () => { + it('Renders Y-axis options for the telemetry object', async () => { + await Vue.nextTick(); let yAxisElement = element.querySelectorAll( '.gl-plot-axis-area.gl-plot-y .gl-plot-y-label__select' ); @@ -433,7 +438,7 @@ xdescribe('the plugin', function () { expect(options[1].value).toBe('Another attribute'); }); - it('Updates the Y-axis label when changed', () => { + xit('Updates the Y-axis label when changed', () => { const configId = openmct.objects.makeKeyString(testTelemetryObject.identifier); const config = configStore.get(configId); const yAxisElement = element.querySelectorAll('.gl-plot-axis-area.gl-plot-y')[0].__vue__; @@ -456,7 +461,8 @@ xdescribe('the plugin', function () { describe('pause and play controls', () => { beforeEach(() => { - openmct.time.clock('local', { + openmct.time.setClock('local'); + openmct.time.setClockOffsets({ start: -1000, end: 100 }); @@ -487,7 +493,8 @@ xdescribe('the plugin', function () { describe('resume actions on errant click', () => { beforeEach(() => { - openmct.time.clock('local', { + openmct.time.setClock('local'); + openmct.time.setClockOffsets({ start: -1000, end: 100 }); @@ -560,7 +567,8 @@ xdescribe('the plugin', function () { expect(limitEl.length).toBe(0); }); - it('lines are displayed when configuration is set to true', (done) => { + it('lines are displayed when configuration is set to true', async () => { + await Vue.nextTick(); const configId = openmct.objects.makeKeyString(testTelemetryObject.identifier); const config = configStore.get(configId); config.yAxis.set('displayRange', { @@ -569,11 +577,9 @@ xdescribe('the plugin', function () { }); config.series.models[0].set('limitLines', true); - Vue.nextTick(() => { - let limitEl = element.querySelectorAll('.js-limit-area .js-limit-line'); - expect(limitEl.length).toBe(4); - done(); - }); + await Vue.nextTick(); + let limitEl = element.querySelectorAll('.js-limit-area .js-limit-line'); + expect(limitEl.length).toBe(4); }); }); }); @@ -676,14 +682,13 @@ xdescribe('the plugin', function () { openmct.router.path = null; }); - it('requests historical data when over the threshold', (done) => { + xit('requests historical data when over the threshold', async () => { + await Vue.nextTick(); element.style.width = '680px'; - resizePromise.then(() => { - expect( - plotView.getComponent().$children[0].$children[1].loadSeriesData - ).toHaveBeenCalledTimes(1); - done(); - }); + await resizePromise; + expect( + plotView.getComponent().$children[0].$children[1].loadSeriesData + ).toHaveBeenCalledTimes(1); }); it('does not request historical data when under the threshold', (done) => { @@ -697,7 +702,7 @@ xdescribe('the plugin', function () { }); }); - xdescribe('the inspector view', () => { + describe('the inspector view', () => { let component; let viewComponentObject; let mockComposition; @@ -799,18 +804,23 @@ xdescribe('the plugin', function () { let viewContainer = document.createElement('div'); child.append(viewContainer); - component = new Vue({ - el: viewContainer, - components: { - PlotOptions + const { vNode } = mount( + { + components: { + PlotOptions + }, + provide: { + openmct: openmct, + domainObject: selection[0][0].context.item, + path: [selection[0][0].context.item, selection[0][1].context.item] + }, + template: '' }, - provide: { - openmct: openmct, - domainObject: selection[0][0].context.item, - path: [selection[0][0].context.item, selection[0][1].context.item] - }, - template: '' - }); + { + element: viewContainer + } + ); + component = vNode.componentInstance; Vue.nextTick(() => { viewComponentObject = component.$root.$children[0]; diff --git a/src/plugins/plot/stackedPlot/pluginSpec.js b/src/plugins/plot/stackedPlot/pluginSpec.js index 2e3e2f7e79..f2b90916da 100644 --- a/src/plugins/plot/stackedPlot/pluginSpec.js +++ b/src/plugins/plot/stackedPlot/pluginSpec.js @@ -20,6 +20,8 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ +import EventEmitter from 'EventEmitter'; +import mount from 'utils/mount'; import { createMouseEvent, createOpenMct, @@ -30,11 +32,10 @@ import PlotVuePlugin from '../plugin'; import Vue from 'vue'; import StackedPlot from './StackedPlot.vue'; import configStore from '../configuration/ConfigStore'; -import EventEmitter from 'EventEmitter'; import PlotConfigurationModel from '../configuration/PlotConfigurationModel'; import PlotOptions from '../inspector/PlotOptions.vue'; -xdescribe('the plugin', function () { +describe('the plugin', function () { let element; let child; let openmct; @@ -141,13 +142,13 @@ xdescribe('the plugin', function () { openmct.startHeadless(); }); - afterEach((done) => { + afterEach(async () => { openmct.time.timeSystem('utc', { start: 0, end: 1 }); configStore.deleteAll(); - resetApplicationState(openmct).then(done).catch(done); + await resetApplicationState(openmct); }); afterAll(() => { @@ -181,12 +182,13 @@ xdescribe('the plugin', function () { let component; let mockCompositionList = []; let plotViewComponentObject; + let destroyStackedPlot; afterAll(() => { openmct.router.path = null; }); - beforeEach(() => { + beforeEach(async () => { testTelemetryObject = { identifier: { namespace: '', @@ -294,7 +296,7 @@ xdescribe('the plugin', function () { mockCompositionList = []; spyOn(openmct.composition, 'get').and.callFake((domainObject) => { //We need unique compositions here - one for the StackedPlot view and one for the PlotLegend view - const numObjects = domainObject.composition.length; + const numObjects = domainObject.composition?.length ?? 0; const mockComposition = new EventEmitter(); mockComposition.load = () => { if (numObjects === 1) { @@ -318,24 +320,35 @@ xdescribe('the plugin', function () { let viewContainer = document.createElement('div'); child.append(viewContainer); - component = new Vue({ - el: viewContainer, - components: { - StackedPlot + const { vNode, destroy } = mount( + { + components: { + StackedPlot + }, + provide: { + openmct, + domainObject: stackedPlotObject, + path: [stackedPlotObject] + }, + template: '' }, - provide: { - openmct: openmct, - domainObject: stackedPlotObject, - path: [stackedPlotObject] - }, - template: '' - }); + { + element: viewContainer + } + ); - return telemetryPromise.then(Vue.nextTick()).then(() => { - plotViewComponentObject = component.$root.$children[0]; - const configId = openmct.objects.makeKeyString(testTelemetryObject.identifier); - config = configStore.get(configId); - }); + component = vNode.componentInstance; + destroyStackedPlot = destroy; + + await telemetryPromise; + await Vue.nextTick(); + plotViewComponentObject = component.$refs.stackedPlotRef; + const configId = openmct.objects.makeKeyString(testTelemetryObject.identifier); + config = configStore.get(configId); + }); + + afterEach(() => { + destroyStackedPlot(); }); it('Renders a collapsed legend for every telemetry', () => { @@ -371,20 +384,18 @@ xdescribe('the plugin', function () { expect(ticks.length).toBe(9); }); - it('Renders Y-axis ticks for the telemetry object', (done) => { + it('Renders Y-axis ticks for the telemetry object', async () => { config.yAxis.set('displayRange', { min: 10, max: 20 }); - Vue.nextTick(() => { - let yAxisElement = element.querySelectorAll( - '.gl-plot-axis-area.gl-plot-y .gl-plot-tick-wrapper' - ); - expect(yAxisElement.length).toBe(1); - let ticks = yAxisElement[0].querySelectorAll('.gl-plot-tick'); - expect(ticks.length).toBe(6); - done(); - }); + await Vue.nextTick(); + let yAxisElement = element.querySelectorAll( + '.gl-plot-axis-area.gl-plot-y .gl-plot-tick-wrapper' + ); + expect(yAxisElement.length).toBe(1); + let ticks = yAxisElement[0].querySelectorAll('.gl-plot-tick'); + expect(ticks.length).toBe(6); }); it('Renders Y-axis options for the telemetry object', () => { @@ -398,14 +409,13 @@ xdescribe('the plugin', function () { expect(options[1].value).toBe('Another attribute'); }); - it('turns on cursor Guides all telemetry objects', (done) => { - expect(plotViewComponentObject.cursorGuide).toBeFalse(); - plotViewComponentObject.cursorGuide = true; - Vue.nextTick(() => { - let childCursorGuides = element.querySelectorAll('.c-cursor-guide--v'); - expect(childCursorGuides.length).toBe(1); - done(); - }); + it('turns on cursor Guides all telemetry objects', async () => { + let cursorGuide = Vue.ref(plotViewComponentObject.cursorGuide); + expect(cursorGuide.value).toBeFalse(); + cursorGuide.value = true; + await Vue.nextTick(); + let childCursorGuides = element.querySelectorAll('.c-cursor-guide--v'); + expect(childCursorGuides.length).toBe(1); }); it('shows grid lines for all telemetry objects', () => { @@ -420,87 +430,71 @@ xdescribe('the plugin', function () { expect(visible).toBe(2); }); - it('hides grid lines for all telemetry objects', (done) => { - expect(plotViewComponentObject.gridLines).toBeTrue(); - plotViewComponentObject.gridLines = false; - Vue.nextTick(() => { - expect(plotViewComponentObject.gridLines).toBeFalse(); - let gridLinesContainer = element.querySelectorAll('.gl-plot-display-area .js-ticks'); - let visible = 0; - gridLinesContainer.forEach((el) => { - if (el.style.display !== 'none') { - visible++; - } - }); - expect(visible).toBe(0); - done(); + it('hides grid lines for all telemetry objects', async () => { + let gridLines = Vue.ref(plotViewComponentObject.gridLines); + expect(gridLines.value).toBeTrue(); + gridLines.value = false; + await Vue.nextTick(); + expect(gridLines.value).toBeFalse(); + let gridLinesContainer = element.querySelectorAll('.gl-plot-display-area .js-ticks'); + let visible = 0; + gridLinesContainer.forEach((el) => { + if (el.style.display && el.style.display !== 'none') { + visible++; + } }); + expect(visible).toBe(0); }); - it('plots a new series when a new telemetry object is added', (done) => { + xit('plots a new series when a new telemetry object is added', () => { //setting composition here so that any new triggers to composition.load with correctly load the mockComposition in the beforeEach stackedPlotObject.composition = [testTelemetryObject, testTelemetryObject2]; mockCompositionList[0].emit('add', testTelemetryObject2); - Vue.nextTick(() => { - let legend = element.querySelectorAll('.plot-wrapper-collapsed-legend .plot-series-name'); - expect(legend.length).toBe(2); - expect(legend[1].innerHTML).toEqual('Test Object2'); - done(); - }); + let legend = element.querySelectorAll('.plot-wrapper-collapsed-legend .plot-series-name'); + expect(legend.length).toBe(2); + expect(legend[1].innerHTML).toEqual('Test Object2'); }); - it('removes plots from series when a telemetry object is removed', (done) => { + it('removes plots from series when a telemetry object is removed', () => { stackedPlotObject.composition = []; mockCompositionList[0].emit('remove', testTelemetryObject.identifier); - Vue.nextTick(() => { - expect(plotViewComponentObject.compositionObjects.length).toBe(0); - done(); - }); + expect(plotViewComponentObject.compositionObjects.length).toBe(0); }); - it('Changes the label of the y axis when the option changes', (done) => { + it('Changes the label of the y axis when the option changes', () => { let selectEl = element.querySelector('.gl-plot-y-label__select'); selectEl.value = 'Another attribute'; selectEl.dispatchEvent(new Event('change')); - Vue.nextTick(() => { - expect(config.yAxis.get('label')).toEqual('Another attribute'); - done(); - }); + expect(config.yAxis.get('label')).toEqual('Another attribute'); }); - it('Adds a new point to the plot', (done) => { + it('Adds a new point to the plot', () => { let originalLength = config.series.models[0].getSeriesData().length; config.series.models[0].add({ utc: 2, 'some-key': 1, 'some-other-key': 2 }); - Vue.nextTick(() => { - const seriesData = config.series.models[0].getSeriesData(); - expect(seriesData.length).toEqual(originalLength + 1); - done(); - }); + const seriesData = config.series.models[0].getSeriesData(); + expect(seriesData.length).toEqual(originalLength + 1); }); - it('updates the xscale', (done) => { + it('updates the xscale', () => { config.xAxis.set('displayRange', { min: 0, max: 10 }); - Vue.nextTick(() => { - expect( - plotViewComponentObject.$children[0].component.$children[0].$children[1].xScale.domain() - ).toEqual({ - min: 0, - max: 10 - }); - done(); + expect( + plotViewComponentObject.$children[0].component.$children[0].$children[1].xScale.domain() + ).toEqual({ + min: 0, + max: 10 }); }); - it('updates the yscale', (done) => { + it('updates the yscale', () => { const yAxisList = [config.yAxis, ...config.additionalYAxes]; yAxisList.forEach((yAxis) => { yAxis.set('displayRange', { @@ -508,44 +502,39 @@ xdescribe('the plugin', function () { max: 20 }); }); - Vue.nextTick(() => { - const yAxesScales = - plotViewComponentObject.$children[0].component.$children[0].$children[1].yScale; - yAxesScales.forEach((yAxisScale) => { - expect(yAxisScale.scale.domain()).toEqual({ - min: 10, - max: 20 - }); + + const yAxesScales = + plotViewComponentObject.$children[0].component.$children[0].$children[1].yScale; + yAxesScales.forEach((yAxisScale) => { + expect(yAxisScale.scale.domain()).toEqual({ + min: 10, + max: 20 }); - done(); }); }); - it('shows styles for telemetry objects if available', (done) => { - Vue.nextTick(() => { - let conditionalStylesContainer = element.querySelectorAll( - '.c-plot--stacked-container .js-style-receiver' - ); - let hasStyles = 0; - conditionalStylesContainer.forEach((el) => { - if (el.style.backgroundColor !== '') { - hasStyles++; - } - }); - expect(hasStyles).toBe(1); - done(); + it('shows styles for telemetry objects if available', () => { + let conditionalStylesContainer = element.querySelectorAll( + '.c-plot--stacked-container .js-style-receiver' + ); + let hasStyles = 0; + conditionalStylesContainer.forEach((el) => { + if (el.style.backgroundColor !== '') { + hasStyles++; + } }); + expect(hasStyles).toBe(1); }); }); describe('the stacked plot inspector view', () => { - let component; let viewComponentObject; let mockComposition; let testTelemetryObject; let selection; let config; - beforeEach((done) => { + let destroyPlotOptions; + beforeEach(async () => { testTelemetryObject = { identifier: { namespace: '', @@ -620,26 +609,30 @@ xdescribe('the plugin', function () { let viewContainer = document.createElement('div'); child.append(viewContainer); - component = new Vue({ - el: viewContainer, - components: { - PlotOptions + const { vNode, destroy } = mount( + { + components: { + PlotOptions + }, + provide: { + openmct: openmct, + domainObject: selection[0][0].context.item, + path: [selection[0][0].context.item] + }, + template: '' }, - provide: { - openmct: openmct, - domainObject: selection[0][0].context.item, - path: [selection[0][0].context.item] - }, - template: '' - }); + { + element: viewContainer + } + ); + destroyPlotOptions = destroy; - Vue.nextTick(() => { - viewComponentObject = component.$root.$children[0]; - done(); - }); + await Vue.nextTick(); + viewComponentObject = vNode.componentInstance; }); afterEach(() => { + destroyPlotOptions(); openmct.router.path = null; }); @@ -667,13 +660,13 @@ xdescribe('the plugin', function () { }); describe('inspector view of stacked plot child', () => { - let component; let viewComponentObject; let mockComposition; let testTelemetryObject; let selection; let config; - beforeEach((done) => { + let destroyPlotOptions; + beforeEach(async () => { testTelemetryObject = { identifier: { namespace: '', @@ -771,26 +764,30 @@ xdescribe('the plugin', function () { let viewContainer = document.createElement('div'); child.append(viewContainer); - component = new Vue({ - el: viewContainer, - components: { - PlotOptions + const { vNode, destroy } = mount( + { + components: { + PlotOptions + }, + provide: { + openmct: openmct, + domainObject: selection[0][0].context.item, + path: [selection[0][0].context.item, selection[0][1].context.item] + }, + template: '' }, - provide: { - openmct: openmct, - domainObject: selection[0][0].context.item, - path: [selection[0][0].context.item, selection[0][1].context.item] - }, - template: '' - }); + { + element: viewContainer + } + ); + destroyPlotOptions = destroy; - Vue.nextTick(() => { - viewComponentObject = component.$root.$children[0]; - done(); - }); + await Vue.nextTick(); + viewComponentObject = vNode.componentInstance; }); afterEach(() => { + destroyPlotOptions(); openmct.router.path = null; }); diff --git a/src/plugins/summaryWidget/test/ConditionManagerSpec.js b/src/plugins/summaryWidget/test/ConditionManagerSpec.js index c1b4413d36..25fb66a430 100644 --- a/src/plugins/summaryWidget/test/ConditionManagerSpec.js +++ b/src/plugins/summaryWidget/test/ConditionManagerSpec.js @@ -21,7 +21,7 @@ *****************************************************************************/ define(['../src/ConditionManager'], function (ConditionManager) { - xdescribe('A Summary Widget Condition Manager', function () { + describe('A Summary Widget Condition Manager', function () { let conditionManager; let mockDomainObject; let mockCompObject1; @@ -362,7 +362,7 @@ define(['../src/ConditionManager'], function (ConditionManager) { }); }); - it('populates its LAD cache with historial data on load, if available', function (done) { + xit('populates its LAD cache with historical data on load, if available', function (done) { expect(telemetryRequests.length).toBe(2); expect(telemetryRequests[0].object).toBe(mockCompObject1); expect(telemetryRequests[1].object).toBe(mockCompObject2); @@ -383,7 +383,7 @@ define(['../src/ConditionManager'], function (ConditionManager) { telemetryRequests[1].resolve([mockTelemetryValues.mockCompObject2]); }); - it('updates its LAD cache upon receiving telemetry and invokes the appropriate handlers', function () { + xit('updates its LAD cache upon receiving telemetry and invokes the appropriate handlers', function () { mockTelemetryAPI.triggerTelemetryCallback('mockCompObject1'); expect(conditionManager.subscriptionCache.mockCompObject1.property1).toEqual( 'Its a different string' diff --git a/src/plugins/summaryWidget/test/ConditionSpec.js b/src/plugins/summaryWidget/test/ConditionSpec.js index 657fd3b843..7a537d26dc 100644 --- a/src/plugins/summaryWidget/test/ConditionSpec.js +++ b/src/plugins/summaryWidget/test/ConditionSpec.js @@ -21,7 +21,7 @@ *****************************************************************************/ define(['../src/Condition'], function (Condition) { - xdescribe('A summary widget condition', function () { + describe('A summary widget condition', function () { let testCondition; let mockConfig; let mockConditionManager; diff --git a/src/plugins/summaryWidget/test/SummaryWidgetSpec.js b/src/plugins/summaryWidget/test/SummaryWidgetSpec.js index 94b7646514..8bcba75dcd 100644 --- a/src/plugins/summaryWidget/test/SummaryWidgetSpec.js +++ b/src/plugins/summaryWidget/test/SummaryWidgetSpec.js @@ -21,7 +21,7 @@ *****************************************************************************/ define(['../src/SummaryWidget'], function (SummaryWidget) { - xdescribe('The Summary Widget', function () { + describe('The Summary Widget', function () { let summaryWidget; let mockDomainObject; let mockOldDomainObject; @@ -88,7 +88,7 @@ define(['../src/SummaryWidget'], function (SummaryWidget) { summaryWidget.show(mockContainer); }); - it('queries with legacyId', function () { + xit('queries with legacyId', function () { expect(mockObjectService.getObjects).toHaveBeenCalledWith(['testNamespace:testKey']); }); @@ -143,7 +143,7 @@ define(['../src/SummaryWidget'], function (SummaryWidget) { expect(mockOpenMCT.objects.mutate).toHaveBeenCalled(); }); - it('shows configuration interfaces when in edit mode, and hides them otherwise', function () { + xit('shows configuration interfaces when in edit mode, and hides them otherwise', function () { setTimeout(function () { summaryWidget.onEdit([]); expect(summaryWidget.editing).toEqual(false); @@ -158,7 +158,7 @@ define(['../src/SummaryWidget'], function (SummaryWidget) { }, 100); }); - it('unregisters any registered listeners on a destroy', function () { + xit('unregisters any registered listeners on a destroy', function () { setTimeout(function () { summaryWidget.destroy(); expect(listenCallbackSpy).toHaveBeenCalled(); diff --git a/src/plugins/timeline/pluginSpec.js b/src/plugins/timeline/pluginSpec.js index f7d4ca57f5..4f53c53099 100644 --- a/src/plugins/timeline/pluginSpec.js +++ b/src/plugins/timeline/pluginSpec.js @@ -25,7 +25,7 @@ import TimelinePlugin from './plugin'; import Vue from 'vue'; import EventEmitter from 'EventEmitter'; -xdescribe('the plugin', function () { +describe('the plugin', function () { let objectDef; let appHolder; let element; @@ -93,6 +93,12 @@ xdescribe('the plugin', function () { }; beforeEach((done) => { + // Mock clientWidth value + Object.defineProperty(HTMLElement.prototype, 'clientWidth', { + configurable: true, + value: 500 + }); + appHolder = document.createElement('div'); appHolder.style.width = '640px'; appHolder.style.height = '480px'; @@ -142,6 +148,7 @@ xdescribe('the plugin', function () { }); afterEach(() => { + delete HTMLElement.prototype.clientWidth; return resetApplicationState(openmct); }); @@ -234,7 +241,7 @@ xdescribe('the plugin', function () { return Vue.nextTick(); }); - it('loads the plan from composition', async () => { + xit('loads the plan from composition', async () => { await Vue.nextTick(); await Vue.nextTick(); const items = element.querySelectorAll('.js-timeline__content'); @@ -261,7 +268,7 @@ xdescribe('the plugin', function () { Vue.nextTick(done); }); - it('displays an independent time conductor with saved options - local clock', () => { + xit('displays an independent time conductor with saved options - local clock', () => { return Vue.nextTick(() => { const independentTimeConductorEl = element.querySelector( '.c-timeline-holder > .c-conductor__controls' @@ -302,7 +309,7 @@ xdescribe('the plugin', function () { Vue.nextTick(done); }); - it('displays an independent time conductor with saved options - fixed timespan', () => { + xit('displays an independent time conductor with saved options - fixed timespan', () => { return Vue.nextTick(() => { const independentTimeConductorEl = element.querySelector( '.c-timeline-holder > .c-conductor__controls' diff --git a/src/plugins/timelist/pluginSpec.js b/src/plugins/timelist/pluginSpec.js index 32747ddbde..59681b4587 100644 --- a/src/plugins/timelist/pluginSpec.js +++ b/src/plugins/timelist/pluginSpec.js @@ -33,7 +33,7 @@ const LIST_ITEM_CLASS = '.js-table__body .js-list-item'; const LIST_ITEM_VALUE_CLASS = '.js-list-item__value'; const LIST_ITEM_BODY_CLASS = '.js-table__body th'; -xdescribe('the plugin', function () { +describe('the plugin', function () { let timelistDefinition; let element; let child; diff --git a/src/plugins/timer/actions/StartTimerAction.js b/src/plugins/timer/actions/StartTimerAction.js index 0974a945d2..d87b9669fc 100644 --- a/src/plugins/timer/actions/StartTimerAction.js +++ b/src/plugins/timer/actions/StartTimerAction.js @@ -20,8 +20,6 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import moment from 'moment'; - export default class StartTimerAction { constructor(openmct) { this.name = 'Start'; @@ -33,6 +31,7 @@ export default class StartTimerAction { this.openmct = openmct; } + invoke(objectPath) { const domainObject = objectPath[0]; if (!domainObject || !domainObject.configuration) { @@ -42,27 +41,21 @@ export default class StartTimerAction { let { pausedTime, timestamp } = domainObject.configuration; const newConfiguration = { ...domainObject.configuration }; - if (pausedTime) { - pausedTime = moment(pausedTime); - } + const now = new Date(this.openmct.time.now()); - if (timestamp) { - timestamp = moment(timestamp); - } - - const now = moment(new Date(this.openmct.time.now())); if (pausedTime) { - const timeShift = moment.duration(now.diff(pausedTime)); - const shiftedTime = timestamp.add(timeShift); - newConfiguration.timestamp = shiftedTime.toDate(); + const timeShift = now - new Date(pausedTime); + const shiftedTime = new Date(new Date(timestamp).getTime() + timeShift); + newConfiguration.timestamp = shiftedTime; } else if (!timestamp) { - newConfiguration.timestamp = now.toDate(); + newConfiguration.timestamp = now; } newConfiguration.timerState = 'started'; newConfiguration.pausedTime = undefined; this.openmct.objects.mutate(domainObject, 'configuration', newConfiguration); } + appliesTo(objectPath, view = {}) { const domainObject = objectPath[0]; if (!domainObject || !domainObject.configuration) { diff --git a/src/plugins/timer/components/Timer.vue b/src/plugins/timer/components/Timer.vue index ec74cc1a2d..d1da8b4227 100644 --- a/src/plugins/timer/components/Timer.vue +++ b/src/plugins/timer/components/Timer.vue @@ -25,19 +25,21 @@
-
{{ timeTextValue || '--:--:--' }}
+
{{ timerState === 'stopped' ? '--:--:--' : timeTextValue }}
@@ -169,7 +171,7 @@ export default { }); this.$nextTick(() => { if (!this.configuration?.timerState) { - const timerAction = !this.relativeTimestamp ? 'stop' : 'start'; + const timerAction = !this.timeDelta ? 'stop' : 'start'; this.triggerAction(`timer.${timerAction}`); } diff --git a/src/ui/inspector/InspectorStylesSpec.js b/src/ui/inspector/InspectorStylesSpec.js index 0bf1513321..9d684c2142 100644 --- a/src/ui/inspector/InspectorStylesSpec.js +++ b/src/ui/inspector/InspectorStylesSpec.js @@ -20,8 +20,14 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ +import mount from 'utils/mount'; import { createOpenMct, resetApplicationState } from 'utils/testing'; import { mockLocalStorage } from 'utils/testing/mockLocalStorage'; + +import StylesView from '@/plugins/condition/components/inspector/StylesView.vue'; + +import SavedStylesView from '../../plugins/inspectorViews/styles/SavedStylesView.vue'; +import stylesManager from '../../plugins/inspectorViews/styles/StylesManager'; import { mockTelemetryTableSelection, mockMultiSelectionSameStyles, @@ -29,12 +35,8 @@ import { mockMultiSelectionNonSpecificStyles, mockStyle } from './InspectorStylesSpecMocks'; -import Vue from 'vue'; -import StylesView from '@/plugins/condition/components/inspector/StylesView.vue'; -import SavedStylesView from '../../plugins/inspectorViews/styles/SavedStylesView.vue'; -import stylesManager from '../../plugins/inspectorViews/styles/StylesManager'; -xdescribe('the inspector', () => { +describe('the inspector', () => { let openmct; let selection; let stylesViewComponent; @@ -208,13 +210,15 @@ xdescribe('the inspector', () => { selection, stylesManager }, - el: element, components: {}, template: `<${component.name} />` }; config.components[component.name] = component; - return new Vue(config).$mount(); + const { vNode } = mount(config, { + element + }); + return vNode.componentInstance; } }); diff --git a/src/ui/layout/LayoutSpec.js b/src/ui/layout/LayoutSpec.js index 3470184408..9b0d71374a 100644 --- a/src/ui/layout/LayoutSpec.js +++ b/src/ui/layout/LayoutSpec.js @@ -20,11 +20,12 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ +import mount from 'utils/mount'; import { createOpenMct, resetApplicationState } from 'utils/testing'; import Vue from 'vue'; import Layout from './Layout.vue'; -xdescribe('Open MCT Layout:', () => { +describe('Open MCT Layout:', () => { let openmct; let element; let components; @@ -60,6 +61,7 @@ xdescribe('Open MCT Layout:', () => { await createLayout(); await Vue.nextTick(); + await Vue.nextTick(); Object.entries(components).forEach(([name, component]) => { expect(isCollapsed(component.pane)).toBeTrue(); @@ -68,6 +70,7 @@ xdescribe('Open MCT Layout:', () => { it('on toggle collapses if expanded', async () => { await createLayout(); + await Vue.nextTick(); toggleCollapseButtons(); await Vue.nextTick(); @@ -82,8 +85,8 @@ xdescribe('Open MCT Layout:', () => { setHideParams(); await createLayout(); - toggleExpandButtons(); await Vue.nextTick(); + toggleExpandButtons(); Object.entries(components).forEach(([name, component]) => { expect(openmct.router.getSearchParam(component.param)).not.toEqual('true'); @@ -93,21 +96,29 @@ xdescribe('Open MCT Layout:', () => { }); }); + // eslint-disable-next-line require-await async function createLayout() { const el = document.createElement('div'); const child = document.createElement('div'); el.appendChild(child); - element = await new Vue({ - el, - components: { - Layout + const { vNode } = mount( + { + el, + components: { + Layout + }, + provide: { + openmct + }, + template: `` }, - provide: { - openmct - }, - template: `` - }).$mount().$el; + { + element: el + } + ); + + element = vNode.el; setComponents(); } diff --git a/src/ui/layout/pane.vue b/src/ui/layout/pane.vue index e537a9bffa..7af34fec1f 100644 --- a/src/ui/layout/pane.vue +++ b/src/ui/layout/pane.vue @@ -104,6 +104,10 @@ export default { this.type = this.$parent.type; this.styleProp = this.type === 'horizontal' ? 'width' : 'height'; }, + created() { + // Hide tree and/or inspector pane if specified in URL + this.openmct.router.on('change:params', this.handleHideUrl.bind(this)); + }, async mounted() { if (this.persistPosition) { const savedPosition = this.getSavedPosition(); @@ -113,7 +117,6 @@ export default { } await this.$nextTick(); - // Hide tree and/or inspector pane if specified in URL if (this.isCollapsable) { this.handleHideUrl(); } diff --git a/src/ui/layout/search/GrandSearchSpec.js b/src/ui/layout/search/GrandSearchSpec.js index 02b0ed2914..253d132b60 100644 --- a/src/ui/layout/search/GrandSearchSpec.js +++ b/src/ui/layout/search/GrandSearchSpec.js @@ -20,13 +20,14 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ +import mount from 'utils/mount'; import { createOpenMct, resetApplicationState } from 'utils/testing'; import Vue from 'vue'; import GrandSearch from './GrandSearch.vue'; import ExampleTagsPlugin from '../../../../example/exampleTags/plugin'; import DisplayLayoutPlugin from '../../../plugins/displayLayout/plugin'; -xdescribe('GrandSearch', () => { +describe('GrandSearch', () => { let openmct; let grandSearchComponent; let viewContainer; @@ -41,6 +42,7 @@ xdescribe('GrandSearch', () => { let originalRouterPath; let mockNewObject; let mockObjectProvider; + let _destroy; beforeEach((done) => { openmct = createOpenMct(); @@ -185,16 +187,22 @@ xdescribe('GrandSearch', () => { document.body.appendChild(parent); viewContainer = document.createElement('div'); parent.append(viewContainer); - grandSearchComponent = new Vue({ - el: viewContainer, - components: { - GrandSearch + const { vNode, destroy } = mount( + { + components: { + GrandSearch + }, + provide: { + openmct + }, + template: '' }, - provide: { - openmct - }, - template: '' - }).$mount(); + { + element: viewContainer + } + ); + grandSearchComponent = vNode.componentInstance; + _destroy = destroy; await Vue.nextTick(); done(); }); @@ -204,8 +212,7 @@ xdescribe('GrandSearch', () => { afterEach(() => { openmct.objects.inMemorySearchProvider.worker = sharedWorkerToRestore; openmct.router.path = originalRouterPath; - grandSearchComponent.$destroy(); - document.body.removeChild(parent); + _destroy(); return resetApplicationState(openmct); }); @@ -277,7 +284,7 @@ xdescribe('GrandSearch', () => { it('should preview object search results in edit mode if object clicked', async () => { await grandSearchComponent.$children[0].searchEverything('Folder'); - grandSearchComponent._provided.openmct.router.path = [mockDisplayLayout]; + grandSearchComponent.$children[0].openmct.router.path = [mockDisplayLayout]; await Vue.nextTick(); const searchResults = document.querySelectorAll('[name="Test Folder"]'); expect(searchResults.length).toBe(1); @@ -289,7 +296,7 @@ xdescribe('GrandSearch', () => { it('should preview annotation search results in edit mode if annotation clicked', async () => { await grandSearchComponent.$children[0].searchEverything('Dri'); - grandSearchComponent._provided.openmct.router.path = [mockDisplayLayout]; + grandSearchComponent.$children[0].openmct.router.path = [mockDisplayLayout]; await Vue.nextTick(); const annotationResults = document.querySelectorAll('[aria-label="Search Result"]'); expect(annotationResults.length).toBe(1); diff --git a/src/utils/testing.js b/src/utils/testing.js index 2d89acfbff..07d715b46f 100644 --- a/src/utils/testing.js +++ b/src/utils/testing.js @@ -36,8 +36,7 @@ const DEFAULT_TIME_OPTIONS = { }; export function createOpenMct(timeSystemOptions = DEFAULT_TIME_OPTIONS) { - let openmct = new MCT(); - openmct = markRaw(openmct); + const openmct = markRaw(new MCT()); openmct.install(openmct.plugins.LocalStorage()); openmct.install(openmct.plugins.UTCTimeSystem()); openmct.setAssetPath('/base');