mirror of
https://github.com/nasa/openmct.git
synced 2025-01-18 02:39:56 +00:00
Timelist bug fixes (#6661)
* Ensure timelist scrolling happens correctly for clock as well as fixed time * If an activity has already started, show the duration as time to/since the end of the activity * Addresses review comments: Reverse +/- indicators, removes milliseconds from times. * Fix linting issues * Add e2e test for timelist display * Scroll to 'now' if available
This commit is contained in:
parent
4c5de37cff
commit
022dffd419
122
e2e/tests/functional/planning/timelist.e2e.spec.js
Normal file
122
e2e/tests/functional/planning/timelist.e2e.spec.js
Normal file
@ -0,0 +1,122 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2023, 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.
|
||||
*****************************************************************************/
|
||||
|
||||
const { test, expect } = require('../../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults, createPlanFromJSON } = require('../../../appActions');
|
||||
|
||||
const testPlan = {
|
||||
TEST_GROUP: [
|
||||
{
|
||||
name: 'Past event 1',
|
||||
start: 1660320408000,
|
||||
end: 1660343797000,
|
||||
type: 'TEST-GROUP',
|
||||
color: 'orange',
|
||||
textColor: 'white'
|
||||
},
|
||||
{
|
||||
name: 'Past event 2',
|
||||
start: 1660406808000,
|
||||
end: 1660429160000,
|
||||
type: 'TEST-GROUP',
|
||||
color: 'orange',
|
||||
textColor: 'white'
|
||||
},
|
||||
{
|
||||
name: 'Past event 3',
|
||||
start: 1660493208000,
|
||||
end: 1660503981000,
|
||||
type: 'TEST-GROUP',
|
||||
color: 'orange',
|
||||
textColor: 'white'
|
||||
},
|
||||
{
|
||||
name: 'Past event 4',
|
||||
start: 1660579608000,
|
||||
end: 1660624108000,
|
||||
type: 'TEST-GROUP',
|
||||
color: 'orange',
|
||||
textColor: 'white'
|
||||
},
|
||||
{
|
||||
name: 'Past event 5',
|
||||
start: 1660666008000,
|
||||
end: 1660681529000,
|
||||
type: 'TEST-GROUP',
|
||||
color: 'orange',
|
||||
textColor: 'white'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
test.describe('Time List', () => {
|
||||
test('Create a Time List, add a single Plan to it and verify all the activities are displayed with no milliseconds', async ({
|
||||
page
|
||||
}) => {
|
||||
// Goto baseURL
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
const timelist = await test.step('Create a Time List', async () => {
|
||||
const createdTimeList = await createDomainObjectWithDefaults(page, { type: 'Time List' });
|
||||
const objectName = await page.locator('.l-browse-bar__object-name').innerText();
|
||||
expect(objectName).toBe(createdTimeList.name);
|
||||
|
||||
return createdTimeList;
|
||||
});
|
||||
|
||||
await test.step('Create a Plan and add it to the timelist', async () => {
|
||||
const createdPlan = await createPlanFromJSON(page, {
|
||||
name: 'Test Plan',
|
||||
json: testPlan
|
||||
});
|
||||
|
||||
await page.goto(timelist.url);
|
||||
// Expand the tree to show the plan
|
||||
await page.click("button[title='Show selected item in tree']");
|
||||
await page.dragAndDrop(`role=treeitem[name=/${createdPlan.name}/]`, '.c-object-view');
|
||||
await page.click("button[title='Save']");
|
||||
await page.click("li[title='Save and Finish Editing']");
|
||||
const startBound = testPlan.TEST_GROUP[0].start;
|
||||
const endBound = testPlan.TEST_GROUP[testPlan.TEST_GROUP.length - 1].end;
|
||||
|
||||
// Switch to fixed time mode with all plan events within the bounds
|
||||
await page.goto(
|
||||
`${timelist.url}?tc.mode=fixed&tc.startBound=${startBound}&tc.endBound=${endBound}&tc.timeSystem=utc&view=timelist.view`
|
||||
);
|
||||
|
||||
// Verify all events are displayed
|
||||
const eventCount = await page.locator('.js-list-item').count();
|
||||
expect(eventCount).toEqual(testPlan.TEST_GROUP.length);
|
||||
});
|
||||
|
||||
await test.step('Does not show milliseconds in times', async () => {
|
||||
// Get the first activity
|
||||
const row = await page.locator('.js-list-item').first();
|
||||
// Verify that none fo the times have milliseconds displayed.
|
||||
// Example: 2024-11-17T16:00:00Z is correct and 2024-11-17T16:00:00.000Z is wrong
|
||||
|
||||
await expect(row.locator('.--start')).not.toContainText('.');
|
||||
await expect(row.locator('.--end')).not.toContainText('.');
|
||||
await expect(row.locator('.--duration')).not.toContainText('.');
|
||||
});
|
||||
});
|
||||
});
|
@ -41,8 +41,10 @@ import moment from 'moment';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
const SCROLL_TIMEOUT = 10000;
|
||||
const ROW_HEIGHT = 30;
|
||||
const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss:SSS';
|
||||
const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss';
|
||||
const CURRENT_CSS_SUFFIX = '--is-current';
|
||||
const PAST_CSS_SUFFIX = '--is-past';
|
||||
const FUTURE_CSS_SUFFIX = '--is-future';
|
||||
const headerItems = [
|
||||
{
|
||||
defaultDirection: true,
|
||||
@ -79,9 +81,9 @@ const headerItems = [
|
||||
format: function (value) {
|
||||
let result;
|
||||
if (value < 0) {
|
||||
result = `-${getPreciseDuration(Math.abs(value))}`;
|
||||
result = `+${getPreciseDuration(Math.abs(value), true)}`;
|
||||
} else if (value > 0) {
|
||||
result = `+${getPreciseDuration(value)}`;
|
||||
result = `-${getPreciseDuration(value, true)}`;
|
||||
} else {
|
||||
result = 'Now';
|
||||
}
|
||||
@ -360,11 +362,12 @@ export default {
|
||||
groups.forEach((key) => {
|
||||
activities = activities.concat(this.planData[key]);
|
||||
});
|
||||
activities = activities.filter(this.filterActivities);
|
||||
// filter activities first, then sort by start time
|
||||
activities = activities.filter(this.filterActivities).sort(this.sortByStartTime);
|
||||
activities = this.applyStyles(activities);
|
||||
this.setScrollTop();
|
||||
// sort by start time
|
||||
this.planActivities = activities.sort(this.sortByStartTime);
|
||||
this.planActivities = activities;
|
||||
//We need to wait for the next tick since we need the height of the row from the DOM
|
||||
this.$nextTick(this.setScrollTop);
|
||||
},
|
||||
updateTimeStampAndListActivities(time) {
|
||||
this.timestamp = time;
|
||||
@ -410,30 +413,41 @@ export default {
|
||||
},
|
||||
applyStyles(activities) {
|
||||
let firstCurrentActivityIndex = -1;
|
||||
let activityClosestToNowIndex = -1;
|
||||
let currentActivitiesCount = 0;
|
||||
const styledActivities = activities.map((activity, index) => {
|
||||
if (this.timestamp >= activity.start && this.timestamp <= activity.end) {
|
||||
activity.cssClass = '--is-current';
|
||||
activity.cssClass = CURRENT_CSS_SUFFIX;
|
||||
if (firstCurrentActivityIndex < 0) {
|
||||
firstCurrentActivityIndex = index;
|
||||
}
|
||||
|
||||
currentActivitiesCount = currentActivitiesCount + 1;
|
||||
} else if (this.timestamp < activity.start) {
|
||||
activity.cssClass = '--is-future';
|
||||
activity.cssClass = FUTURE_CSS_SUFFIX;
|
||||
//the index of the first activity that's greater than the current timestamp
|
||||
if (activityClosestToNowIndex < 0) {
|
||||
activityClosestToNowIndex = index;
|
||||
}
|
||||
} else {
|
||||
activity.cssClass = '--is-past';
|
||||
activity.cssClass = PAST_CSS_SUFFIX;
|
||||
}
|
||||
|
||||
if (!activity.key) {
|
||||
activity.key = uuid();
|
||||
}
|
||||
|
||||
activity.duration = activity.start - this.timestamp;
|
||||
if (activity.start < this.timestamp) {
|
||||
//if the activity start time has passed, display the time to the end of the activity
|
||||
activity.duration = activity.end - this.timestamp;
|
||||
} else {
|
||||
activity.duration = activity.start - this.timestamp;
|
||||
}
|
||||
|
||||
return activity;
|
||||
});
|
||||
|
||||
this.activityClosestToNowIndex = activityClosestToNowIndex;
|
||||
this.firstCurrentActivityIndex = firstCurrentActivityIndex;
|
||||
this.currentActivitiesCount = currentActivitiesCount;
|
||||
|
||||
@ -451,13 +465,22 @@ export default {
|
||||
}
|
||||
|
||||
this.firstCurrentActivityIndex = -1;
|
||||
this.activityClosestToNowIndex = -1;
|
||||
this.currentActivitiesCount = 0;
|
||||
this.$el.parentElement?.scrollTo({ top: 0 });
|
||||
this.autoScrolled = false;
|
||||
},
|
||||
setScrollTop() {
|
||||
//scroll to somewhere mid-way of the current activities
|
||||
if (this.firstCurrentActivityIndex > -1) {
|
||||
//The view isn't ready yet
|
||||
if (!this.$el.parentElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const row = this.$el.querySelector('.js-list-item');
|
||||
if (row && this.firstCurrentActivityIndex > -1) {
|
||||
// scroll to somewhere mid-way of the current activities
|
||||
const ROW_HEIGHT = row.getBoundingClientRect().height;
|
||||
|
||||
if (this.canAutoScroll() === false) {
|
||||
return;
|
||||
}
|
||||
@ -469,7 +492,22 @@ export default {
|
||||
behavior: 'smooth'
|
||||
});
|
||||
this.autoScrolled = false;
|
||||
} else if (row && this.activityClosestToNowIndex > -1) {
|
||||
// scroll to somewhere close to 'now'
|
||||
|
||||
const ROW_HEIGHT = row.getBoundingClientRect().height;
|
||||
|
||||
if (this.canAutoScroll() === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$el.parentElement.scrollTo({
|
||||
top: ROW_HEIGHT * (this.activityClosestToNowIndex - 1),
|
||||
behavior: 'smooth'
|
||||
});
|
||||
this.autoScrolled = false;
|
||||
} else {
|
||||
// scroll to the top
|
||||
this.resetScroll();
|
||||
}
|
||||
},
|
||||
|
@ -219,10 +219,10 @@ describe('the plugin', function () {
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua'
|
||||
);
|
||||
expect(itemValues[0].innerHTML.trim()).toEqual(
|
||||
`${moment(twoHoursPast).format('YYYY-MM-DD HH:mm:ss:SSS')}Z`
|
||||
`${moment(twoHoursPast).format('YYYY-MM-DD HH:mm:ss')}Z`
|
||||
);
|
||||
expect(itemValues[1].innerHTML.trim()).toEqual(
|
||||
`${moment(oneHourPast).format('YYYY-MM-DD HH:mm:ss:SSS')}Z`
|
||||
`${moment(oneHourPast).format('YYYY-MM-DD HH:mm:ss')}Z`
|
||||
);
|
||||
|
||||
done();
|
||||
|
@ -63,14 +63,16 @@ export function millisecondsToDHMS(numericDuration) {
|
||||
return `${dhms ? '+' : ''} ${dhms}`;
|
||||
}
|
||||
|
||||
export function getPreciseDuration(value) {
|
||||
export function getPreciseDuration(value, excludeMilliSeconds) {
|
||||
const ms = value || 0;
|
||||
|
||||
return [
|
||||
const duration = [
|
||||
toDoubleDigits(Math.floor(normalizeAge(ms / ONE_DAY))),
|
||||
toDoubleDigits(Math.floor(normalizeAge((ms % ONE_DAY) / ONE_HOUR))),
|
||||
toDoubleDigits(Math.floor(normalizeAge((ms % ONE_HOUR) / ONE_MINUTE))),
|
||||
toDoubleDigits(Math.floor(normalizeAge((ms % ONE_MINUTE) / ONE_SECOND))),
|
||||
toTripleDigits(Math.floor(normalizeAge(ms % ONE_SECOND)))
|
||||
].join(':');
|
||||
toDoubleDigits(Math.floor(normalizeAge((ms % ONE_MINUTE) / ONE_SECOND)))
|
||||
];
|
||||
if (!excludeMilliSeconds) {
|
||||
duration.push(toTripleDigits(Math.floor(normalizeAge(ms % ONE_SECOND))));
|
||||
}
|
||||
return duration.join(':');
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user