From e3fcbe1a3525720c0494c0b4463f73581899916d Mon Sep 17 00:00:00 2001 From: Jesse Mazzella Date: Thu, 25 Jul 2024 16:55:50 -0700 Subject: [PATCH] fix(#7791): tc form shouldn't submit bounds changes on dismiss (#7792) * fix(#7791): tc form shouldn't submit bounds changes on dismiss * test(e2e): add tests for validating time conductor popup - update appAction for setting time conductor in fixed mode - add a11y to time conductor in fixed mode - update tests using `setTimeConductorBounds` * fix(#7791): actually fix the problem. Also, add a test. * test: add annotation to regression test * docs: comments * test: fix the reset image button flake ONCE AND FOR ALL - wait for the rightmost image thumbnail to be in the viewport :D * test: add tests for `setTimeConductorMode` and `setTimeConductorBounds` --- e2e/appActions.js | 73 ++++--- e2e/tests/framework/appActions.e2e.spec.js | 35 +++- .../generateLocalStorageData.e2e.spec.js | 25 ++- .../imagery/exampleImagery.e2e.spec.js | 3 + .../plugins/plot/logPlot.e2e.spec.js | 59 +++--- .../telemetryTable/telemetryTable.e2e.spec.js | 13 +- .../timeConductor/timeConductor.e2e.spec.js | 185 +++++++++++++----- e2e/tests/visual-a11y/imagery.visual.spec.js | 7 + .../timeConductor/ConductorInputsFixed.vue | 4 +- src/plugins/timeConductor/TimePopupFixed.vue | 70 +++---- 10 files changed, 302 insertions(+), 172 deletions(-) diff --git a/e2e/appActions.js b/e2e/appActions.js index dea7257a47..c14bceb020 100644 --- a/e2e/appActions.js +++ b/e2e/appActions.js @@ -436,61 +436,67 @@ async function setRealTimeMode(page) { /** * Set the values (hours, mins, secs) for the TimeConductor offsets when in realtime mode * @param {import('@playwright/test').Page} page - * @param {OffsetValues} offset - * @param {import('@playwright/test').Locator} offsetButton + * @param {OffsetValues} offset - Object containing offset values + * @param {boolean} [offset.submitChanges=true] - If true, submit the offset changes; otherwise, discard them */ async function setTimeConductorOffset( page, - { startHours, startMins, startSecs, endHours, endMins, endSecs } + { startHours, startMins, startSecs, endHours, endMins, endSecs, submitChanges = true } ) { if (startHours) { - await page.getByRole('spinbutton', { name: 'Start offset hours' }).fill(startHours); + await page.getByLabel('Start offset hours').fill(startHours); } if (startMins) { - await page.getByRole('spinbutton', { name: 'Start offset minutes' }).fill(startMins); + await page.getByLabel('Start offset minutes').fill(startMins); } if (startSecs) { - await page.getByRole('spinbutton', { name: 'Start offset seconds' }).fill(startSecs); + await page.getByLabel('Start offset seconds').fill(startSecs); } if (endHours) { - await page.getByRole('spinbutton', { name: 'End offset hours' }).fill(endHours); + await page.getByLabel('End offset hours').fill(endHours); } if (endMins) { - await page.getByRole('spinbutton', { name: 'End offset minutes' }).fill(endMins); + await page.getByLabel('End offset minutes').fill(endMins); } if (endSecs) { - await page.getByRole('spinbutton', { name: 'End offset seconds' }).fill(endSecs); + await page.getByLabel('End offset seconds').fill(endSecs); } // Click the check button - await page.locator('.pr-time-input--buttons .icon-check').click(); + if (submitChanges) { + await page.getByLabel('Submit time offsets').click(); + } else { + await page.getByLabel('Discard changes and close time popup').click(); + } } /** * Set the values (hours, mins, secs) for the start time offset when in realtime mode * @param {import('@playwright/test').Page} page * @param {OffsetValues} offset + * @param {boolean} [submit=true] If true, submit the offset changes; otherwise, discard them */ -async function setStartOffset(page, offset) { +async function setStartOffset(page, { submitChanges = true, ...offset }) { // Click 'mode' button await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click(); - await setTimeConductorOffset(page, offset); + await setTimeConductorOffset(page, { submitChanges, ...offset }); } /** * Set the values (hours, mins, secs) for the end time offset when in realtime mode * @param {import('@playwright/test').Page} page * @param {OffsetValues} offset + * @param {boolean} [submit=true] If true, submit the offset changes; otherwise, discard them */ -async function setEndOffset(page, offset) { +async function setEndOffset(page, { submitChanges = true, ...offset }) { // Click 'mode' button await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click(); - await setTimeConductorOffset(page, offset); + await setTimeConductorOffset(page, { submitChanges, ...offset }); } /** @@ -499,17 +505,40 @@ async function setEndOffset(page, offset) { * NOTE: Unless explicitly testing the Time Conductor itself, it is advised to instead * navigate directly to the object with the desired time bounds using `navigateToObjectWithFixedTimeBounds()`. * @param {import('@playwright/test').Page} page - * @param {string} startDate - * @param {string} endDate + * @param {Object} bounds - The time conductor bounds + * @param {string} [bounds.startDate] - The start date in YYYY-MM-DD format + * @param {string} [bounds.startTime] - The start time in HH:mm:ss format + * @param {string} [bounds.endDate] - The end date in YYYY-MM-DD format + * @param {string} [bounds.endTime] - The end time in HH:mm:ss format + * @param {boolean} [bounds.submitChanges=true] - If true, submit the changes; otherwise, discard them. */ -async function setTimeConductorBounds(page, startDate, endDate) { - // Bring up the time conductor popup - expect(await page.locator('.l-shell__time-conductor.c-compact-tc').count()).toBe(1); - await page.click('.l-shell__time-conductor.c-compact-tc'); +async function setTimeConductorBounds(page, { submitChanges = true, ...bounds }) { + const { startDate, endDate, startTime, endTime } = bounds; - await setTimeBounds(page, startDate, endDate); + // Open the time conductor popup + await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click(); - await page.keyboard.press('Enter'); + if (startDate) { + await page.getByLabel('Start date').fill(startDate); + } + + if (startTime) { + await page.getByLabel('Start time').fill(startTime); + } + + if (endDate) { + await page.getByLabel('End date').fill(endDate); + } + + if (endTime) { + await page.getByLabel('End time').fill(endTime); + } + + if (submitChanges) { + await page.getByLabel('Submit time bounds').click(); + } else { + await page.getByLabel('Discard changes and close time popup').click(); + } } /** diff --git a/e2e/tests/framework/appActions.e2e.spec.js b/e2e/tests/framework/appActions.e2e.spec.js index cc93247951..d58d73968c 100644 --- a/e2e/tests/framework/appActions.e2e.spec.js +++ b/e2e/tests/framework/appActions.e2e.spec.js @@ -24,14 +24,18 @@ import { createDomainObjectWithDefaults, createNotification, expandEntireTree, - openObjectTreeContextMenu + openObjectTreeContextMenu, + setFixedTimeMode, + setRealTimeMode, + setTimeConductorBounds } from '../../appActions.js'; import { expect, test } from '../../pluginFixtures.js'; test.describe('AppActions', () => { - test('createDomainObjectsWithDefaults', async ({ page }) => { + test.beforeEach(async ({ page }) => { await page.goto('./', { waitUntil: 'domcontentloaded' }); - + }); + test('createDomainObjectsWithDefaults', async ({ page }) => { const e2eFolder = await createDomainObjectWithDefaults(page, { type: 'Folder', name: 'e2e folder' @@ -91,7 +95,6 @@ test.describe('AppActions', () => { }); }); test('createNotification', async ({ page }) => { - await page.goto('./', { waitUntil: 'domcontentloaded' }); await createNotification(page, { message: 'Test info notification', severity: 'info' @@ -115,8 +118,6 @@ test.describe('AppActions', () => { await page.locator('[aria-label="Dismiss"]').click(); }); test('expandEntireTree', async ({ page }) => { - await page.goto('./', { waitUntil: 'domcontentloaded' }); - const rootFolder = await createDomainObjectWithDefaults(page, { type: 'Folder' }); @@ -168,12 +169,30 @@ test.describe('AppActions', () => { expect(await locatorTreeCollapsedItems.count()).toBe(0); }); test('openObjectTreeContextMenu', async ({ page }) => { - await page.goto('./', { waitUntil: 'domcontentloaded' }); - const folder = await createDomainObjectWithDefaults(page, { type: 'Folder' }); await openObjectTreeContextMenu(page, folder.url); await expect(page.getByLabel(`${folder.name} Context Menu`)).toBeVisible(); }); + test('setTimeConductorMode', async ({ page }) => { + await setFixedTimeMode(page); + await expect(page.getByLabel('Start bounds:')).toBeVisible(); + await expect(page.getByLabel('End bounds:')).toBeVisible(); + await setRealTimeMode(page); + await expect(page.getByLabel('Start offset')).toBeVisible(); + await expect(page.getByLabel('End offset')).toBeVisible(); + }); + test('setTimeConductorBounds', async ({ page }) => { + // Assume in real-time mode by default + await setFixedTimeMode(page); + await setTimeConductorBounds(page, { + startDate: '2024-01-01', + endDate: '2024-01-02', + startTime: '00:00:00', + endTime: '23:59:59' + }); + await expect(page.getByLabel('Start bounds: 2024-01-01 00:00:00')).toBeVisible(); + await expect(page.getByLabel('End bounds: 2024-01-02 23:59:59')).toBeVisible(); + }); }); diff --git a/e2e/tests/framework/generateLocalStorageData.e2e.spec.js b/e2e/tests/framework/generateLocalStorageData.e2e.spec.js index de8741cc88..ee9ff5d73b 100644 --- a/e2e/tests/framework/generateLocalStorageData.e2e.spec.js +++ b/e2e/tests/framework/generateLocalStorageData.e2e.spec.js @@ -117,18 +117,25 @@ test.describe('Generate Visual Test Data @localStorage @generatedata @clock', () end: '2024-11-12 20:11:11.000Z' }); - const NEW_GLOBAL_START_BOUNDS = '2024-11-11 19:11:11.000Z'; - const NEW_GLOBAL_END_BOUNDS = '2024-11-11 20:11:11.000Z'; + const NEW_GLOBAL_START_DATE = '2024-11-11'; + const NEW_GLOBAL_START_TIME = '19:11:11'; + const NEW_GLOBAL_END_DATE = '2024-11-11'; + const NEW_GLOBAL_END_TIME = '20:11:11'; - await setTimeConductorBounds(page, NEW_GLOBAL_START_BOUNDS, NEW_GLOBAL_END_BOUNDS); + await setTimeConductorBounds(page, { + startDate: NEW_GLOBAL_START_DATE, + startTime: NEW_GLOBAL_START_TIME, + endDate: NEW_GLOBAL_END_DATE, + endTime: NEW_GLOBAL_END_TIME + }); // Verify that the global time conductor bounds have been updated - expect( - await page.getByLabel('Global Time Conductor').getByLabel('Start bounds').textContent() - ).toEqual(NEW_GLOBAL_START_BOUNDS); - expect( - await page.getByLabel('Global Time Conductor').getByLabel('End bounds').textContent() - ).toEqual(NEW_GLOBAL_END_BOUNDS); + await expect( + page.getByLabel(`Start bounds: ${NEW_GLOBAL_START_DATE} ${NEW_GLOBAL_START_TIME}.000Z`) + ).toBeVisible(); + await expect( + page.getByLabel(`End bounds: ${NEW_GLOBAL_END_DATE} ${NEW_GLOBAL_END_TIME}.000Z`) + ).toBeVisible(); //Save localStorage for future test execution await context.storageState({ diff --git a/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js b/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js index c3e920960c..224bc519ba 100644 --- a/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js +++ b/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js @@ -53,6 +53,9 @@ test.describe('Example Imagery Object', () => { // Verify that the created object is focused await expect(page.locator('.l-browse-bar__object-name')).toContainText(exampleImagery.name); await page.getByLabel('Focused Image Element').hover({ trial: true }); + + // Wait for image thumbnail auto-scroll to complete + await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport(); }); test('Can use Mouse Wheel to zoom in and out of latest image', async ({ page }) => { diff --git a/e2e/tests/functional/plugins/plot/logPlot.e2e.spec.js b/e2e/tests/functional/plugins/plot/logPlot.e2e.spec.js index 20e4443034..e327b3d093 100644 --- a/e2e/tests/functional/plugins/plot/logPlot.e2e.spec.js +++ b/e2e/tests/functional/plugins/plot/logPlot.e2e.spec.js @@ -25,7 +25,7 @@ Tests to verify log plot functionality. Note this test suite if very much under necessarily be used for reference when writing new tests in this area. */ -import { setTimeConductorBounds } from '../../../../appActions.js'; +import { createDomainObjectWithDefaults, setTimeConductorBounds } from '../../../../appActions.js'; import { expect, test } from '../../../../pluginFixtures.js'; test.describe('Log plot tests', () => { @@ -86,51 +86,36 @@ async function makeOverlayPlot(page, myItemsFolderName) { // Set a specific time range for consistency, otherwise it will change // on every test to a range based on the current time. - const start = '2022-03-29 22:00:00.000Z'; - const end = '2022-03-29 22:00:30.000Z'; + const startDate = '2022-03-29'; + const startTime = '22:00:00'; + const endDate = '2022-03-29'; + const endTime = '22:00:30'; - await setTimeConductorBounds(page, start, end); + await setTimeConductorBounds(page, { startDate, startTime, endDate, endTime }); - // create overlay plot - - await page.locator('button.c-create-button').click(); - await page.locator('li[role="menuitem"]:has-text("Overlay Plot")').click(); - // Click OK button and wait for Navigate event - await Promise.all([ - page.waitForLoadState(), - await page.getByRole('button', { name: 'Save' }).click(), - // Wait for Save Banner to appear - page.waitForSelector('.c-message-banner__message') - ]); - - // save the overlay plot - await saveOverlayPlot(page); + const overlayPlot = await createDomainObjectWithDefaults(page, { + type: 'Overlay Plot', + name: 'Unnamed Overlay Plot' + }); // create a sinewave generator + await createDomainObjectWithDefaults(page, { + type: 'Sine Wave Generator', + name: 'Unnamed Sine Wave Generator', + parent: overlayPlot.uuid + }); - await page.locator('button.c-create-button').click(); - await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click(); + await page.getByLabel('More actions').click(); + await page.getByLabel('Edit Properties...').click(); // set amplitude to 6, offset 4, data rate 2 hz + await page.getByLabel('Amplitude', { exact: true }).fill('6'); + await page.getByLabel('Offset', { exact: true }).fill('4'); + await page.getByLabel('Data Rate (hz)', { exact: true }).fill('2'); - await page.getByLabel('Amplitude').fill('6'); - await page.getByLabel('Offset').fill('4'); - await page.getByLabel('Data Rate (hz)').fill('2'); + await page.getByLabel('Save').click(); - // Click OK button and wait for Navigate event - await Promise.all([ - page.waitForLoadState(), - await page.getByRole('button', { name: 'Save' }).click(), - // Wait for Save Banner to appear - page.waitForSelector('.c-message-banner__message') - ]); - - // click on overlay plot - await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click(); - await Promise.all([ - page.waitForLoadState(), - page.locator('text=Unnamed Overlay Plot').first().click() - ]); + await page.goto(overlayPlot.url); } /** diff --git a/e2e/tests/functional/plugins/telemetryTable/telemetryTable.e2e.spec.js b/e2e/tests/functional/plugins/telemetryTable/telemetryTable.e2e.spec.js index 688bf50c84..3c94b06952 100644 --- a/e2e/tests/functional/plugins/telemetryTable/telemetryTable.e2e.spec.js +++ b/e2e/tests/functional/plugins/telemetryTable/telemetryTable.e2e.spec.js @@ -112,13 +112,14 @@ test.describe('Telemetry Table', () => { // Subtract 5 minutes from the current end bound datetime and set it // Bring up the time conductor popup - let endDate = await page.locator('[aria-label="End bounds"]').textContent(); - endDate = new Date(endDate); + let endTimeStamp = await page.getByLabel('End bounds').textContent(); + endTimeStamp = new Date(endTimeStamp); - endDate.setUTCMinutes(endDate.getUTCMinutes() - 5); - endDate = endDate.toISOString().replace(/T/, ' '); + endTimeStamp.setUTCMinutes(endTimeStamp.getUTCMinutes() - 5); + const endDate = endTimeStamp.toISOString().split('T')[0]; + const endTime = endTimeStamp.toISOString().split('T')[1]; - await setTimeConductorBounds(page, undefined, endDate); + await setTimeConductorBounds(page, { endDate, endTime }); await expect(tableWrapper).not.toHaveClass(/is-paused/); @@ -131,7 +132,7 @@ test.describe('Telemetry Table', () => { // Verify that it is <= our new end bound const latestMilliseconds = Date.parse(latestTelemetryDate); - const endBoundMilliseconds = Date.parse(endDate); + const endBoundMilliseconds = Date.parse(endTimeStamp); expect(latestMilliseconds).toBeLessThanOrEqual(endBoundMilliseconds); }); diff --git a/e2e/tests/functional/plugins/timeConductor/timeConductor.e2e.spec.js b/e2e/tests/functional/plugins/timeConductor/timeConductor.e2e.spec.js index 8d3822852f..a873b7c306 100644 --- a/e2e/tests/functional/plugins/timeConductor/timeConductor.e2e.spec.js +++ b/e2e/tests/functional/plugins/timeConductor/timeConductor.e2e.spec.js @@ -30,68 +30,80 @@ import { import { expect, test } from '../../../../pluginFixtures.js'; test.describe('Time conductor operations', () => { - test('validate start time does not exceeds end time', async ({ page }) => { + test('validate start time does not exceed end time', async ({ page }) => { // Go to baseURL await page.goto('./', { waitUntil: 'domcontentloaded' }); const year = new Date().getFullYear(); - let startDate = 'xxxx-01-01 01:00:00.000Z'; - startDate = year + startDate.substring(4); + // Set initial valid time bounds + const startDate = `${year}-01-01`; + const startTime = '01:00:00'; + const endDate = `${year}-01-01`; + const endTime = '02:00:00'; + await setTimeConductorBounds(page, { startDate, startTime, endDate, endTime }); - let endDate = 'xxxx-01-01 02:00:00.000Z'; - endDate = year + endDate.substring(4); + // Open the time conductor popup + await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click(); - await setTimeConductorBounds(page, startDate, endDate); + // Test invalid start date + const invalidStartDate = `${year}-01-02`; + await page.getByLabel('Start date').fill(invalidStartDate); + await expect(page.getByLabel('Submit time bounds')).toBeDisabled(); + await page.getByLabel('Start date').fill(startDate); + await expect(page.getByLabel('Submit time bounds')).toBeEnabled(); - // invalid start date - startDate = year + 1 + startDate.substring(4); - await setTimeConductorBounds(page, startDate); + // Test invalid end date + const invalidEndDate = `${year - 1}-12-31`; + await page.getByLabel('End date').fill(invalidEndDate); + await expect(page.getByLabel('Submit time bounds')).toBeDisabled(); + await page.getByLabel('End date').fill(endDate); + await expect(page.getByLabel('Submit time bounds')).toBeEnabled(); - // Bring up the time conductor popup - const timeConductorMode = page.locator('.c-compact-tc'); - await timeConductorMode.click(); - const startDateLocator = page.locator('input[type="text"]').first(); - const endDateLocator = page.locator('input[type="text"]').nth(2); + // Test invalid start time + const invalidStartTime = '42:00:00'; + await page.getByLabel('Start time').fill(invalidStartTime); + await expect(page.getByLabel('Submit time bounds')).toBeDisabled(); + await page.getByLabel('Start time').fill(startTime); + await expect(page.getByLabel('Submit time bounds')).toBeEnabled(); - await endDateLocator.click(); + // Test invalid end time + const invalidEndTime = '43:00:00'; + await page.getByLabel('End time').fill(invalidEndTime); + await expect(page.getByLabel('Submit time bounds')).toBeDisabled(); + await page.getByLabel('End time').fill(endTime); + await expect(page.getByLabel('Submit time bounds')).toBeEnabled(); - const startDateValidityStatus = await startDateLocator.evaluate((element) => - element.checkValidity() + // Submit valid time bounds + await page.getByLabel('Submit time bounds').click(); + + // Verify the submitted time bounds + await expect(page.getByLabel('Start bounds')).toHaveText( + new RegExp(`${startDate} ${startTime}.000Z`) ); - expect(startDateValidityStatus).not.toBeTruthy(); - - // fix to valid start date - startDate = year - 1 + startDate.substring(4); - await setTimeConductorBounds(page, startDate); - - // invalid end date - endDate = year - 2 + endDate.substring(4); - await setTimeConductorBounds(page, undefined, endDate); - - await startDateLocator.click(); - - const endDateValidityStatus = await endDateLocator.evaluate((element) => - element.checkValidity() + await expect(page.getByLabel('End bounds')).toHaveText( + new RegExp(`${endDate} ${endTime}.000Z`) ); - expect(endDateValidityStatus).not.toBeTruthy(); }); }); -// Testing instructions: -// Try to change the realtime offsets when in realtime (local clock) mode. -test.describe('Time conductor input fields real-time mode', () => { - test('validate input fields in real-time mode', async ({ page }) => { +test.describe('Global Time Conductor', () => { + test.beforeEach(async ({ page }) => { + await page.goto('./', { waitUntil: 'domcontentloaded' }); + }); + + test('Input field validation: real-time mode', async ({ page }) => { const startOffset = { + startHours: '01', + startMins: '29', startSecs: '23' }; const endOffset = { + endHours: '01', + endMins: '30', endSecs: '31' }; - // Go to baseURL - await page.goto('./', { waitUntil: 'domcontentloaded' }); - // Switch to real-time mode await setRealTimeMode(page); @@ -99,13 +111,95 @@ test.describe('Time conductor input fields real-time mode', () => { await setStartOffset(page, startOffset); // Verify time was updated on time offset button - await expect(page.locator('.c-compact-tc__setting-value.icon-minus')).toContainText('00:30:23'); + await expect(page.getByLabel('Start offset: 01:29:23')).toBeVisible(); // Set end time offset await setEndOffset(page, endOffset); // Verify time was updated on preceding time offset button - await expect(page.locator('.c-compact-tc__setting-value.icon-plus')).toContainText('00:00:31'); + await expect(page.getByLabel('End offset: 01:30:31')).toBeVisible(); + + // Discard changes and verify that offsets remain unchanged + await setStartOffset(page, { + startHours: '00', + startMins: '30', + startSecs: '00', + submitChanges: false + }); + + await expect(page.getByLabel('Start offset: 01:29:23')).toBeVisible(); + await expect(page.getByLabel('End offset: 01:30:31')).toBeVisible(); + }); + + test('Input field validation: fixed time mode', async ({ page }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/nasa/openmct/issues/7791' + }); + // Switch to fixed time mode + await setFixedTimeMode(page); + + // Define valid time bounds for testing + const validBounds = { + startDate: '2024-04-20', + startTime: '00:04:20', + endDate: '2024-04-20', + endTime: '16:04:20' + }; + // Set valid time conductor bounds ✌️ + await setTimeConductorBounds(page, validBounds); + + // Verify that the time bounds are set correctly + await expect(page.getByLabel(`Start bounds: 2024-04-20 00:04:20.000Z`)).toBeVisible(); + await expect(page.getByLabel(`End bounds: 2024-04-20 16:04:20.000Z`)).toBeVisible(); + + // Open the Time Conductor Mode popup + await page.getByLabel('Time Conductor Mode').click(); + + // Test invalid start date + const invalidStartDate = '2024-04-21'; + await page.getByLabel('Start date').fill(invalidStartDate); + await expect(page.getByLabel('Submit time bounds')).toBeDisabled(); + await page.getByLabel('Start date').fill(validBounds.startDate); + await expect(page.getByLabel('Submit time bounds')).toBeEnabled(); + + // Test invalid end date + const invalidEndDate = '2024-04-19'; + await page.getByLabel('End date').fill(invalidEndDate); + await expect(page.getByLabel('Submit time bounds')).toBeDisabled(); + await page.getByLabel('End date').fill(validBounds.endDate); + await expect(page.getByLabel('Submit time bounds')).toBeEnabled(); + + // Test invalid start time + const invalidStartTime = '16:04:21'; + await page.getByLabel('Start time').fill(invalidStartTime); + await expect(page.getByLabel('Submit time bounds')).toBeDisabled(); + await page.getByLabel('Start time').fill(validBounds.startTime); + await expect(page.getByLabel('Submit time bounds')).toBeEnabled(); + + // Test invalid end time + const invalidEndTime = '00:04:19'; + await page.getByLabel('End time').fill(invalidEndTime); + await expect(page.getByLabel('Submit time bounds')).toBeDisabled(); + await page.getByLabel('End time').fill(validBounds.endTime); + await expect(page.getByLabel('Submit time bounds')).toBeEnabled(); + + // Verify that the time bounds remain unchanged after invalid inputs + await expect(page.getByLabel(`Start bounds: 2024-04-20 00:04:20.000Z`)).toBeVisible(); + await expect(page.getByLabel(`End bounds: 2024-04-20 16:04:20.000Z`)).toBeVisible(); + + // Discard changes and verify that bounds remain unchanged + await setTimeConductorBounds(page, { + startDate: validBounds.startDate, + startTime: '04:20:00', + endDate: validBounds.endDate, + endTime: '04:20:20', + submitChanges: false + }); + + // Verify that the original time bounds are still displayed after discarding changes + await expect(page.getByLabel(`Start bounds: 2024-04-20 00:04:20.000Z`)).toBeVisible(); + await expect(page.getByLabel(`End bounds: 2024-04-20 16:04:20.000Z`)).toBeVisible(); }); /** @@ -147,14 +241,15 @@ test.describe('Time conductor input fields real-time mode', () => { await setRealTimeMode(page); // Verify updated start time offset persists after mode switch - await expect(page.locator('.c-compact-tc__setting-value.icon-minus')).toContainText('00:30:23'); + await expect(page.getByLabel('Start offset: 00:30:23')).toBeVisible(); // Verify updated end time offset persists after mode switch - await expect(page.locator('.c-compact-tc__setting-value.icon-plus')).toContainText('00:00:01'); + await expect(page.getByLabel('End offset: 00:00:01')).toBeVisible(); // Verify url parameters persist after mode switch - expect(page.url()).toContain(`startDelta=${startDelta}`); - expect(page.url()).toContain(`endDelta=${endDelta}`); + // eslint-disable-next-line no-useless-escape + const urlRegex = new RegExp(`.*tc\.startDelta=${startDelta}&tc\.endDelta=${endDelta}.*`); + await page.waitForURL(urlRegex); }); test.fixme( diff --git a/e2e/tests/visual-a11y/imagery.visual.spec.js b/e2e/tests/visual-a11y/imagery.visual.spec.js index c09e4a0c3e..b063dee0f7 100644 --- a/e2e/tests/visual-a11y/imagery.visual.spec.js +++ b/e2e/tests/visual-a11y/imagery.visual.spec.js @@ -64,6 +64,10 @@ test.describe('Visual - Example Imagery', () => { test('Example Imagery in Fixed Time', async ({ page, theme }) => { await page.goto(exampleImagery.url, { waitUntil: 'domcontentloaded' }); + // Wait for the thumbnails to finish their scroll animation + // (Wait until the rightmost thumbnail is in view) + await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport(); + await expect(page.getByLabel('Image Wrapper')).toBeVisible(); await percySnapshot(page, `Example Imagery in Fixed Time (theme: ${theme})`); @@ -76,6 +80,9 @@ test.describe('Visual - Example Imagery', () => { test('Example Imagery in Real Time', async ({ page, theme }) => { await page.goto(exampleImagery.url, { waitUntil: 'domcontentloaded' }); + // Wait for the thumbnails to finish their scroll animation + // (Wait until the rightmost thumbnail is in view) + await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport(); await setRealTimeMode(page, true); await expect(page.getByLabel('Image Wrapper')).toBeVisible(); diff --git a/src/plugins/timeConductor/ConductorInputsFixed.vue b/src/plugins/timeConductor/ConductorInputsFixed.vue index 8911b91817..ee5309f25f 100644 --- a/src/plugins/timeConductor/ConductorInputsFixed.vue +++ b/src/plugins/timeConductor/ConductorInputsFixed.vue @@ -32,7 +32,7 @@
{{ formattedBounds.start }}
@@ -40,7 +40,7 @@
{{ formattedBounds.end }}
diff --git a/src/plugins/timeConductor/TimePopupFixed.vue b/src/plugins/timeConductor/TimePopupFixed.vue index 74c5c2fb86..7c098e4462 100644 --- a/src/plugins/timeConductor/TimePopupFixed.vue +++ b/src/plugins/timeConductor/TimePopupFixed.vue @@ -88,7 +88,7 @@ @@ -119,25 +119,21 @@ export default { }, emits: ['update', 'dismiss'], data() { - let timeSystem = this.openmct.time.getTimeSystem(); - let durationFormatter = this.getFormatter( - timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER - ); - let timeFormatter = this.getFormatter(timeSystem.timeFormat); - let bounds = this.bounds || this.openmct.time.getBounds(); + const timeSystem = this.openmct.time.getTimeSystem(); + const bounds = this.openmct.time.getBounds(); return { - timeFormatter, - durationFormatter, + timeFormatter: this.getFormatter(timeSystem.timeFormat), + durationFormatter: this.getFormatter(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER), bounds: { start: bounds.start, end: bounds.end }, formattedBounds: { - start: timeFormatter.format(bounds.start).split(' ')[0], - end: timeFormatter.format(bounds.end).split(' ')[0], - startTime: durationFormatter.format(Math.abs(bounds.start)), - endTime: durationFormatter.format(Math.abs(bounds.end)) + start: '', + end: '', + startTime: '', + endTime: '' }, isUTCBased: timeSystem.isUTCBased, isDisabled: false @@ -157,9 +153,12 @@ export default { deep: true } }, - mounted() { + created() { this.handleNewBounds = _.throttle(this.handleNewBounds, 300); + }, + mounted() { this.setTimeSystem(JSON.parse(JSON.stringify(this.openmct.time.getTimeSystem()))); + this.setViewFromBounds(this.bounds); }, beforeUnmount() { this.clearAllValidation(); @@ -173,8 +172,10 @@ export default { [this.$refs.startDate, this.$refs.endDate].forEach(this.clearValidationForInput); }, clearValidationForInput(input) { - input.setCustomValidity(''); - input.title = ''; + if (input) { + input.setCustomValidity(''); + input.title = ''; + } }, setBounds(bounds) { this.bounds = bounds; @@ -186,7 +187,6 @@ export default { this.formattedBounds.endTime = this.durationFormatter.format(Math.abs(bounds.end)); }, setTimeSystem(timeSystem) { - this.timeSystem = timeSystem; this.timeFormatter = this.getFormatter(timeSystem.timeFormat); this.durationFormatter = this.getFormatter( timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER @@ -207,39 +207,31 @@ export default { `${this.formattedBounds.end} ${this.formattedBounds.endTime}` ); - this.$emit('update', { - start: start, - end: end - }); + this.$emit('update', { start, end }); } if (dismiss) { this.$emit('dismiss'); - return false; } }, handleFormSubmission(shouldDismiss) { - // Validate bounds before submission this.validateAllBounds('startDate'); this.validateAllBounds('endDate'); - // Submit the form if it's valid if (!this.isDisabled) { this.setBoundsFromView(shouldDismiss); } }, validateAllBounds(ref) { - this.isDisabled = false; // Reset isDisabled at the start of validation + this.isDisabled = false; if (!this.areBoundsFormatsValid()) { this.isDisabled = true; return false; } - let validationResult = { - valid: true - }; + let validationResult = { valid: true }; const currentInput = this.$refs[ref]; return [this.$refs.startDate, this.$refs.endDate].every((input) => { @@ -255,38 +247,30 @@ export default { // const limit = this.getBoundsLimit(); const limit = false; - if (this.timeSystem.isUTCBased && limit && boundsValues.end - boundsValues.start > limit) { + if (this.isUTCBased && limit && boundsValues.end - boundsValues.start > limit) { if (input === currentInput) { validationResult = { valid: false, message: 'Start and end difference exceeds allowable limit' }; } - } else { - if (input === currentInput) { - validationResult = this.openmct.time.validateBounds(boundsValues); - } + } else if (input === currentInput) { + validationResult = this.openmct.time.validateBounds(boundsValues); } return this.handleValidationResults(input, validationResult); }); }, areBoundsFormatsValid() { - let validationResult = { - valid: true - }; - return [this.$refs.startDate, this.$refs.endDate].every((input) => { const formattedDate = input === this.$refs.startDate ? `${this.formattedBounds.start} ${this.formattedBounds.startTime}` : `${this.formattedBounds.end} ${this.formattedBounds.endTime}`; - if (!this.timeFormatter.validate(formattedDate)) { - validationResult = { - valid: false, - message: 'Invalid date' - }; - } + + const validationResult = this.timeFormatter.validate(formattedDate) + ? { valid: true } + : { valid: false, message: 'Invalid date' }; return this.handleValidationResults(input, validationResult); });