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`
This commit is contained in:
Jesse Mazzella 2024-07-25 16:55:50 -07:00 committed by GitHub
parent 689f7cc815
commit e3fcbe1a35
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 302 additions and 172 deletions

View File

@ -436,61 +436,67 @@ async function setRealTimeMode(page) {
/** /**
* Set the values (hours, mins, secs) for the TimeConductor offsets when in realtime mode * Set the values (hours, mins, secs) for the TimeConductor offsets when in realtime mode
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
* @param {OffsetValues} offset * @param {OffsetValues} offset - Object containing offset values
* @param {import('@playwright/test').Locator} offsetButton * @param {boolean} [offset.submitChanges=true] - If true, submit the offset changes; otherwise, discard them
*/ */
async function setTimeConductorOffset( async function setTimeConductorOffset(
page, page,
{ startHours, startMins, startSecs, endHours, endMins, endSecs } { startHours, startMins, startSecs, endHours, endMins, endSecs, submitChanges = true }
) { ) {
if (startHours) { if (startHours) {
await page.getByRole('spinbutton', { name: 'Start offset hours' }).fill(startHours); await page.getByLabel('Start offset hours').fill(startHours);
} }
if (startMins) { if (startMins) {
await page.getByRole('spinbutton', { name: 'Start offset minutes' }).fill(startMins); await page.getByLabel('Start offset minutes').fill(startMins);
} }
if (startSecs) { if (startSecs) {
await page.getByRole('spinbutton', { name: 'Start offset seconds' }).fill(startSecs); await page.getByLabel('Start offset seconds').fill(startSecs);
} }
if (endHours) { if (endHours) {
await page.getByRole('spinbutton', { name: 'End offset hours' }).fill(endHours); await page.getByLabel('End offset hours').fill(endHours);
} }
if (endMins) { if (endMins) {
await page.getByRole('spinbutton', { name: 'End offset minutes' }).fill(endMins); await page.getByLabel('End offset minutes').fill(endMins);
} }
if (endSecs) { if (endSecs) {
await page.getByRole('spinbutton', { name: 'End offset seconds' }).fill(endSecs); await page.getByLabel('End offset seconds').fill(endSecs);
} }
// Click the check button // 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 * Set the values (hours, mins, secs) for the start time offset when in realtime mode
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
* @param {OffsetValues} offset * @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 // Click 'mode' button
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click(); 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 * Set the values (hours, mins, secs) for the end time offset when in realtime mode
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
* @param {OffsetValues} offset * @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 // Click 'mode' button
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click(); 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 * 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()`. * navigate directly to the object with the desired time bounds using `navigateToObjectWithFixedTimeBounds()`.
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
* @param {string} startDate * @param {Object} bounds - The time conductor bounds
* @param {string} endDate * @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) { async function setTimeConductorBounds(page, { submitChanges = true, ...bounds }) {
// Bring up the time conductor popup const { startDate, endDate, startTime, endTime } = bounds;
expect(await page.locator('.l-shell__time-conductor.c-compact-tc').count()).toBe(1);
await page.click('.l-shell__time-conductor.c-compact-tc');
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();
}
} }
/** /**

View File

@ -24,14 +24,18 @@ import {
createDomainObjectWithDefaults, createDomainObjectWithDefaults,
createNotification, createNotification,
expandEntireTree, expandEntireTree,
openObjectTreeContextMenu openObjectTreeContextMenu,
setFixedTimeMode,
setRealTimeMode,
setTimeConductorBounds
} from '../../appActions.js'; } from '../../appActions.js';
import { expect, test } from '../../pluginFixtures.js'; import { expect, test } from '../../pluginFixtures.js';
test.describe('AppActions', () => { test.describe('AppActions', () => {
test('createDomainObjectsWithDefaults', async ({ page }) => { test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' }); await page.goto('./', { waitUntil: 'domcontentloaded' });
});
test('createDomainObjectsWithDefaults', async ({ page }) => {
const e2eFolder = await createDomainObjectWithDefaults(page, { const e2eFolder = await createDomainObjectWithDefaults(page, {
type: 'Folder', type: 'Folder',
name: 'e2e folder' name: 'e2e folder'
@ -91,7 +95,6 @@ test.describe('AppActions', () => {
}); });
}); });
test('createNotification', async ({ page }) => { test('createNotification', async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
await createNotification(page, { await createNotification(page, {
message: 'Test info notification', message: 'Test info notification',
severity: 'info' severity: 'info'
@ -115,8 +118,6 @@ test.describe('AppActions', () => {
await page.locator('[aria-label="Dismiss"]').click(); await page.locator('[aria-label="Dismiss"]').click();
}); });
test('expandEntireTree', async ({ page }) => { test('expandEntireTree', async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
const rootFolder = await createDomainObjectWithDefaults(page, { const rootFolder = await createDomainObjectWithDefaults(page, {
type: 'Folder' type: 'Folder'
}); });
@ -168,12 +169,30 @@ test.describe('AppActions', () => {
expect(await locatorTreeCollapsedItems.count()).toBe(0); expect(await locatorTreeCollapsedItems.count()).toBe(0);
}); });
test('openObjectTreeContextMenu', async ({ page }) => { test('openObjectTreeContextMenu', async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
const folder = await createDomainObjectWithDefaults(page, { const folder = await createDomainObjectWithDefaults(page, {
type: 'Folder' type: 'Folder'
}); });
await openObjectTreeContextMenu(page, folder.url); await openObjectTreeContextMenu(page, folder.url);
await expect(page.getByLabel(`${folder.name} Context Menu`)).toBeVisible(); 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();
});
}); });

View File

@ -117,18 +117,25 @@ test.describe('Generate Visual Test Data @localStorage @generatedata @clock', ()
end: '2024-11-12 20:11:11.000Z' end: '2024-11-12 20:11:11.000Z'
}); });
const NEW_GLOBAL_START_BOUNDS = '2024-11-11 19:11:11.000Z'; const NEW_GLOBAL_START_DATE = '2024-11-11';
const NEW_GLOBAL_END_BOUNDS = '2024-11-11 20:11:11.000Z'; 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 // Verify that the global time conductor bounds have been updated
expect( await expect(
await page.getByLabel('Global Time Conductor').getByLabel('Start bounds').textContent() page.getByLabel(`Start bounds: ${NEW_GLOBAL_START_DATE} ${NEW_GLOBAL_START_TIME}.000Z`)
).toEqual(NEW_GLOBAL_START_BOUNDS); ).toBeVisible();
expect( await expect(
await page.getByLabel('Global Time Conductor').getByLabel('End bounds').textContent() page.getByLabel(`End bounds: ${NEW_GLOBAL_END_DATE} ${NEW_GLOBAL_END_TIME}.000Z`)
).toEqual(NEW_GLOBAL_END_BOUNDS); ).toBeVisible();
//Save localStorage for future test execution //Save localStorage for future test execution
await context.storageState({ await context.storageState({

View File

@ -53,6 +53,9 @@ test.describe('Example Imagery Object', () => {
// Verify that the created object is focused // Verify that the created object is focused
await expect(page.locator('.l-browse-bar__object-name')).toContainText(exampleImagery.name); await expect(page.locator('.l-browse-bar__object-name')).toContainText(exampleImagery.name);
await page.getByLabel('Focused Image Element').hover({ trial: true }); 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 }) => { test('Can use Mouse Wheel to zoom in and out of latest image', async ({ page }) => {

View File

@ -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. 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'; import { expect, test } from '../../../../pluginFixtures.js';
test.describe('Log plot tests', () => { 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 // Set a specific time range for consistency, otherwise it will change
// on every test to a range based on the current time. // on every test to a range based on the current time.
const start = '2022-03-29 22:00:00.000Z'; const startDate = '2022-03-29';
const end = '2022-03-29 22:00:30.000Z'; 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 const overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot',
await page.locator('button.c-create-button').click(); name: 'Unnamed Overlay Plot'
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);
// create a sinewave generator // 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.getByLabel('More actions').click();
await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click(); await page.getByLabel('Edit Properties...').click();
// set amplitude to 6, offset 4, data rate 2 hz // 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('Save').click();
await page.getByLabel('Offset').fill('4');
await page.getByLabel('Data Rate (hz)').fill('2');
// Click OK button and wait for Navigate event await page.goto(overlayPlot.url);
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()
]);
} }
/** /**

View File

@ -112,13 +112,14 @@ test.describe('Telemetry Table', () => {
// Subtract 5 minutes from the current end bound datetime and set it // Subtract 5 minutes from the current end bound datetime and set it
// Bring up the time conductor popup // Bring up the time conductor popup
let endDate = await page.locator('[aria-label="End bounds"]').textContent(); let endTimeStamp = await page.getByLabel('End bounds').textContent();
endDate = new Date(endDate); endTimeStamp = new Date(endTimeStamp);
endDate.setUTCMinutes(endDate.getUTCMinutes() - 5); endTimeStamp.setUTCMinutes(endTimeStamp.getUTCMinutes() - 5);
endDate = endDate.toISOString().replace(/T/, ' '); 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/); await expect(tableWrapper).not.toHaveClass(/is-paused/);
@ -131,7 +132,7 @@ test.describe('Telemetry Table', () => {
// Verify that it is <= our new end bound // Verify that it is <= our new end bound
const latestMilliseconds = Date.parse(latestTelemetryDate); const latestMilliseconds = Date.parse(latestTelemetryDate);
const endBoundMilliseconds = Date.parse(endDate); const endBoundMilliseconds = Date.parse(endTimeStamp);
expect(latestMilliseconds).toBeLessThanOrEqual(endBoundMilliseconds); expect(latestMilliseconds).toBeLessThanOrEqual(endBoundMilliseconds);
}); });

View File

@ -30,68 +30,80 @@ import {
import { expect, test } from '../../../../pluginFixtures.js'; import { expect, test } from '../../../../pluginFixtures.js';
test.describe('Time conductor operations', () => { 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 // Go to baseURL
await page.goto('./', { waitUntil: 'domcontentloaded' }); await page.goto('./', { waitUntil: 'domcontentloaded' });
const year = new Date().getFullYear(); const year = new Date().getFullYear();
let startDate = 'xxxx-01-01 01:00:00.000Z'; // Set initial valid time bounds
startDate = year + startDate.substring(4); 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'; // Open the time conductor popup
endDate = year + endDate.substring(4); 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 // Test invalid end date
startDate = year + 1 + startDate.substring(4); const invalidEndDate = `${year - 1}-12-31`;
await setTimeConductorBounds(page, startDate); 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 // Test invalid start time
const timeConductorMode = page.locator('.c-compact-tc'); const invalidStartTime = '42:00:00';
await timeConductorMode.click(); await page.getByLabel('Start time').fill(invalidStartTime);
const startDateLocator = page.locator('input[type="text"]').first(); await expect(page.getByLabel('Submit time bounds')).toBeDisabled();
const endDateLocator = page.locator('input[type="text"]').nth(2); 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) => // Submit valid time bounds
element.checkValidity() 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(); await expect(page.getByLabel('End bounds')).toHaveText(
new RegExp(`${endDate} ${endTime}.000Z`)
// 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()
); );
expect(endDateValidityStatus).not.toBeTruthy();
}); });
}); });
// Testing instructions: test.describe('Global Time Conductor', () => {
// Try to change the realtime offsets when in realtime (local clock) mode. test.beforeEach(async ({ page }) => {
test.describe('Time conductor input fields real-time mode', () => { await page.goto('./', { waitUntil: 'domcontentloaded' });
test('validate input fields in real-time mode', async ({ page }) => { });
test('Input field validation: real-time mode', async ({ page }) => {
const startOffset = { const startOffset = {
startHours: '01',
startMins: '29',
startSecs: '23' startSecs: '23'
}; };
const endOffset = { const endOffset = {
endHours: '01',
endMins: '30',
endSecs: '31' endSecs: '31'
}; };
// Go to baseURL
await page.goto('./', { waitUntil: 'domcontentloaded' });
// Switch to real-time mode // Switch to real-time mode
await setRealTimeMode(page); await setRealTimeMode(page);
@ -99,13 +111,95 @@ test.describe('Time conductor input fields real-time mode', () => {
await setStartOffset(page, startOffset); await setStartOffset(page, startOffset);
// Verify time was updated on time offset button // 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 // Set end time offset
await setEndOffset(page, endOffset); await setEndOffset(page, endOffset);
// Verify time was updated on preceding time offset button // 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); await setRealTimeMode(page);
// Verify updated start time offset persists after mode switch // 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 // 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 // Verify url parameters persist after mode switch
expect(page.url()).toContain(`startDelta=${startDelta}`); // eslint-disable-next-line no-useless-escape
expect(page.url()).toContain(`endDelta=${endDelta}`); const urlRegex = new RegExp(`.*tc\.startDelta=${startDelta}&tc\.endDelta=${endDelta}.*`);
await page.waitForURL(urlRegex);
}); });
test.fixme( test.fixme(

View File

@ -64,6 +64,10 @@ test.describe('Visual - Example Imagery', () => {
test('Example Imagery in Fixed Time', async ({ page, theme }) => { test('Example Imagery in Fixed Time', async ({ page, theme }) => {
await page.goto(exampleImagery.url, { waitUntil: 'domcontentloaded' }); 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 expect(page.getByLabel('Image Wrapper')).toBeVisible();
await percySnapshot(page, `Example Imagery in Fixed Time (theme: ${theme})`); 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 }) => { test('Example Imagery in Real Time', async ({ page, theme }) => {
await page.goto(exampleImagery.url, { waitUntil: 'domcontentloaded' }); 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 setRealTimeMode(page, true);
await expect(page.getByLabel('Image Wrapper')).toBeVisible(); await expect(page.getByLabel('Image Wrapper')).toBeVisible();

View File

@ -32,7 +32,7 @@
<div <div
class="c-compact-tc__setting-value u-fade-truncate--lg --no-sep" class="c-compact-tc__setting-value u-fade-truncate--lg --no-sep"
:title="`Start bounds: ${formattedBounds.start}`" :title="`Start bounds: ${formattedBounds.start}`"
aria-label="Start bounds" :aria-label="`Start bounds: ${formattedBounds.start}`"
> >
{{ formattedBounds.start }} {{ formattedBounds.start }}
</div> </div>
@ -40,7 +40,7 @@
<div <div
class="c-compact-tc__setting-value u-fade-truncate--lg --no-sep" class="c-compact-tc__setting-value u-fade-truncate--lg --no-sep"
:title="`End bounds: ${formattedBounds.end}`" :title="`End bounds: ${formattedBounds.end}`"
aria-label="End bounds" :aria-label="`End bounds: ${formattedBounds.end}`"
> >
{{ formattedBounds.end }} {{ formattedBounds.end }}
</div> </div>

View File

@ -88,7 +88,7 @@
<button <button
class="c-button icon-x" class="c-button icon-x"
aria-label="Discard changes and close time popup" aria-label="Discard changes and close time popup"
@click.prevent="handleFormSubmission(true)" @click.prevent="hide"
></button> ></button>
</div> </div>
</div> </div>
@ -119,25 +119,21 @@ export default {
}, },
emits: ['update', 'dismiss'], emits: ['update', 'dismiss'],
data() { data() {
let timeSystem = this.openmct.time.getTimeSystem(); const timeSystem = this.openmct.time.getTimeSystem();
let durationFormatter = this.getFormatter( const bounds = this.openmct.time.getBounds();
timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER
);
let timeFormatter = this.getFormatter(timeSystem.timeFormat);
let bounds = this.bounds || this.openmct.time.getBounds();
return { return {
timeFormatter, timeFormatter: this.getFormatter(timeSystem.timeFormat),
durationFormatter, durationFormatter: this.getFormatter(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER),
bounds: { bounds: {
start: bounds.start, start: bounds.start,
end: bounds.end end: bounds.end
}, },
formattedBounds: { formattedBounds: {
start: timeFormatter.format(bounds.start).split(' ')[0], start: '',
end: timeFormatter.format(bounds.end).split(' ')[0], end: '',
startTime: durationFormatter.format(Math.abs(bounds.start)), startTime: '',
endTime: durationFormatter.format(Math.abs(bounds.end)) endTime: ''
}, },
isUTCBased: timeSystem.isUTCBased, isUTCBased: timeSystem.isUTCBased,
isDisabled: false isDisabled: false
@ -157,9 +153,12 @@ export default {
deep: true deep: true
} }
}, },
mounted() { created() {
this.handleNewBounds = _.throttle(this.handleNewBounds, 300); this.handleNewBounds = _.throttle(this.handleNewBounds, 300);
},
mounted() {
this.setTimeSystem(JSON.parse(JSON.stringify(this.openmct.time.getTimeSystem()))); this.setTimeSystem(JSON.parse(JSON.stringify(this.openmct.time.getTimeSystem())));
this.setViewFromBounds(this.bounds);
}, },
beforeUnmount() { beforeUnmount() {
this.clearAllValidation(); this.clearAllValidation();
@ -173,8 +172,10 @@ export default {
[this.$refs.startDate, this.$refs.endDate].forEach(this.clearValidationForInput); [this.$refs.startDate, this.$refs.endDate].forEach(this.clearValidationForInput);
}, },
clearValidationForInput(input) { clearValidationForInput(input) {
if (input) {
input.setCustomValidity(''); input.setCustomValidity('');
input.title = ''; input.title = '';
}
}, },
setBounds(bounds) { setBounds(bounds) {
this.bounds = bounds; this.bounds = bounds;
@ -186,7 +187,6 @@ export default {
this.formattedBounds.endTime = this.durationFormatter.format(Math.abs(bounds.end)); this.formattedBounds.endTime = this.durationFormatter.format(Math.abs(bounds.end));
}, },
setTimeSystem(timeSystem) { setTimeSystem(timeSystem) {
this.timeSystem = timeSystem;
this.timeFormatter = this.getFormatter(timeSystem.timeFormat); this.timeFormatter = this.getFormatter(timeSystem.timeFormat);
this.durationFormatter = this.getFormatter( this.durationFormatter = this.getFormatter(
timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER
@ -207,39 +207,31 @@ export default {
`${this.formattedBounds.end} ${this.formattedBounds.endTime}` `${this.formattedBounds.end} ${this.formattedBounds.endTime}`
); );
this.$emit('update', { this.$emit('update', { start, end });
start: start,
end: end
});
} }
if (dismiss) { if (dismiss) {
this.$emit('dismiss'); this.$emit('dismiss');
return false; return false;
} }
}, },
handleFormSubmission(shouldDismiss) { handleFormSubmission(shouldDismiss) {
// Validate bounds before submission
this.validateAllBounds('startDate'); this.validateAllBounds('startDate');
this.validateAllBounds('endDate'); this.validateAllBounds('endDate');
// Submit the form if it's valid
if (!this.isDisabled) { if (!this.isDisabled) {
this.setBoundsFromView(shouldDismiss); this.setBoundsFromView(shouldDismiss);
} }
}, },
validateAllBounds(ref) { validateAllBounds(ref) {
this.isDisabled = false; // Reset isDisabled at the start of validation this.isDisabled = false;
if (!this.areBoundsFormatsValid()) { if (!this.areBoundsFormatsValid()) {
this.isDisabled = true; this.isDisabled = true;
return false; return false;
} }
let validationResult = { let validationResult = { valid: true };
valid: true
};
const currentInput = this.$refs[ref]; const currentInput = this.$refs[ref];
return [this.$refs.startDate, this.$refs.endDate].every((input) => { return [this.$refs.startDate, this.$refs.endDate].every((input) => {
@ -255,38 +247,30 @@ export default {
// const limit = this.getBoundsLimit(); // const limit = this.getBoundsLimit();
const limit = false; 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) { if (input === currentInput) {
validationResult = { validationResult = {
valid: false, valid: false,
message: 'Start and end difference exceeds allowable limit' message: 'Start and end difference exceeds allowable limit'
}; };
} }
} else { } else if (input === currentInput) {
if (input === currentInput) {
validationResult = this.openmct.time.validateBounds(boundsValues); validationResult = this.openmct.time.validateBounds(boundsValues);
} }
}
return this.handleValidationResults(input, validationResult); return this.handleValidationResults(input, validationResult);
}); });
}, },
areBoundsFormatsValid() { areBoundsFormatsValid() {
let validationResult = {
valid: true
};
return [this.$refs.startDate, this.$refs.endDate].every((input) => { return [this.$refs.startDate, this.$refs.endDate].every((input) => {
const formattedDate = const formattedDate =
input === this.$refs.startDate input === this.$refs.startDate
? `${this.formattedBounds.start} ${this.formattedBounds.startTime}` ? `${this.formattedBounds.start} ${this.formattedBounds.startTime}`
: `${this.formattedBounds.end} ${this.formattedBounds.endTime}`; : `${this.formattedBounds.end} ${this.formattedBounds.endTime}`;
if (!this.timeFormatter.validate(formattedDate)) {
validationResult = { const validationResult = this.timeFormatter.validate(formattedDate)
valid: false, ? { valid: true }
message: 'Invalid date' : { valid: false, message: 'Invalid date' };
};
}
return this.handleValidationResults(input, validationResult); return this.handleValidationResults(input, validationResult);
}); });