From a8fbabe695a82f2f1826f8d2aeeb3f42ddfe5ebe Mon Sep 17 00:00:00 2001 From: David Tsay <3614296+davetsay@users.noreply.github.com> Date: Fri, 27 Sep 2024 14:32:14 -0700 Subject: [PATCH] fix(#7825): imagery pause (#7842) * more readable * unpause explicitly * fix jsdoc * e2e testing multiple image removal * prettier * fix to remove multiple images from history * move tests that use playwright clock api into own file * fix playwright clock tests * add aria-label to element * prevent straggler debounced function call on unmount * clean up and fix tests * update paths * lint fix * lint fix --------- Co-authored-by: John Hill --- e2e/helper/imageryUtils.js | 33 ++ .../imagery/exampleImagery.e2e.spec.js | 211 +++----- .../exampleImageryControlledClock.e2e.spec.js | 489 ++++++++++++++++++ src/api/telemetry/TelemetryCollection.js | 3 +- .../imagery/components/ImageryTimeView.vue | 1 + .../imagery/components/ImageryView.vue | 4 +- src/plugins/imagery/mixins/imageryData.js | 17 +- src/plugins/timeline/TimelineViewLayout.vue | 1 + 8 files changed, 613 insertions(+), 146 deletions(-) create mode 100644 e2e/helper/imageryUtils.js create mode 100644 e2e/tests/functional/plugins/imagery/exampleImageryControlledClock.e2e.spec.js diff --git a/e2e/helper/imageryUtils.js b/e2e/helper/imageryUtils.js new file mode 100644 index 0000000000..880964076c --- /dev/null +++ b/e2e/helper/imageryUtils.js @@ -0,0 +1,33 @@ +import { createDomainObjectWithDefaults } from '../appActions.js'; +import { expect } from '../pluginFixtures.js'; + +const IMAGE_LOAD_DELAY = 5 * 1000; +const FIVE_MINUTES = 1000 * 60 * 5; +const THIRTY_SECONDS = 1000 * 30; +const MOUSE_WHEEL_DELTA_Y = 120; + +/** + * @param {import('@playwright/test').Page} page + */ +async function createImageryViewWithShortDelay(page, { name, parent }) { + await createDomainObjectWithDefaults(page, { + name, + type: 'Example Imagery', + parent + }); + + await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery'); + await page.getByLabel('More actions').click(); + await page.getByLabel('Edit Properties').click(); + // Clear and set Image load delay to minimum value + await page.locator('input[type="number"]').fill(`${IMAGE_LOAD_DELAY}`); + await page.getByLabel('Save').click(); +} + +export { + createImageryViewWithShortDelay, + FIVE_MINUTES, + IMAGE_LOAD_DELAY, + MOUSE_WHEEL_DELTA_Y, + THIRTY_SECONDS +}; diff --git a/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js b/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js index d75e104e0d..d5d218bc05 100644 --- a/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js +++ b/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js @@ -30,16 +30,19 @@ import { navigateToObjectWithRealTime, setRealTimeMode } from '../../../../appActions.js'; -import { MISSION_TIME } from '../../../../constants.js'; +import { + createImageryViewWithShortDelay, + FIVE_MINUTES, + IMAGE_LOAD_DELAY, + MOUSE_WHEEL_DELTA_Y, + THIRTY_SECONDS +} from '../../../../helper/imageryUtils.js'; import { expect, test } from '../../../../pluginFixtures.js'; + const panHotkey = process.platform === 'linux' ? ['Shift', 'Alt'] : ['Alt']; const tagHotkey = ['Shift', 'Alt']; const expectedAltText = process.platform === 'linux' ? 'Shift+Alt drag to pan' : 'Alt drag to pan'; const thumbnailUrlParamsRegexp = /\?w=100&h=100/; -const IMAGE_LOAD_DELAY = 5 * 1000; -const MOUSE_WHEEL_DELTA_Y = 120; -const FIVE_MINUTES = 1000 * 60 * 5; -const THIRTY_SECONDS = 1000 * 30; //The following block of tests verifies the basic functionality of example imagery and serves as a template for Imagery objects embedded in other objects. test.describe('Example Imagery Object', () => { @@ -357,15 +360,10 @@ test.describe('Example Imagery Object', () => { }); }); -test.describe('Example Imagery in Display Layout @clock', () => { +test.describe('Example Imagery in Display Layout', () => { let displayLayout; test.beforeEach(async ({ page }) => { - // We mock the clock so that we don't need to wait for time driven events - // to verify functionality. - await page.clock.install({ time: MISSION_TIME }); - await page.clock.resume(); - // Go to baseURL await page.goto('./', { waitUntil: 'domcontentloaded' }); @@ -428,12 +426,7 @@ test.describe('Example Imagery in Display Layout @clock', () => { await expect.soft(pausePlayButton).toHaveClass(/is-paused/); }); - test('Imagery View operations @clock', async ({ page }) => { - test.info().annotations.push({ - type: 'issue', - description: 'https://github.com/nasa/openmct/issues/5265' - }); - + test('Imagery View operations', async ({ page }) => { // Edit mode await page.getByLabel('Edit Object').click(); @@ -526,14 +519,9 @@ test.describe('Example Imagery in Display Layout @clock', () => { }); }); -test.describe('Example Imagery in Flexible layout @clock', () => { +test.describe('Example Imagery in Flexible layout', () => { let flexibleLayout; test.beforeEach(async ({ page }) => { - // We mock the clock so that we don't need to wait for time driven events - // to verify functionality. - await page.clock.install({ time: MISSION_TIME }); - await page.clock.resume(); - await page.goto('./', { waitUntil: 'domcontentloaded' }); flexibleLayout = await createDomainObjectWithDefaults(page, { type: 'Flexible Layout' }); @@ -562,7 +550,7 @@ test.describe('Example Imagery in Flexible layout @clock', () => { await page.getByRole('button', { name: 'Close' }).click(); }); - test('Imagery View operations @clock', async ({ page, browserName }) => { + test('Imagery View operations', async ({ page, browserName }) => { test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox'); test.info().annotations.push({ type: 'issue', @@ -573,14 +561,10 @@ test.describe('Example Imagery in Flexible layout @clock', () => { }); }); -test.describe('Example Imagery in Tabs View @clock', () => { +test.describe('Example Imagery in Tabs View', () => { let tabsView; - test.beforeEach(async ({ page }) => { - // We mock the clock so that we don't need to wait for time driven events - // to verify functionality. - await page.clock.install({ time: MISSION_TIME }); - await page.clock.resume(); + test.beforeEach(async ({ page }) => { await page.goto('./', { waitUntil: 'domcontentloaded' }); tabsView = await createDomainObjectWithDefaults(page, { type: 'Tabs View' }); @@ -607,7 +591,8 @@ test.describe('Example Imagery in Tabs View @clock', () => { // Wait for image thumbnail auto-scroll to complete await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport(); }); - test('Imagery View operations @clock', async ({ page }) => { + + test('Imagery View operations', async ({ page }) => { await performImageryViewOperationsAndAssert(page, tabsView); }); }); @@ -668,16 +653,19 @@ test.describe('Example Imagery in Time Strip', () => { * 3. Can pan the image using the pan hotkey + mouse drag * 4. Clicking on the left arrow button pauses imagery and moves to the previous image * 5. Imagery is updated as new images stream in, regardless of pause status - * 6. Old images are discarded when new images stream in - * 7. Image brightness/contrast can be adjusted by dragging the sliders + * 6. Old images are discarded when their timestamps fall out of bounds + * 7. Multiple images can be discarded when their timestamps fall out of bounds + * 8. Image brightness/contrast can be adjusted by dragging the sliders * @param {import('@playwright/test').Page} page */ async function performImageryViewOperationsAndAssert(page, layoutObject) { - // Verify that imagery thumbnails use a thumbnail url - const thumbnailImages = page.getByLabel('Image thumbnail from').locator('.c-thumb__image'); - const mainImage = page.locator('.c-imagery__main-image__image'); - await expect(thumbnailImages.first()).toHaveAttribute('src', thumbnailUrlParamsRegexp); - await expect(mainImage).not.toHaveAttribute('src', thumbnailUrlParamsRegexp); + await test.step('Verify that imagery thumbnails use a thumbnail url', async () => { + const thumbnailImages = page.getByLabel('Image thumbnail from').locator('.c-thumb__image'); + const mainImage = page.locator('.c-imagery__main-image__image'); + await expect(thumbnailImages.first()).toHaveAttribute('src', thumbnailUrlParamsRegexp); + await expect(mainImage).not.toHaveAttribute('src', thumbnailUrlParamsRegexp); + }); + // Click previous image button const previousImageButton = page.getByLabel('Previous image'); await expect(previousImageButton).toBeVisible(); @@ -736,19 +724,6 @@ async function performImageryViewOperationsAndAssert(page, layoutObject) { // Unpause imagery await page.locator('.pause-play').click(); - // verify that old images are discarded - const lastImageInBounds = page.getByLabel('Image thumbnail from').first(); - const lastImageTimestamp = await lastImageInBounds.getAttribute('title'); - expect(lastImageTimestamp).not.toBeNull(); - - // go forward in time to ensure old images are discarded - await page.clock.fastForward(IMAGE_LOAD_DELAY); - await page.clock.resume(); - await expect(page.getByLabel(lastImageTimestamp)).toBeHidden(); - - //Get background-image url from background-image css prop - await assertBackgroundImageUrlFromBackgroundCss(page); - // Open the image filter menu await page.locator('[role=toolbar] button[title="Brightness and contrast"]').click(); @@ -815,24 +790,6 @@ async function assertBackgroundImageBrightness(page, expected) { expect(actual).toBe(expected); } -/** - * @param {import('@playwright/test').Page} page - */ -async function assertBackgroundImageUrlFromBackgroundCss(page) { - const backgroundImage = page.getByLabel('Focused Image Element'); - const backgroundImageUrl = await backgroundImage.evaluate((el) => { - return window - .getComputedStyle(el) - .getPropertyValue('background-image') - .match(/url\(([^)]+)\)/)[1]; - }); - - // go forward in time to ensure old images are discarded - await page.clock.fastForward(IMAGE_LOAD_DELAY); - await page.clock.resume(); - await expect(backgroundImage).not.toHaveJSProperty('background-image', backgroundImageUrl); -} - /** * @param {import('@playwright/test').Page} page */ @@ -918,62 +875,66 @@ async function mouseZoomOnImageAndAssert(page, factor = 2) { * @param {import('@playwright/test').Page} page */ async function buttonZoomOnImageAndAssert(page) { - // Lock the zoom and pan so it doesn't reset if a new image comes in - await page.getByLabel('Focused Image Element').hover({ trial: true }); - const lockButton = page.getByRole('button', { - name: 'Lock current zoom and pan across all images' - }); - if (!(await lockButton.isVisible())) { + await test.step('Can zoom using buttons', async () => { + // Lock the zoom and pan so it doesn't reset if a new image comes in await page.getByLabel('Focused Image Element').hover({ trial: true }); - } - await lockButton.click(); + const lockButton = page.getByRole('button', { + name: 'Lock current zoom and pan across all images' + }); - await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty( - 'style.transform', - 'scale(1) translate(0px, 0px)' - ); + await lockButton.isVisible(); + // if (!(await lockButton.isVisible())) { + // await page.getByLabel('Focused Image Element').hover({ trial: true }); + // } + await lockButton.click(); - // Get initial image dimensions - const initialBoundingBox = await page.getByLabel('Focused Image Element').boundingBox(); + await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty( + 'style.transform', + 'scale(1) translate(0px, 0px)' + ); - // Zoom in twice via button - await zoomIntoImageryByButton(page); - await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty( - 'style.transform', - 'scale(2) translate(0px, 0px)' - ); - await zoomIntoImageryByButton(page); - await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty( - 'style.transform', - 'scale(3) translate(0px, 0px)' - ); + // Get initial image dimensions + const initialBoundingBox = await page.getByLabel('Focused Image Element').boundingBox(); - // Get and assert zoomed in image dimensions - const zoomedInBoundingBox = await page.getByLabel('Focused Image Element').boundingBox(); - expect(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height); - expect(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width); + // Zoom in twice via button + await zoomIntoImageryByButton(page); + await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty( + 'style.transform', + 'scale(2) translate(0px, 0px)' + ); + await zoomIntoImageryByButton(page); + await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty( + 'style.transform', + 'scale(3) translate(0px, 0px)' + ); - // Zoom out once via button - await zoomOutOfImageryByButton(page); - await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty( - 'style.transform', - 'scale(2) translate(0px, 0px)' - ); + // Get and assert zoomed in image dimensions + const zoomedInBoundingBox = await page.getByLabel('Focused Image Element').boundingBox(); + expect(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height); + expect(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width); - // Get and assert zoomed out image dimensions - const zoomedOutBoundingBox = await page.getByLabel('Focused Image Element').boundingBox(); - expect(zoomedOutBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height); - expect(zoomedOutBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width); + // Zoom out once via button + await zoomOutOfImageryByButton(page); + await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty( + 'style.transform', + 'scale(2) translate(0px, 0px)' + ); - // Zoom out again via button, assert against the initial image dimensions - await zoomOutOfImageryByButton(page); - await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty( - 'style.transform', - 'scale(1) translate(0px, 0px)' - ); + // Get and assert zoomed out image dimensions + const zoomedOutBoundingBox = await page.getByLabel('Focused Image Element').boundingBox(); + expect(zoomedOutBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height); + expect(zoomedOutBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width); - const finalBoundingBox = await page.getByLabel('Focused Image Element').boundingBox(); - expect(finalBoundingBox).toEqual(initialBoundingBox); + // Zoom out again via button, assert against the initial image dimensions + await zoomOutOfImageryByButton(page); + await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty( + 'style.transform', + 'scale(1) translate(0px, 0px)' + ); + + const finalBoundingBox = await page.getByLabel('Focused Image Element').boundingBox(); + expect(finalBoundingBox).toEqual(initialBoundingBox); + }); } /** @@ -1035,24 +996,6 @@ async function resetImageryPanAndZoom(page) { await expect(page.locator('.c-thumb__viewable-area')).toBeHidden(); } -/** - * @param {import('@playwright/test').Page} page - */ -async function createImageryViewWithShortDelay(page, { name, parent }) { - await createDomainObjectWithDefaults(page, { - name, - type: 'Example Imagery', - parent - }); - - await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery'); - await page.getByLabel('More actions').click(); - await page.getByLabel('Edit Properties').click(); - // Clear and set Image load delay to minimum value - await page.locator('input[type="number"]').fill(`${IMAGE_LOAD_DELAY}`); - await page.getByLabel('Save').click(); -} - /** * @param {import('@playwright/test').Page} page */ diff --git a/e2e/tests/functional/plugins/imagery/exampleImageryControlledClock.e2e.spec.js b/e2e/tests/functional/plugins/imagery/exampleImageryControlledClock.e2e.spec.js new file mode 100644 index 0000000000..97c45acbd7 --- /dev/null +++ b/e2e/tests/functional/plugins/imagery/exampleImageryControlledClock.e2e.spec.js @@ -0,0 +1,489 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2024, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +/* +This test suite is dedicated to testing how imagery functions over time. +It only assumes that example imagery is present. +It uses https://playwright.dev/docs/clock to have control over time +*/ + +import { + createDomainObjectWithDefaults, + navigateToObjectWithRealTime, + setRealTimeMode, + setStartOffset +} from '../../../../appActions.js'; +import { MISSION_TIME } from '../../../../constants.js'; +import { + createImageryViewWithShortDelay, + FIVE_MINUTES, + IMAGE_LOAD_DELAY, + THIRTY_SECONDS +} from '../../../../helper/imageryUtils.js'; +import { expect, test } from '../../../../pluginFixtures.js'; + +test.describe('Example Imagery Object with Controlled Clock @clock', () => { + test.beforeEach(async ({ page }) => { + // We mock the clock so that we don't need to wait for time driven events + // to verify functionality. + await page.clock.install({ time: MISSION_TIME }); + await page.clock.resume(); + + //Go to baseURL + await page.goto('./', { waitUntil: 'domcontentloaded' }); + + // Create a default 'Example Imagery' object + // Click the Create button + await page.getByRole('button', { name: 'Create' }).click(); + + // Click text=Example Imagery + await page.getByRole('menuitem', { name: 'Example Imagery' }).click(); + + // Clear and set Image load delay to minimum value + await page.locator('input[type="number"]').clear(); + await page.locator('input[type="number"]').fill(`${IMAGE_LOAD_DELAY}`); + + await page.getByLabel('Save').click(); + + // Verify that the created object is focused + await expect(page.locator('.l-browse-bar__object-name')).toContainText( + 'Unnamed Example Imagery' + ); + await page.getByLabel('Focused Image Element').hover({ trial: true }); + + // set realtime mode + await setRealTimeMode(page); + await setStartOffset(page, { startMins: '05' }); + }); + + test('Imagery Time Bounding', async ({ page }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/nasa/openmct/issues/5265' + }); + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/nasa/openmct/issues/7825' + }); + + // verify that old images are discarded + const lastImageInBounds = page.getByLabel('Image thumbnail from').first(); + const lastImageTimestamp = await lastImageInBounds.getAttribute('title'); + expect(lastImageTimestamp).not.toBeNull(); + + // go forward in time to ensure old images are discarded + await page.clock.fastForward(IMAGE_LOAD_DELAY); + await page.clock.resume(); + await expect(page.getByLabel(lastImageTimestamp)).toBeHidden(); + + // go way forward in time to ensure multiple images are discarded + const IMAGES_TO_DISCARD_COUNT = 5; + + const lastImageToDiscard = page + .getByLabel('Image thumbnail from') + .nth(IMAGES_TO_DISCARD_COUNT - 1); + const lastImageToDiscardTimestamp = await lastImageToDiscard.getAttribute('title'); + expect(lastImageToDiscardTimestamp).not.toBeNull(); + + const imageAfterLastImageToDiscard = page + .getByLabel('Image thumbnail from') + .nth(IMAGES_TO_DISCARD_COUNT); + const imageAfterLastImageToDiscardTimestamp = + await imageAfterLastImageToDiscard.getAttribute('title'); + expect(imageAfterLastImageToDiscardTimestamp).not.toBeNull(); + + await page.clock.fastForward(IMAGE_LOAD_DELAY * IMAGES_TO_DISCARD_COUNT); + await page.clock.resume(); + + await expect(page.getByLabel(lastImageToDiscardTimestamp)).toBeHidden(); + await expect(page.getByLabel(imageAfterLastImageToDiscardTimestamp)).toBeVisible(); + }); + + test('Get background-image url from background-image css prop', async ({ page }) => { + await assertBackgroundImageUrlFromBackgroundCss(page); + }); +}); + +test.describe('Example Imagery in Display Layout with Controlled Clock @clock', () => { + let displayLayout; + + test.beforeEach(async ({ page }) => { + // We mock the clock so that we don't need to wait for time driven events + // to verify functionality. + await page.clock.install({ time: MISSION_TIME }); + await page.clock.resume(); + + // Go to baseURL + await page.goto('./', { waitUntil: 'domcontentloaded' }); + + displayLayout = await createDomainObjectWithDefaults(page, { type: 'Display Layout' }); + + // Create Example Imagery inside Display Layout + await createImageryViewWithShortDelay(page, { + name: 'Unnamed Example Imagery', + parent: displayLayout.uuid + }); + + // set realtime mode + await navigateToObjectWithRealTime( + page, + displayLayout.url, + `${FIVE_MINUTES}`, + `${THIRTY_SECONDS}` + ); + }); + + test('Imagery Time Bounding', async ({ page }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/nasa/openmct/issues/5265' + }); + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/nasa/openmct/issues/7825' + }); + + // Edit mode + await page.getByLabel('Edit Object').click(); + + // Click on example imagery to expose toolbar + await page.locator('.c-so-view__header').click(); + + // Adjust object height + await page.locator('div[title="Resize object height"] > input').click(); + await page.locator('div[title="Resize object height"] > input').fill('50'); + + // Adjust object width + await page.locator('div[title="Resize object width"] > input').click(); + await page.locator('div[title="Resize object width"] > input').fill('50'); + + await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport(); + + // verify that old images are discarded + const lastImageInBounds = page.getByLabel('Image thumbnail from').first(); + const lastImageTimestamp = await lastImageInBounds.getAttribute('title'); + expect(lastImageTimestamp).not.toBeNull(); + + // go forward in time to ensure old images are discarded + await page.clock.fastForward(IMAGE_LOAD_DELAY); + await page.clock.resume(); + await expect(page.getByLabel(lastImageTimestamp)).toBeHidden(); + + // go way forward in time to ensure multiple images are discarded + const IMAGES_TO_DISCARD_COUNT = 5; + + const lastImageToDiscard = page + .getByLabel('Image thumbnail from') + .nth(IMAGES_TO_DISCARD_COUNT - 1); + const lastImageToDiscardTimestamp = await lastImageToDiscard.getAttribute('title'); + expect(lastImageToDiscardTimestamp).not.toBeNull(); + + const imageAfterLastImageToDiscard = page + .getByLabel('Image thumbnail from') + .nth(IMAGES_TO_DISCARD_COUNT); + const imageAfterLastImageToDiscardTimestamp = + await imageAfterLastImageToDiscard.getAttribute('title'); + expect(imageAfterLastImageToDiscardTimestamp).not.toBeNull(); + + await page.clock.fastForward(IMAGE_LOAD_DELAY * IMAGES_TO_DISCARD_COUNT); + await page.clock.resume(); + + await expect(page.getByLabel(lastImageToDiscardTimestamp)).toBeHidden(); + await expect(page.getByLabel(imageAfterLastImageToDiscardTimestamp)).toBeVisible(); + }); + + test('Get background-image url from background-image css prop @clock', async ({ page }) => { + await assertBackgroundImageUrlFromBackgroundCss(page); + }); +}); + +test.describe('Example Imagery in Flexible layout with Controlled Clock @clock', () => { + let flexibleLayout; + + test.beforeEach(async ({ page }) => { + // We mock the clock so that we don't need to wait for time driven events + // to verify functionality. + await page.clock.install({ time: MISSION_TIME }); + await page.clock.resume(); + + await page.goto('./', { waitUntil: 'domcontentloaded' }); + + flexibleLayout = await createDomainObjectWithDefaults(page, { type: 'Flexible Layout' }); + + // Create Example Imagery inside the Flexible Layout + await createImageryViewWithShortDelay(page, { + name: 'Unnamed Example Imagery', + parent: flexibleLayout.uuid + }); + + // set realtime mode + await navigateToObjectWithRealTime( + page, + flexibleLayout.url, + `${FIVE_MINUTES}`, + `${THIRTY_SECONDS}` + ); + + // Wait for image thumbnail auto-scroll to complete + await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport(); + }); + + test('Imagery Time Bounding @clock', async ({ page, browserName }) => { + test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox'); + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/nasa/openmct/issues/5326' + }); + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/nasa/openmct/issues/7825' + }); + + // verify that old images are discarded + const lastImageInBounds = page.getByLabel('Image thumbnail from').first(); + const lastImageTimestamp = await lastImageInBounds.getAttribute('title'); + expect(lastImageTimestamp).not.toBeNull(); + + // go forward in time to ensure old images are discarded + await page.clock.fastForward(IMAGE_LOAD_DELAY); + await page.clock.resume(); + await expect(page.getByLabel(lastImageTimestamp)).toBeHidden(); + + // go way forward in time to ensure multiple images are discarded + const IMAGES_TO_DISCARD_COUNT = 5; + + const lastImageToDiscard = page + .getByLabel('Image thumbnail from') + .nth(IMAGES_TO_DISCARD_COUNT - 1); + const lastImageToDiscardTimestamp = await lastImageToDiscard.getAttribute('title'); + expect(lastImageToDiscardTimestamp).not.toBeNull(); + + const imageAfterLastImageToDiscard = page + .getByLabel('Image thumbnail from') + .nth(IMAGES_TO_DISCARD_COUNT); + const imageAfterLastImageToDiscardTimestamp = + await imageAfterLastImageToDiscard.getAttribute('title'); + expect(imageAfterLastImageToDiscardTimestamp).not.toBeNull(); + + await page.clock.fastForward(IMAGE_LOAD_DELAY * IMAGES_TO_DISCARD_COUNT); + await page.clock.resume(); + + await expect(page.getByLabel(lastImageToDiscardTimestamp)).toBeHidden(); + await expect(page.getByLabel(imageAfterLastImageToDiscardTimestamp)).toBeVisible(); + }); + + test('Get background-image url from background-image css prop @clock', async ({ page }) => { + await assertBackgroundImageUrlFromBackgroundCss(page); + }); +}); + +test.describe('Example Imagery in Tabs View with Controlled Clock @clock', () => { + let timeStripObject; + + test.beforeEach(async ({ page }) => { + // We mock the clock so that we don't need to wait for time driven events + // to verify functionality. + await page.clock.install({ time: MISSION_TIME }); + await page.clock.resume(); + + await page.goto('./', { waitUntil: 'domcontentloaded' }); + + timeStripObject = await createDomainObjectWithDefaults(page, { type: 'Tabs View' }); + await page.goto(timeStripObject.url); + + /* Create Example Imagery with minimum Image Load Delay */ + // Click the Create button + await page.getByRole('button', { name: 'Create' }).click(); + + // Click text=Example Imagery + await page.getByRole('menuitem', { name: 'Example Imagery' }).click(); + + // Clear and set Image load delay to minimum value + await page.locator('input[type="number"]').clear(); + await page.locator('input[type="number"]').fill(`${IMAGE_LOAD_DELAY}`); + + await page.getByLabel('Save').click(); + + await expect(page.locator('.l-browse-bar__object-name')).toContainText( + 'Unnamed Example Imagery' + ); + + // set realtime mode + await navigateToObjectWithRealTime( + page, + timeStripObject.url, + `${FIVE_MINUTES}`, + `${THIRTY_SECONDS}` + ); + + // Wait for image thumbnail auto-scroll to complete + await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport(); + }); + + test('Imagery Time Bounding @clock', async ({ page }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/nasa/openmct/issues/5265' + }); + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/nasa/openmct/issues/7825' + }); + + // verify that old images are discarded + const lastImageInBounds = page.getByLabel('Image thumbnail from').first(); + const lastImageTimestamp = await lastImageInBounds.getAttribute('title'); + expect(lastImageTimestamp).not.toBeNull(); + + // go forward in time to ensure old images are discarded + await page.clock.fastForward(IMAGE_LOAD_DELAY); + await page.clock.resume(); + await expect(page.getByLabel(lastImageTimestamp)).toBeHidden(); + + // go way forward in time to ensure multiple images are discarded + const IMAGES_TO_DISCARD_COUNT = 5; + + const lastImageToDiscard = page + .getByLabel('Image thumbnail from') + .nth(IMAGES_TO_DISCARD_COUNT - 1); + const lastImageToDiscardTimestamp = await lastImageToDiscard.getAttribute('title'); + expect(lastImageToDiscardTimestamp).not.toBeNull(); + + const imageAfterLastImageToDiscard = page + .getByLabel('Image thumbnail from') + .nth(IMAGES_TO_DISCARD_COUNT); + const imageAfterLastImageToDiscardTimestamp = + await imageAfterLastImageToDiscard.getAttribute('title'); + expect(imageAfterLastImageToDiscardTimestamp).not.toBeNull(); + + await page.clock.fastForward(IMAGE_LOAD_DELAY * IMAGES_TO_DISCARD_COUNT); + await page.clock.resume(); + + await expect(page.getByLabel(lastImageToDiscardTimestamp)).toBeHidden(); + await expect(page.getByLabel(imageAfterLastImageToDiscardTimestamp)).toBeVisible(); + }); + + test('Get background-image url from background-image css prop @clock', async ({ page }) => { + await assertBackgroundImageUrlFromBackgroundCss(page); + }); +}); + +test.describe('Example Imagery in Time Strip with Controlled Clock @clock', () => { + let timeStripObject; + + test.beforeEach(async ({ page }) => { + // We mock the clock so that we don't need to wait for time driven events + // to verify functionality. + await page.clock.install({ time: MISSION_TIME }); + await page.clock.resume(); + + await page.goto('./', { waitUntil: 'domcontentloaded' }); + + timeStripObject = await createDomainObjectWithDefaults(page, { type: 'Time Strip' }); + await page.goto(timeStripObject.url); + + /* Create Example Imagery with minimum Image Load Delay */ + // Click the Create button + await page.getByRole('button', { name: 'Create' }).click(); + + // Click text=Example Imagery + await page.getByRole('menuitem', { name: 'Example Imagery' }).click(); + + // Clear and set Image load delay to minimum value + await page.locator('input[type="number"]').clear(); + await page.locator('input[type="number"]').fill(`${IMAGE_LOAD_DELAY}`); + + await page.getByLabel('Save').click(); + + await expect(page.locator('.l-browse-bar__object-name')).toContainText( + 'Unnamed Example Imagery' + ); + + // set realtime mode + await navigateToObjectWithRealTime( + page, + timeStripObject.url, + `${FIVE_MINUTES}`, + `${THIRTY_SECONDS}` + ); + + // Wait for image thumbnail auto-scroll to complete + await expect(page.getByLabel('wrapper-').last()).toBeInViewport(); + }); + + test('Imagery Time Bounding @clock', async ({ page }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/nasa/openmct/issues/5265' + }); + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/nasa/openmct/issues/7825' + }); + + // verify that old images are discarded + const lastImageInBounds = page.getByLabel('wrapper-').first(); + const lastImageTimestamp = await lastImageInBounds.getAttribute('aria-label'); + expect(lastImageTimestamp).not.toBeNull(); + + // go forward in time to ensure old images are discarded + await page.clock.fastForward(IMAGE_LOAD_DELAY); + await page.clock.resume(); + await expect(page.getByLabel(lastImageTimestamp)).toBeHidden(); + + // go way forward in time to ensure multiple images are discarded + const IMAGES_TO_DISCARD_COUNT = 5; + + const lastImageToDiscard = page.getByLabel('wrapper-').nth(IMAGES_TO_DISCARD_COUNT - 1); + const lastImageToDiscardTimestamp = await lastImageToDiscard.getAttribute('aria-label'); + expect(lastImageToDiscardTimestamp).not.toBeNull(); + + const imageAfterLastImageToDiscard = page.getByLabel('wrapper-').nth(IMAGES_TO_DISCARD_COUNT); + const imageAfterLastImageToDiscardTimestamp = + await imageAfterLastImageToDiscard.getAttribute('aria-label'); + expect(imageAfterLastImageToDiscardTimestamp).not.toBeNull(); + + await page.clock.fastForward(IMAGE_LOAD_DELAY * IMAGES_TO_DISCARD_COUNT); + await page.clock.resume(); + + await expect(page.getByLabel(lastImageToDiscardTimestamp)).toBeHidden(); + await expect(page.getByLabel(imageAfterLastImageToDiscardTimestamp)).toBeVisible(); + }); +}); + +/** + * @param {import('@playwright/test').Page} page + */ +async function assertBackgroundImageUrlFromBackgroundCss(page) { + const backgroundImage = page.getByLabel('Focused Image Element'); + const backgroundImageUrl = await backgroundImage.evaluate((el) => { + return window + .getComputedStyle(el) + .getPropertyValue('background-image') + .match(/url\(([^)]+)\)/)[1]; + }); + + // go forward in time to ensure old images are discarded + await page.clock.fastForward(IMAGE_LOAD_DELAY); + await page.clock.resume(); + await expect(backgroundImage).not.toHaveJSProperty('background-image', backgroundImageUrl); +} diff --git a/src/api/telemetry/TelemetryCollection.js b/src/api/telemetry/TelemetryCollection.js index 78e48dc89b..d6f53816b9 100644 --- a/src/api/telemetry/TelemetryCollection.js +++ b/src/api/telemetry/TelemetryCollection.js @@ -116,8 +116,7 @@ export default class TelemetryCollection extends EventEmitter { } /** - * This will start the requests for historical and realtime data, - * as well as setting up initial values and watchers + * @returns {Array} All bounded telemetry */ getAll() { return this.boundedTelemetry; diff --git a/src/plugins/imagery/components/ImageryTimeView.vue b/src/plugins/imagery/components/ImageryTimeView.vue index dfb796b5b6..9c65939e65 100644 --- a/src/plugins/imagery/components/ImageryTimeView.vue +++ b/src/plugins/imagery/components/ImageryTimeView.vue @@ -370,6 +370,7 @@ export default { createImageWrapper(index, image, showImagePlaceholders) { const id = `${ID_PREFIX}${image.time}`; let imageWrapper = document.createElement('div'); + imageWrapper.ariaLabel = id; imageWrapper.classList.add(IMAGE_WRAPPER_CLASS); imageWrapper.style.left = `${this.xScale(image.time)}px`; this.setNSAttributesForElement(imageWrapper, { diff --git a/src/plugins/imagery/components/ImageryView.vue b/src/plugins/imagery/components/ImageryView.vue index 5f868b9eb8..db0541f029 100644 --- a/src/plugins/imagery/components/ImageryView.vue +++ b/src/plugins/imagery/components/ImageryView.vue @@ -620,7 +620,7 @@ export default { if (matchIndex > -1) { this.setFocusedImage(matchIndex); } else { - this.paused(); + this.paused(false); } } @@ -1082,7 +1082,7 @@ export default { paused(state) { this.isPaused = Boolean(state); - if (!state) { + if (!this.isPaused) { this.previousFocusedImage = null; this.setFocusedImage(this.nextImageIndex); this.autoScroll = true; diff --git a/src/plugins/imagery/mixins/imageryData.js b/src/plugins/imagery/mixins/imageryData.js index 804d06f4e3..04914b30a0 100644 --- a/src/plugins/imagery/mixins/imageryData.js +++ b/src/plugins/imagery/mixins/imageryData.js @@ -90,16 +90,17 @@ export default { dataCleared() { this.imageHistory = []; }, - dataRemoved(dataToRemove) { - this.imageHistory = this.imageHistory.filter((existingDatum) => { - const shouldKeep = dataToRemove.some((datumToRemove) => { - const existingDatumTimestamp = this.parseTime(existingDatum); - const datumToRemoveTimestamp = this.parseTime(datumToRemove); + dataRemoved(removed) { + const removedTimestamps = {}; + removed.forEach((_removed) => { + const removedTimestamp = this.parseTime(_removed); + removedTimestamps[removedTimestamp] = true; + }); - return existingDatumTimestamp !== datumToRemoveTimestamp; - }); + this.imageHistory = this.imageHistory.filter((image) => { + const imageTimestamp = this.parseTime(image); - return shouldKeep; + return !removedTimestamps[imageTimestamp]; }); }, setDataTimeContext() { diff --git a/src/plugins/timeline/TimelineViewLayout.vue b/src/plugins/timeline/TimelineViewLayout.vue index 4a85a9a04d..b1652599d4 100644 --- a/src/plugins/timeline/TimelineViewLayout.vue +++ b/src/plugins/timeline/TimelineViewLayout.vue @@ -99,6 +99,7 @@ export default { this.composition.off('remove', this.removeItem); this.composition.off('reorder', this.reorder); this.stopFollowingTimeContext(); + this.handleContentResize.cancel(); this.contentResizeObserver.disconnect(); }, mounted() {