diff --git a/e2e/fixtures.js b/e2e/fixtures.js index aef1399af5..d602857d1f 100644 --- a/e2e/fixtures.js +++ b/e2e/fixtures.js @@ -4,15 +4,29 @@ const base = require('@playwright/test'); const { expect } = require('@playwright/test'); +/** + * Takes a `ConsoleMessage` and returns a formatted string + * @param {import('@playwright/test').ConsoleMessage} msg + * @returns {String} formatted string with message type, text, url, and line and column numbers + */ +function consoleMessageToString(msg) { + const { url, lineNumber, columnNumber } = msg.location(); + + return `[${msg.type()}] ${msg.text()} + at (${url} ${lineNumber}:${columnNumber})`; +} + exports.test = base.test.extend({ page: async ({ baseURL, page }, use) => { const messages = []; - page.on('console', msg => messages.push(`[${msg.type()}] ${msg.text()}`)); + page.on('console', (msg) => messages.push(msg)); await use(page); - await expect.soft(messages.toString()).not.toContain('[error]'); + messages.forEach( + msg => expect.soft(msg.type(), `Console error detected: ${consoleMessageToString(msg)}`).not.toEqual('error') + ); }, browser: async ({ playwright, browser }, use, workerInfo) => { - // Use browserless if configured + // Use browserless if configured if (workerInfo.project.name.match(/browserless/)) { const vBrowser = await playwright.chromium.connectOverCDP({ endpointURL: 'ws://localhost:3003' diff --git a/e2e/tests/recycled_storage.json b/e2e/tests/recycled_storage.json index c20b7ee73d..53c7695977 100644 --- a/e2e/tests/recycled_storage.json +++ b/e2e/tests/recycled_storage.json @@ -19,4 +19,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/package.json b/package.json index 4a91bc8ac5..09c56a5c5d 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,7 @@ "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:debug": "cross-env NODE_ENV=debug karma start --no-single-run", - "test:e2e:ci": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome smoke branding default condition timeConductor clock exampleImagery notebook 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", "test:e2e:local": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome", "test:e2e:updatesnapshots": "npx playwright test --config=e2e/playwright-local.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", diff --git a/src/plugins/displayLayout/components/layout-frame.scss b/src/plugins/displayLayout/components/layout-frame.scss index d036814021..656a535c8f 100644 --- a/src/plugins/displayLayout/components/layout-frame.scss +++ b/src/plugins/displayLayout/components/layout-frame.scss @@ -28,7 +28,6 @@ &[s-selected] { // All frames selected while editing - border: $editFrameSelectedBorder; box-shadow: $editFrameSelectedShdw; .c-frame__move-bar { diff --git a/src/plugins/displayLayout/pluginSpec.js b/src/plugins/displayLayout/pluginSpec.js index da0fa312ab..8e6a56e5f2 100644 --- a/src/plugins/displayLayout/pluginSpec.js +++ b/src/plugins/displayLayout/pluginSpec.js @@ -41,7 +41,7 @@ describe('the plugin', function () { element.appendChild(child); openmct.on('start', done); - openmct.startHeadless(); + openmct.start(child); }); afterEach(() => { diff --git a/src/plugins/faultManagement/FaultManagementListItem.vue b/src/plugins/faultManagement/FaultManagementListItem.vue index 977d5d6634..1acb39a9cb 100644 --- a/src/plugins/faultManagement/FaultManagementListItem.vue +++ b/src/plugins/faultManagement/FaultManagementListItem.vue @@ -40,7 +40,7 @@ class="c-fault-mgmt__list-severity" :title="fault.severity" :class="[ - 'is-severity-' + fault.severity + 'is-severity-' + severity ]" > diff --git a/src/plugins/flexibleLayout/components/flexible-layout.scss b/src/plugins/flexibleLayout/components/flexible-layout.scss index ac44eb3d3c..6fe96a446b 100644 --- a/src/plugins/flexibleLayout/components/flexible-layout.scss +++ b/src/plugins/flexibleLayout/components/flexible-layout.scss @@ -141,6 +141,10 @@ } } } + + [s-selected].c-fl-frame__drag-wrapper { + border: $editFrameSelectedBorder; + } } /****** THEIR FRAMES */ diff --git a/src/plugins/flexibleLayout/pluginSpec.js b/src/plugins/flexibleLayout/pluginSpec.js index c88c7ffea6..470fb6e8b3 100644 --- a/src/plugins/flexibleLayout/pluginSpec.js +++ b/src/plugins/flexibleLayout/pluginSpec.js @@ -22,6 +22,7 @@ import { createOpenMct, resetApplicationState } from 'utils/testing'; import FlexibleLayout from './plugin'; +import Vue from 'vue'; describe('the plugin', function () { let element; @@ -61,7 +62,7 @@ describe('the plugin', function () { element.appendChild(child); openmct.on('start', done); - openmct.startHeadless(); + openmct.start(child); }); afterEach(() => { @@ -83,6 +84,16 @@ describe('the plugin', function () { it('provides a view', () => { expect(flexibleLayoutViewProvider).toBeDefined(); }); + + it('renders a view', async () => { + const flexibleView = flexibleLayoutViewProvider.view(testViewObject, []); + flexibleView.show(child, false); + + await Vue.nextTick(); + const flexTitle = child.querySelector('.l-browse-bar .c-object-label__name'); + + expect(flexTitle).not.toBeNull(); + }); }); describe('the toolbar', () => { diff --git a/src/plugins/imagery/components/ImageControls.vue b/src/plugins/imagery/components/ImageControls.vue index b14c13f8b2..eae50f6b49 100644 --- a/src/plugins/imagery/components/ImageControls.vue +++ b/src/plugins/imagery/components/ImageControls.vue @@ -173,7 +173,7 @@ export default { this.$emit('filtersUpdated', this.filters); }, handleResetFilters() { - this.filters = DEFAULT_FILTER_VALUES; + this.filters = {...DEFAULT_FILTER_VALUES}; this.notifyFiltersChanged(); }, limitZoomRange(factor) { diff --git a/src/plugins/imagery/components/ImageryView.vue b/src/plugins/imagery/components/ImageryView.vue index 8f011cf8ef..66d53669b3 100644 --- a/src/plugins/imagery/components/ImageryView.vue +++ b/src/plugins/imagery/components/ImageryView.vue @@ -403,6 +403,9 @@ export default { formattedDuration() { let result = 'N/A'; let negativeAge = -1; + if (!Number.isInteger(this.numericDuration)) { + return result; + } if (this.numericDuration > TWENTYFOUR_HOURS) { negativeAge *= (this.numericDuration / TWENTYFOUR_HOURS); @@ -905,8 +908,10 @@ export default { let currentTime = this.timeContext.clock() && this.timeContext.clock().currentValue(); if (currentTime === undefined) { this.numericDuration = currentTime; - } else { + } else if (Number.isInteger(this.parsedSelectedTime)) { this.numericDuration = currentTime - this.parsedSelectedTime; + } else { + this.numericDuration = undefined; } }, resetAgeCSS() { diff --git a/src/plugins/imagery/components/imagery-view.scss b/src/plugins/imagery/components/imagery-view.scss index e781fdce9a..50895d7e32 100644 --- a/src/plugins/imagery/components/imagery-view.scss +++ b/src/plugins/imagery/components/imagery-view.scss @@ -68,15 +68,24 @@ overflow: hidden; } &__background-image { + // Actually does the image display background-position: center; background-repeat: no-repeat; background-size: contain; + height: 100%; //fallback value } &__image { + // Present to allow Save As... image + position: absolute; height: 100%; width: 100%; - visibility: hidden; - display: contents; + opacity: 0; + } + + &__image-save-proxy { + height: 100%; + width: 100%; + z-index: 10; } } diff --git a/src/plugins/imagery/mixins/imageryData.js b/src/plugins/imagery/mixins/imageryData.js index 64137d46d9..c5251d1218 100644 --- a/src/plugins/imagery/mixins/imageryData.js +++ b/src/plugins/imagery/mixins/imageryData.js @@ -70,22 +70,18 @@ export default { this.timeContext.off('timeSystem', this.timeSystemChange); } }, - datumIsNotValid(datum) { - if (this.imageHistory.length === 0) { + isDatumValid(datum) { + //TODO: Add a check to see if there are duplicate images (identical image timestamp and url subsequently) + if (!datum) { return false; } - const datumURL = this.formatImageUrl(datum); - const lastHistoryURL = this.formatImageUrl(this.imageHistory.slice(-1)[0]); - - // datum is not valid if it matches the last datum in history, - // or it is before the last datum in the history const datumTimeCheck = this.parseTime(datum); - const historyTimeCheck = this.parseTime(this.imageHistory.slice(-1)[0]); - const matchesLast = (datumTimeCheck === historyTimeCheck) && (datumURL === lastHistoryURL); - const isStale = datumTimeCheck < historyTimeCheck; + const bounds = this.timeContext.bounds(); - return matchesLast || isStale; + const isOutOfBounds = datumTimeCheck < bounds.start || datumTimeCheck > bounds.end; + + return !isOutOfBounds; }, formatImageUrl(datum) { if (!datum) { @@ -132,25 +128,19 @@ export default { return this.requestHistory(); }, async requestHistory() { - let bounds = this.timeContext.bounds(); this.requestCount++; const requestId = this.requestCount; - this.imageHistory = []; + const bounds = this.timeContext.bounds(); - let data = await this.openmct.telemetry + const data = await this.openmct.telemetry .request(this.domainObject, bounds) || []; - - if (this.requestCount === requestId) { - let imagery = []; - data.forEach((datum) => { - let image = this.normalizeDatum(datum); - if (image) { - imagery.push(image); - } - }); - //this is to optimize anything that reacts to imageHistory length - this.imageHistory = imagery; + // wait until new request resolves to do comparison + if (this.requestCount !== requestId) { + return this.imageHistory = []; } + + const imagery = data.filter(this.isDatumValid).map(this.normalizeDatum); + this.imageHistory = imagery; }, clearData(domainObjectToClear) { // global clearData button is accepted therefore no truthy check on inputted param @@ -180,27 +170,29 @@ export default { .subscribe(this.domainObject, (datum) => { let parsedTimestamp = this.parseTime(datum); let bounds = this.timeContext.bounds(); + if (!(parsedTimestamp >= bounds.start && parsedTimestamp <= bounds.end)) { + return; + } - if (parsedTimestamp >= bounds.start && parsedTimestamp <= bounds.end) { - let image = this.normalizeDatum(datum); - if (image) { - this.imageHistory.push(image); - } + if (this.isDatumValid(datum)) { + this.imageHistory.push(this.normalizeDatum(datum)); } }); }, normalizeDatum(datum) { - if (this.datumIsNotValid(datum)) { - return; - } - let image = { ...datum }; - image.formattedTime = this.formatTime(datum); - image.url = this.formatImageUrl(datum); - image.time = this.parseTime(image.formattedTime); - image.imageDownloadName = this.getImageDownloadName(datum); + const formattedTime = this.formatTime(datum); + const url = this.formatImageUrl(datum); + const time = this.parseTime(formattedTime); + const imageDownloadName = this.getImageDownloadName(datum); - return image; + return { + ...datum, + formattedTime, + url, + time, + imageDownloadName + }; }, getFormatter(key) { let metadataValue = this.metadata.value(key) || { format: key }; diff --git a/src/plugins/imagery/pluginSpec.js b/src/plugins/imagery/pluginSpec.js index aa2a284d43..44167b3b0a 100644 --- a/src/plugins/imagery/pluginSpec.js +++ b/src/plugins/imagery/pluginSpec.js @@ -84,7 +84,6 @@ describe("The Imagery View Layouts", () => { let telemetryPromise; let telemetryPromiseResolve; let cleanupFirst; - let isClearDataTriggered; let openmct; let parent; @@ -205,20 +204,12 @@ describe("The Imagery View Layouts", () => { cleanupFirst = []; openmct = createOpenMct(); - openmct.time.timeSystem('utc', { - start: START - (5 * ONE_MINUTE), - end: START + (5 * ONE_MINUTE) - }); telemetryPromise = new Promise((resolve) => { telemetryPromiseResolve = resolve; }); spyOn(openmct.telemetry, 'request').and.callFake(() => { - if (isClearDataTriggered) { - return []; - } - telemetryPromiseResolve(imageTelemetry); return telemetryPromise; @@ -337,44 +328,93 @@ describe("The Imagery View Layouts", () => { expect(imageryView).toBeDefined(); }); - describe("imagery view", () => { + describe("Clear data action for imagery", () => { let applicableViews; let imageryViewProvider; let imageryView; + let componentView; let clearDataPlugin; let clearDataAction; beforeEach(() => { + openmct.time.timeSystem('utc', { + start: START - (5 * ONE_MINUTE), + end: START + (5 * ONE_MINUTE) + }); applicableViews = openmct.objectViews.get(imageryObject, [imageryObject]); imageryViewProvider = applicableViews.find(viewProvider => viewProvider.key === imageryKey); imageryView = imageryViewProvider.view(imageryObject, [imageryObject]); imageryView.show(child); + componentView = imageryView._getInstance().$children[0]; + clearDataPlugin = new ClearDataPlugin( ['example.imagery'], {indicator: true} ); openmct.install(clearDataPlugin); clearDataAction = openmct.actions.getAction('clear-data-action'); + + return Vue.nextTick(); + }); + + it('clear data action is installed', () => { + expect(clearDataAction).toBeDefined(); + }); + + it('on clearData action should clear data for object is selected', (done) => { // force show the thumbnails + componentView.forceShowThumbnails = true; + Vue.nextTick(() => { + let clearDataResolve; + let telemetryRequestPromise = new Promise((resolve) => { + clearDataResolve = resolve; + }); + expect(parent.querySelectorAll('.c-imagery__thumb').length).not.toBe(0); + + openmct.objectViews.on('clearData', (_domainObject) => { + return Vue.nextTick(() => { + expect(parent.querySelectorAll('.c-imagery__thumb').length).toBe(0); + + clearDataResolve(); + }); + }); + clearDataAction.invoke(imageryObject); + + telemetryRequestPromise.then(() => { + done(); + }); + }); + }); + }); + + describe("imagery view", () => { + let applicableViews; + let imageryViewProvider; + let imageryView; + + beforeEach(() => { + openmct.time.timeSystem('utc', { + start: START - (5 * ONE_MINUTE), + end: START + (5 * ONE_MINUTE) + }); + + applicableViews = openmct.objectViews.get(imageryObject, [imageryObject]); + imageryViewProvider = applicableViews.find(viewProvider => viewProvider.key === imageryKey); + imageryView = imageryViewProvider.view(imageryObject, [imageryObject]); + imageryView.show(child); + imageryView._getInstance().$children[0].forceShowThumbnails = true; return Vue.nextTick(); }); - afterEach(() => { - isClearDataTriggered = false; - // openmct.time.stopClock(); - // openmct.router.removeListener('change:hash', resolveFunction); - // imageryView.destroy(); - }); - it("on mount should show the the most recent image", (done) => { + it("on mount should show the the most recent image", () => { //Looks like we need Vue.nextTick here so that computed properties settle down - Vue.nextTick(() => { + return Vue.nextTick(() => { const imageInfo = getImageInfo(parent); expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 1].timeId)).not.toEqual(-1); - done(); }); }); @@ -422,7 +462,7 @@ describe("The Imagery View Layouts", () => { it("should show that an image is not new", (done) => { Vue.nextTick(() => { - const target = imageTelemetry[2].url; + const target = imageTelemetry[4].url; parent.querySelectorAll(`img[src='${target}']`)[0].click(); Vue.nextTick(() => { @@ -544,25 +584,6 @@ describe("The Imagery View Layouts", () => { expect(imageSizeAfter.width).toBeLessThan(imageSizeBefore.width); done(); }); - - it('clear data action is installed', () => { - expect(clearDataAction).toBeDefined(); - }); - - it('on clearData action should clear data for object is selected', async (done) => { - // force show the thumbnails - imageryView._getInstance().$children[0].forceShowThumbnails = true; - await Vue.nextTick(); - expect(parent.querySelectorAll('.c-imagery__thumb').length).not.toBe(0); - openmct.objectViews.on('clearData', async (_domainObject) => { - await Vue.nextTick(); - expect(parent.querySelectorAll('.c-imagery__thumb').length).toBe(0); - done(); - }); - // stubbed telemetry data will return empty array when true - isClearDataTriggered = true; - clearDataAction.invoke(imageryObject); - }); }); describe("imagery time strip view", () => { diff --git a/src/plugins/plan/util.js b/src/plugins/plan/util.js index c5e3cc6b83..4854f11b46 100644 --- a/src/plugins/plan/util.js +++ b/src/plugins/plan/util.js @@ -22,7 +22,7 @@ export function getValidatedData(domainObject) { let sourceMap = domainObject.sourceMap; - let body = domainObject.selectFile.body; + let body = domainObject.selectFile?.body; let json = {}; if (typeof body === 'string') { try { @@ -30,7 +30,7 @@ export function getValidatedData(domainObject) { } catch (e) { return json; } - } else { + } else if (body !== undefined) { json = body; } diff --git a/src/plugins/plot/MctPlot.vue b/src/plugins/plot/MctPlot.vue index 8ae4f08bf5..c1ff233ac5 100644 --- a/src/plugins/plot/MctPlot.vue +++ b/src/plugins/plot/MctPlot.vue @@ -26,6 +26,7 @@ :class="[plotLegendExpandedStateClass, plotLegendPositionClass]" > pathObject.type === 'telemetry.plot.stacked'); + }, isFrozen() { return this.config.xAxis.get('frozen') === true && this.config.yAxis.get('frozen') === true; }, plotLegendPositionClass() { - return `plot-legend-${this.config.legend.get('position')}`; + return !this.isNestedWithinAStackedPlot ? `plot-legend-${this.config.legend.get('position')}` : ''; }, plotLegendExpandedStateClass() { + if (this.isNestedWithinAStackedPlot) { + return ''; + } + if (this.config.legend.get('expanded')) { return 'plot-legend-expanded'; } else { @@ -292,6 +314,12 @@ export default { } }, watch: { + limitLineLabels: { + handler(limitLineLabels) { + this.legendHoverChanged(limitLineLabels); + }, + deep: true + }, initGridLines(newGridLines) { this.gridLines = newGridLines; }, @@ -310,6 +338,11 @@ export default { this.config = this.getConfig(); this.legend = this.config.legend; + if (this.isNestedWithinAStackedPlot) { + const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier); + this.$emit('configLoaded', configId); + } + this.listenTo(this.config.series, 'add', this.addSeries, this); this.listenTo(this.config.series, 'remove', this.removeSeries, this); @@ -375,6 +408,7 @@ export default { id: configId, domainObject: this.domainObject, openmct: this.openmct, + palette: this.colorPalette, callback: (data) => { this.data = data; } @@ -758,6 +792,8 @@ export default { }; }); } + + this.$emit('highlights', this.highlights); }, untrackMousePosition() { @@ -792,6 +828,7 @@ export default { if (this.isMouseClick()) { this.lockHighlightPoint = !this.lockHighlightPoint; + this.$emit('lockHighlightPoint', this.lockHighlightPoint); } if (this.pan) { diff --git a/src/plugins/plot/configuration/PlotConfigurationModel.js b/src/plugins/plot/configuration/PlotConfigurationModel.js index 0f6a56cd6f..ce0c6e532e 100644 --- a/src/plugins/plot/configuration/PlotConfigurationModel.js +++ b/src/plugins/plot/configuration/PlotConfigurationModel.js @@ -68,7 +68,8 @@ export default class PlotConfigurationModel extends Model { this.series = new SeriesCollection({ models: options.model.series, plot: this, - openmct: options.openmct + openmct: options.openmct, + palette: options.palette }); if (this.get('domainObject').type === 'telemetry.plot.overlay') { diff --git a/src/plugins/plot/configuration/SeriesCollection.js b/src/plugins/plot/configuration/SeriesCollection.js index 33160a8fa3..b5bb81dbbc 100644 --- a/src/plugins/plot/configuration/SeriesCollection.js +++ b/src/plugins/plot/configuration/SeriesCollection.js @@ -39,7 +39,7 @@ export default class SeriesCollection extends Collection { this.modelClass = PlotSeries; this.plot = options.plot; this.openmct = options.openmct; - this.palette = new ColorPalette(); + this.palette = options.palette || new ColorPalette(); this.listenTo(this, 'add', this.onSeriesAdd, this); this.listenTo(this, 'remove', this.onSeriesRemove, this); this.listenTo(this.plot, 'change:domainObject', this.trackPersistedConfig, this); diff --git a/src/plugins/plot/configuration/YAxisModel.js b/src/plugins/plot/configuration/YAxisModel.js index aaedcaae30..8c03631d6a 100644 --- a/src/plugins/plot/configuration/YAxisModel.js +++ b/src/plugins/plot/configuration/YAxisModel.js @@ -260,7 +260,7 @@ export default class YAxisModel extends Model { const plotModel = this.plot.get('domainObject'); const label = plotModel.configuration?.yAxis?.label; const sampleSeries = seriesCollection.first(); - if (!sampleSeries) { + if (!sampleSeries || !sampleSeries.metadata) { if (!label) { this.unset('label'); } diff --git a/src/plugins/plot/inspector/PlotOptionsBrowse.vue b/src/plugins/plot/inspector/PlotOptionsBrowse.vue index b85a2683f8..12ee36e7d8 100644 --- a/src/plugins/plot/inspector/PlotOptionsBrowse.vue +++ b/src/plugins/plot/inspector/PlotOptionsBrowse.vue @@ -24,7 +24,10 @@ v-if="loaded" class="js-plot-options-browse" > -