From d2df694c406ef81293266d06eb5fef64cbcc09e6 Mon Sep 17 00:00:00 2001 From: Shefali Date: Wed, 1 Nov 2023 10:57:44 -0700 Subject: [PATCH 01/13] Change the centering algorithm for timelist Make duration formatting better --- src/plugins/timelist/TimelistComponent.vue | 83 +++++++++++++--------- src/utils/duration.js | 23 +++++- 2 files changed, 70 insertions(+), 36 deletions(-) diff --git a/src/plugins/timelist/TimelistComponent.vue b/src/plugins/timelist/TimelistComponent.vue index 66a22be8717..13c75f7f82c 100644 --- a/src/plugins/timelist/TimelistComponent.vue +++ b/src/plugins/timelist/TimelistComponent.vue @@ -77,9 +77,9 @@ const headerItems = [ format: function (value) { let result; if (value < 0) { - result = `+${getPreciseDuration(Math.abs(value), true)}`; + result = `+${getPreciseDuration(Math.abs(value), true, true)}`; } else if (value > 0) { - result = `-${getPreciseDuration(value, true)}`; + result = `-${getPreciseDuration(value, true, true)}`; } else { result = 'Now'; } @@ -350,8 +350,10 @@ export default { }, applyStyles(activities) { let firstCurrentActivityIndex = -1; - let activityClosestToNowIndex = -1; + let firstFutureActivityIndex = -1; let currentActivitiesCount = 0; + let pastActivitiesCount = 0; + let futureActivitiesCount = 0; const styledActivities = activities.map((activity, index) => { if (this.timestamp >= activity.start && this.timestamp <= activity.end) { activity.cssClass = CURRENT_CSS_SUFFIX; @@ -363,11 +365,13 @@ export default { } else if (this.timestamp < activity.start) { activity.cssClass = FUTURE_CSS_SUFFIX; //the index of the first activity that's greater than the current timestamp - if (activityClosestToNowIndex < 0) { - activityClosestToNowIndex = index; + if (firstFutureActivityIndex < 0) { + firstFutureActivityIndex = index; } + futureActivitiesCount = futureActivitiesCount + 1; } else { activity.cssClass = PAST_CSS_SUFFIX; + pastActivitiesCount = pastActivitiesCount + 1; } if (!activity.key) { @@ -384,9 +388,14 @@ export default { return activity; }); - this.activityClosestToNowIndex = activityClosestToNowIndex; - this.firstCurrentActivityIndex = firstCurrentActivityIndex; + if (firstCurrentActivityIndex > -1) { + this.firstCurrentOrFutureActivityIndex = firstCurrentActivityIndex; + } else if (firstFutureActivityIndex > -1) { + this.firstCurrentOrFutureActivityIndex = firstFutureActivityIndex; + } this.currentActivitiesCount = currentActivitiesCount; + this.pastActivitiesCount = pastActivitiesCount; + this.futureActivitiesCount = futureActivitiesCount; return styledActivities; }, @@ -401,9 +410,10 @@ export default { return; } - this.firstCurrentActivityIndex = -1; - this.activityClosestToNowIndex = -1; + this.firstCurrentOrFutureActivityIndex = -1; + this.pastActivitiesCount = 0; this.currentActivitiesCount = 0; + this.futureActivitiesCount = 0; this.$el.parentElement?.scrollTo({ top: 0 }); this.autoScrolled = false; }, @@ -413,40 +423,47 @@ export default { 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; + // See #7167 for scrolling algorithm + const scrollTop = this.calculateScrollOffset(); - if (this.canAutoScroll() === false) { - return; - } - - const scrollOffset = - this.currentActivitiesCount > 0 ? Math.floor(this.currentActivitiesCount / 2) : 0; + if (scrollTop === undefined) { + this.resetScroll(); + } else { this.$el.parentElement?.scrollTo({ - top: ROW_HEIGHT * (this.firstCurrentActivityIndex + scrollOffset), + top: scrollTop, behavior: 'smooth' }); this.autoScrolled = false; - } else if (row && this.activityClosestToNowIndex > -1) { - // scroll to somewhere close to 'now' + } + }, + calculateScrollOffset() { + let scrollTop; + //No scrolling necessary if no past events are present + if (this.pastActivitiesCount > 0) { + const row = this.$el.querySelector('.js-list-item'); const ROW_HEIGHT = row.getBoundingClientRect().height; - if (this.canAutoScroll() === false) { - return; - } + const maxViewableActivities = + Math.floor(this.$el.parentElement.getBoundingClientRect().height / ROW_HEIGHT) - 1; - this.$el.parentElement.scrollTo({ - top: ROW_HEIGHT * (this.activityClosestToNowIndex - 1), - behavior: 'smooth' - }); - this.autoScrolled = false; - } else { - // scroll to the top - this.resetScroll(); + const currentAndFutureActivities = this.currentActivitiesCount + this.futureActivitiesCount; + + //If there is more viewable area than all current and future activities combined, then show some past events + const numberOfPastEventsToShow = maxViewableActivities - currentAndFutureActivities; + if (numberOfPastEventsToShow > 0) { + //some past events can be shown - get that scroll index + if (this.pastActivitiesCount > numberOfPastEventsToShow) { + scrollTop = + ROW_HEIGHT * (this.firstCurrentOrFutureActivityIndex + numberOfPastEventsToShow); + } + } else { + // only show current and future events + scrollTop = ROW_HEIGHT * this.firstCurrentOrFutureActivityIndex; + } } + + return scrollTop; }, deferAutoScroll() { //if this is not a user-triggered event, don't defer auto scrolling diff --git a/src/utils/duration.js b/src/utils/duration.js index 5aeafc0ff23..8e93297f24d 100644 --- a/src/utils/duration.js +++ b/src/utils/duration.js @@ -63,10 +63,12 @@ export function millisecondsToDHMS(numericDuration) { return `${dhms ? '+' : ''} ${dhms}`; } -export function getPreciseDuration(value, excludeMilliSeconds) { +export function getPreciseDuration(value, excludeMilliSeconds, useDayFormat) { + let preciseDuration; const ms = value || 0; + const duration = [ - toDoubleDigits(Math.floor(normalizeAge(ms / ONE_DAY))), + 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))) @@ -74,5 +76,20 @@ export function getPreciseDuration(value, excludeMilliSeconds) { if (!excludeMilliSeconds) { duration.push(toTripleDigits(Math.floor(normalizeAge(ms % ONE_SECOND)))); } - return duration.join(':'); + + if (useDayFormat) { + // Format days as XD + const days = duration.shift(); + if (days > 0) { + preciseDuration = `${days}D ${duration.join(':')}`; + } else { + preciseDuration = duration.join(':'); + } + } else { + const days = toDoubleDigits(duration.shift()); + duration.unshift(days); + preciseDuration = duration.join(':'); + } + + return preciseDuration; } From c5713f31c61ebef0f2ab1ecbe7fb7b59cb551f13 Mon Sep 17 00:00:00 2001 From: Jesse Mazzella Date: Mon, 6 Nov 2023 17:29:52 -0800 Subject: [PATCH 02/13] test: add time list countdown and countup test --- e2e/constants.js | 5 +- e2e/helper/planningUtils.js | 24 ++- .../examplePlans/ExamplePlan_Small3.json | 38 ++++ .../functional/planning/timelist.e2e.spec.js | 181 +++++++++++++++++- e2e/tests/visual/planning.visual.spec.js | 12 +- 5 files changed, 241 insertions(+), 19 deletions(-) create mode 100644 e2e/test-data/examplePlans/ExamplePlan_Small3.json diff --git a/e2e/constants.js b/e2e/constants.js index d5da651bcac..2ec8207085d 100644 --- a/e2e/constants.js +++ b/e2e/constants.js @@ -11,8 +11,9 @@ export const MISSION_TIME = 1732413600000; // Saturday, November 23, 2024 6:00:0 /** * URL Constants - * - This is the URL that the browser will be directed to when running visual tests. This URL + * - This is the URL that the browser will be directed to when running visual tests. This URL * - hides the tree and inspector to prevent visual noise * - sets the time bounds to a fixed range */ -export const VISUAL_URL = './#/browse/mine?tc.mode=fixed&tc.startBound=1693592063607&tc.endBound=1693593893607&tc.timeSystem=utc&view=grid&hideInspector=true&hideTree=true'; +export const VISUAL_FIXED_URL = + './#/browse/mine?tc.mode=fixed&tc.startBound=1693592063607&tc.endBound=1693593893607&tc.timeSystem=utc&view=grid&hideInspector=true&hideTree=true'; diff --git a/e2e/helper/planningUtils.js b/e2e/helper/planningUtils.js index 3c5513e2185..1ae0f5fb273 100644 --- a/e2e/helper/planningUtils.js +++ b/e2e/helper/planningUtils.js @@ -89,17 +89,35 @@ function activitiesWithinTimeBounds(start1, end1, start2, end2) { * @param {string} planObjectUrl */ export async function setBoundsToSpanAllActivities(page, planJson, planObjectUrl) { - const activities = Object.values(planJson).flat(); // Get the earliest start value - const start = Math.min(...activities.map((activity) => activity.start)); + const start = getEarliestStartTime(planJson); // Get the latest end value - const end = Math.max(...activities.map((activity) => activity.end)); + const end = getLatestEndTime(planJson); // Set the start and end bounds to the earliest start and latest end await page.goto( `${planObjectUrl}?tc.mode=fixed&tc.startBound=${start}&tc.endBound=${end}&tc.timeSystem=utc&view=plan.view` ); } +/** + * @param {object} planJson + * @returns {number} + */ +export function getEarliestStartTime(planJson) { + const activities = Object.values(planJson).flat(); + return Math.min(...activities.map((activity) => activity.start)); +} + +/** + * + * @param {object} planJson + * @returns {number} + */ +export function getLatestEndTime(planJson) { + const activities = Object.values(planJson).flat(); + return Math.max(...activities.map((activity) => activity.end)); +} + /** * Uses the Open MCT API to set the status of a plan to 'draft'. * @param {import('@playwright/test').Page} page diff --git a/e2e/test-data/examplePlans/ExamplePlan_Small3.json b/e2e/test-data/examplePlans/ExamplePlan_Small3.json new file mode 100644 index 00000000000..2304cf708b8 --- /dev/null +++ b/e2e/test-data/examplePlans/ExamplePlan_Small3.json @@ -0,0 +1,38 @@ +{ + "Group 1": [ + { + "name": "Time until birthday", + "start": 1650320402000, + "end": 1660343797000, + "type": "Group 1", + "color": "orange", + "textColor": "white" + }, + { + "name": "Time until supper", + "start": 1650320402000, + "end": 1650420410000, + "type": "Group 2", + "color": "blue", + "textColor": "white" + } + ], + "Group 2": [ + { + "name": "Time since the last time I ate", + "start": 1650320102001, + "end": 1650320102001, + "type": "Group 2", + "color": "green", + "textColor": "white" + }, + { + "name": "Time since last accident", + "start": 1650320102002, + "end": 1650320102002, + "type": "Group 1", + "color": "yellow", + "textColor": "white" + } + ] +} diff --git a/e2e/tests/functional/planning/timelist.e2e.spec.js b/e2e/tests/functional/planning/timelist.e2e.spec.js index 65802cf92f8..7dbe5e34eb7 100644 --- a/e2e/tests/functional/planning/timelist.e2e.spec.js +++ b/e2e/tests/functional/planning/timelist.e2e.spec.js @@ -22,6 +22,15 @@ const { test, expect } = require('../../../pluginFixtures'); const { createDomainObjectWithDefaults, createPlanFromJSON } = require('../../../appActions'); +const { getEarliestStartTime } = require('../../../helper/planningUtils'); +const examplePlanSmall3 = require('../../../test-data/examplePlans/ExamplePlan_Small3.json'); + +const START_TIME_COLUMN = 0; +const END_TIME_COLUMN = 1; +const TIME_TO_FROM_COLUMN = 2; +const ACTIVITY_COLUMN = 3; +const HEADER_ROW = 0; +const NUM_COLUMNS = 4; const testPlan = { TEST_GROUP: [ @@ -84,22 +93,17 @@ test.describe('Time List', () => { }); await test.step('Create a Plan and add it to the timelist', async () => { - const createdPlan = await createPlanFromJSON(page, { + await createPlanFromJSON(page, { name: 'Test Plan', - json: testPlan + json: testPlan, + parent: timelist.uuid }); 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; - await page.goto(timelist.url); - // 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` @@ -122,3 +126,162 @@ test.describe('Time List', () => { }); }); }); + +/** + * The regular expression used to parse the countdown string. + * Some examples of valid Countdown strings: + * ``` + * '35D 02:03:04' + * '-1D 01:02:03' + * '01:02:03' + * '-05:06:07' + * ``` + */ +const COUNTDOWN_REGEXP = /(-)?(\d+D\s)?(\d{2}):(\d{2}):(\d{2})/; + +/** + * @typedef {Object} CountdownObject + * @property {string} sign - The sign of the countdown ('-' if the countdown is negative, otherwise undefined). + * @property {string} days - The number of days in the countdown (undefined if there are no days). + * @property {string} hours - The number of hours in the countdown. + * @property {string} minutes - The number of minutes in the countdown. + * @property {string} seconds - The number of seconds in the countdown. + * @property {string} toString - The countdown string. + */ + +/** + * Object representing the indices of the capture groups in a countdown regex match. + * + * @typedef {{ SIGN: number, DAYS: number, HOURS: number, MINUTES: number, SECONDS: number, REGEXP: RegExp }} + * @property {number} SIGN - The index for the sign capture group (1 if a '-' sign is present, otherwise undefined). + * @property {number} DAYS - The index for the days capture group (2 for the number of days, otherwise undefined). + * @property {number} HOURS - The index for the hours capture group (3 for the hour part of the time). + * @property {number} MINUTES - The index for the minutes capture group (4 for the minute part of the time). + * @property {number} SECONDS - The index for the seconds capture group (5 for the second part of the time). + */ +const COUNTDOWN = Object.freeze({ + SIGN: 1, + DAYS: 2, + HOURS: 3, + MINUTES: 4, + SECONDS: 5 +}); + +test.describe('Time List with controlled clock', () => { + test.use({ + clockOptions: { + now: getEarliestStartTime(examplePlanSmall3), + shouldAdvanceTime: true + } + }); + test.beforeEach(async ({ page }) => { + await page.goto('./', { waitUntil: 'domcontentloaded' }); + }); + test('Time List shows current events and counts down correctly in real-time mode', async ({ + page + }) => { + await test.step('Create a Time List, add a Plan to it, and switch to real-time mode', async () => { + // Create Time List + const timelist = await createDomainObjectWithDefaults(page, { + type: 'Time List' + }); + + // Create a Plan with events that count down and up. + // Add it as a child to the Time List. + await createPlanFromJSON(page, { + json: examplePlanSmall3, + parent: timelist.uuid + }); + + // Navigate to the Time List in real-time mode + await page.goto( + `${timelist.url}?tc.mode=local&tc.startDelta=900000&tc.endDelta=1800000&tc.timeSystem=utc&view=grid` + ); + }); + + const countUpCells = [ + getCell(page, 1, TIME_TO_FROM_COLUMN), + getCell(page, 2, TIME_TO_FROM_COLUMN) + ]; + const countdownCells = [ + getCell(page, 3, TIME_TO_FROM_COLUMN), + getCell(page, 4, TIME_TO_FROM_COLUMN) + ]; + + // Verify that the countdown cells are counting down + for (let i = 0; i < countdownCells.length; i++) { + await test.step(`Countdown cell ${i + 1} counts down`, async () => { + const countdownCell = countdownCells[i]; + // Get the initial countdown timestamp object + const beforeCountdown = await getCountdownObject(page, i + 3); + // Wait until it changes + await expect(countdownCell).not.toHaveText(beforeCountdown.toString()); + // Get the new countdown timestamp object + const afterCountdown = await getCountdownObject(page, i + 3); + // Verify that the new countdown timestamp object is less than the old one + expect(Number(afterCountdown.seconds)).toBeLessThan(Number(beforeCountdown.seconds)); + }); + } + + // Verify that the countup cells are counting up + for (let i = 0; i < countUpCells.length; i++) { + await test.step(`Countup cell ${i + 1} counts up`, async () => { + const countdownCell = countUpCells[i]; + // Get the initial countup timestamp object + const beforeCountdown = await getCountdownObject(page, i + 1); + // Wait until it changes + await expect(countdownCell).not.toHaveText(beforeCountdown.toString()); + // Get the new countup timestamp object + const afterCountdown = await getCountdownObject(page, i + 1); + // Verify that the new countup timestamp object is greater than the old one + expect(Number(afterCountdown.seconds)).toBeGreaterThan(Number(beforeCountdown.seconds)); + }); + } + }); +}); + +/** + * Get the cell at the given row and column indices. + * @param {import('@playwright/test').Page} page + * @param {number} rowIndex + * @param {number} columnIndex + * @returns {import('@playwright/test').Locator} cell + */ +function getCell(page, rowIndex, columnIndex) { + return page.getByRole('cell').nth(rowIndex * NUM_COLUMNS + columnIndex); +} + +/** + * Return the innerText of the cell at the given row and column indices. + * @param {import('@playwright/test').Page} page + * @param {number} rowIndex + * @param {number} columnIndex + * @returns {Promise} text + */ +async function getCellText(page, rowIndex, columnIndex) { + const text = await getCell(page, rowIndex, columnIndex).innerText(); + return text; +} + +/** + * Get the text from the countdown cell in the given row, assert that it matches the countdown + * regex, and return an object representing the countdown. + * @param {import('@playwright/test').Page} page + * @param {number} rowIndex the row index + * @returns {Promise} countdownObject + */ +async function getCountdownObject(page, rowIndex) { + const timeToFrom = await getCellText(page, HEADER_ROW + rowIndex, TIME_TO_FROM_COLUMN); + + expect(timeToFrom).toMatch(COUNTDOWN_REGEXP); + const match = timeToFrom.match(COUNTDOWN_REGEXP); + + return { + sign: match[COUNTDOWN.SIGN], + days: match[COUNTDOWN.DAYS], + hours: match[COUNTDOWN.HOURS], + minutes: match[COUNTDOWN.MINUTES], + seconds: match[COUNTDOWN.SECONDS], + toString: () => timeToFrom + }; +} diff --git a/e2e/tests/visual/planning.visual.spec.js b/e2e/tests/visual/planning.visual.spec.js index 1ff914d440b..cc3b3644735 100644 --- a/e2e/tests/visual/planning.visual.spec.js +++ b/e2e/tests/visual/planning.visual.spec.js @@ -23,18 +23,20 @@ const { test } = require('../../pluginFixtures'); const { setBoundsToSpanAllActivities, - setDraftStatusForPlan + setDraftStatusForPlan, + getEarliestStartTime } = require('../../helper/planningUtils'); const { createDomainObjectWithDefaults, createPlanFromJSON } = require('../../appActions'); const percySnapshot = require('@percy/playwright'); -const VISUAL_URL = require('../../constants').VISUAL_URL; +const VISUAL_FIXED_URL = require('../../constants').VISUAL_FIXED_URL; const examplePlanSmall = require('../../test-data/examplePlans/ExamplePlan_Small2.json'); +const examplePlanSmall3 = require('../../test-data/examplePlans/ExamplePlan_Small3.json'); const snapshotScope = '.l-shell__pane-main .l-pane__contents'; test.describe('Visual - Planning', () => { test.beforeEach(async ({ page }) => { - await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' }); + await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' }); }); test('Plan View', async ({ page, theme }) => { @@ -54,7 +56,7 @@ test.describe('Visual - Planning', () => { name: 'Plan Visual Test (Draft)', json: examplePlanSmall }); - await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' }); + await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' }); await setDraftStatusForPlan(page, plan); await setBoundsToSpanAllActivities(page, examplePlanSmall, plan.url); @@ -90,7 +92,7 @@ test.describe('Visual - Planning', () => { await setDraftStatusForPlan(page, plan); - await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' }); + await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' }); await setBoundsToSpanAllActivities(page, examplePlanSmall, ganttChart.url); await percySnapshot(page, `Gantt Chart View w/ draft status (theme: ${theme})`, { From cc824e066fd9d2640a2a32569891f66300e5fd22 Mon Sep 17 00:00:00 2001 From: Jesse Mazzella Date: Tue, 7 Nov 2023 16:22:00 -0800 Subject: [PATCH 03/13] test: respond to comments --- .../functional/planning/timelist.e2e.spec.js | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/e2e/tests/functional/planning/timelist.e2e.spec.js b/e2e/tests/functional/planning/timelist.e2e.spec.js index 7dbe5e34eb7..120602797d1 100644 --- a/e2e/tests/functional/planning/timelist.e2e.spec.js +++ b/e2e/tests/functional/planning/timelist.e2e.spec.js @@ -25,9 +25,12 @@ const { createDomainObjectWithDefaults, createPlanFromJSON } = require('../../.. const { getEarliestStartTime } = require('../../../helper/planningUtils'); const examplePlanSmall3 = require('../../../test-data/examplePlans/ExamplePlan_Small3.json'); +// eslint-disable-next-line no-unused-vars const START_TIME_COLUMN = 0; +// eslint-disable-next-line no-unused-vars const END_TIME_COLUMN = 1; const TIME_TO_FROM_COLUMN = 2; +// eslint-disable-next-line no-unused-vars const ACTIVITY_COLUMN = 3; const HEADER_ROW = 0; const NUM_COLUMNS = 4; @@ -200,12 +203,12 @@ test.describe('Time List with controlled clock', () => { }); const countUpCells = [ - getCell(page, 1, TIME_TO_FROM_COLUMN), - getCell(page, 2, TIME_TO_FROM_COLUMN) + getCellByIndex(page, 1, TIME_TO_FROM_COLUMN), + getCellByIndex(page, 2, TIME_TO_FROM_COLUMN) ]; const countdownCells = [ - getCell(page, 3, TIME_TO_FROM_COLUMN), - getCell(page, 4, TIME_TO_FROM_COLUMN) + getCellByIndex(page, 3, TIME_TO_FROM_COLUMN), + getCellByIndex(page, 4, TIME_TO_FROM_COLUMN) ]; // Verify that the countdown cells are counting down @@ -213,27 +216,27 @@ test.describe('Time List with controlled clock', () => { await test.step(`Countdown cell ${i + 1} counts down`, async () => { const countdownCell = countdownCells[i]; // Get the initial countdown timestamp object - const beforeCountdown = await getCountdownObject(page, i + 3); + const beforeCountdown = await getAndAssertCountdownObject(page, i + 3); // Wait until it changes await expect(countdownCell).not.toHaveText(beforeCountdown.toString()); // Get the new countdown timestamp object - const afterCountdown = await getCountdownObject(page, i + 3); + const afterCountdown = await getAndAssertCountdownObject(page, i + 3); // Verify that the new countdown timestamp object is less than the old one expect(Number(afterCountdown.seconds)).toBeLessThan(Number(beforeCountdown.seconds)); }); } - // Verify that the countup cells are counting up + // Verify that the count-up cells are counting up for (let i = 0; i < countUpCells.length; i++) { - await test.step(`Countup cell ${i + 1} counts up`, async () => { + await test.step(`Count-up cell ${i + 1} counts up`, async () => { const countdownCell = countUpCells[i]; // Get the initial countup timestamp object - const beforeCountdown = await getCountdownObject(page, i + 1); + const beforeCountdown = await getAndAssertCountdownObject(page, i + 1); // Wait until it changes await expect(countdownCell).not.toHaveText(beforeCountdown.toString()); - // Get the new countup timestamp object - const afterCountdown = await getCountdownObject(page, i + 1); - // Verify that the new countup timestamp object is greater than the old one + // Get the new count-up timestamp object + const afterCountdown = await getAndAssertCountdownObject(page, i + 1); + // Verify that the new count-up timestamp object is greater than the old one expect(Number(afterCountdown.seconds)).toBeGreaterThan(Number(beforeCountdown.seconds)); }); } @@ -247,7 +250,7 @@ test.describe('Time List with controlled clock', () => { * @param {number} columnIndex * @returns {import('@playwright/test').Locator} cell */ -function getCell(page, rowIndex, columnIndex) { +function getCellByIndex(page, rowIndex, columnIndex) { return page.getByRole('cell').nth(rowIndex * NUM_COLUMNS + columnIndex); } @@ -258,8 +261,8 @@ function getCell(page, rowIndex, columnIndex) { * @param {number} columnIndex * @returns {Promise} text */ -async function getCellText(page, rowIndex, columnIndex) { - const text = await getCell(page, rowIndex, columnIndex).innerText(); +async function getCellTextByIndex(page, rowIndex, columnIndex) { + const text = await getCellByIndex(page, rowIndex, columnIndex).innerText(); return text; } @@ -270,8 +273,8 @@ async function getCellText(page, rowIndex, columnIndex) { * @param {number} rowIndex the row index * @returns {Promise} countdownObject */ -async function getCountdownObject(page, rowIndex) { - const timeToFrom = await getCellText(page, HEADER_ROW + rowIndex, TIME_TO_FROM_COLUMN); +async function getAndAssertCountdownObject(page, rowIndex) { + const timeToFrom = await getCellTextByIndex(page, HEADER_ROW + rowIndex, TIME_TO_FROM_COLUMN); expect(timeToFrom).toMatch(COUNTDOWN_REGEXP); const match = timeToFrom.match(COUNTDOWN_REGEXP); From 755054ea1f7ec7c39ad1083c01d025cc2c4f3a75 Mon Sep 17 00:00:00 2001 From: Jesse Mazzella Date: Tue, 7 Nov 2023 16:23:06 -0800 Subject: [PATCH 04/13] chore: lint fix --- e2e/tests/functional/planning/timelist.e2e.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/tests/functional/planning/timelist.e2e.spec.js b/e2e/tests/functional/planning/timelist.e2e.spec.js index 120602797d1..56daa8cb6f1 100644 --- a/e2e/tests/functional/planning/timelist.e2e.spec.js +++ b/e2e/tests/functional/planning/timelist.e2e.spec.js @@ -230,7 +230,7 @@ test.describe('Time List with controlled clock', () => { for (let i = 0; i < countUpCells.length; i++) { await test.step(`Count-up cell ${i + 1} counts up`, async () => { const countdownCell = countUpCells[i]; - // Get the initial countup timestamp object + // Get the initial count-up timestamp object const beforeCountdown = await getAndAssertCountdownObject(page, i + 1); // Wait until it changes await expect(countdownCell).not.toHaveText(beforeCountdown.toString()); From c13b3754ec373ae9454d49d1b37879295f533e68 Mon Sep 17 00:00:00 2001 From: Jesse Mazzella Date: Tue, 7 Nov 2023 16:31:26 -0800 Subject: [PATCH 05/13] fix: lint errors and visual suite failures --- e2e/constants.js | 2 +- e2e/tests/visual/planning.visual.spec.js | 12 +++++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/e2e/constants.js b/e2e/constants.js index 2ec8207085d..cd513b12175 100644 --- a/e2e/constants.js +++ b/e2e/constants.js @@ -15,5 +15,5 @@ export const MISSION_TIME = 1732413600000; // Saturday, November 23, 2024 6:00:0 * - hides the tree and inspector to prevent visual noise * - sets the time bounds to a fixed range */ -export const VISUAL_FIXED_URL = +export const VISUAL_URL = './#/browse/mine?tc.mode=fixed&tc.startBound=1693592063607&tc.endBound=1693593893607&tc.timeSystem=utc&view=grid&hideInspector=true&hideTree=true'; diff --git a/e2e/tests/visual/planning.visual.spec.js b/e2e/tests/visual/planning.visual.spec.js index cc3b3644735..1ff914d440b 100644 --- a/e2e/tests/visual/planning.visual.spec.js +++ b/e2e/tests/visual/planning.visual.spec.js @@ -23,20 +23,18 @@ const { test } = require('../../pluginFixtures'); const { setBoundsToSpanAllActivities, - setDraftStatusForPlan, - getEarliestStartTime + setDraftStatusForPlan } = require('../../helper/planningUtils'); const { createDomainObjectWithDefaults, createPlanFromJSON } = require('../../appActions'); const percySnapshot = require('@percy/playwright'); -const VISUAL_FIXED_URL = require('../../constants').VISUAL_FIXED_URL; +const VISUAL_URL = require('../../constants').VISUAL_URL; const examplePlanSmall = require('../../test-data/examplePlans/ExamplePlan_Small2.json'); -const examplePlanSmall3 = require('../../test-data/examplePlans/ExamplePlan_Small3.json'); const snapshotScope = '.l-shell__pane-main .l-pane__contents'; test.describe('Visual - Planning', () => { test.beforeEach(async ({ page }) => { - await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' }); + await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' }); }); test('Plan View', async ({ page, theme }) => { @@ -56,7 +54,7 @@ test.describe('Visual - Planning', () => { name: 'Plan Visual Test (Draft)', json: examplePlanSmall }); - await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' }); + await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' }); await setDraftStatusForPlan(page, plan); await setBoundsToSpanAllActivities(page, examplePlanSmall, plan.url); @@ -92,7 +90,7 @@ test.describe('Visual - Planning', () => { await setDraftStatusForPlan(page, plan); - await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' }); + await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' }); await setBoundsToSpanAllActivities(page, examplePlanSmall, ganttChart.url); await percySnapshot(page, `Gantt Chart View w/ draft status (theme: ${theme})`, { From cc7223b316b4f100d47e68b46db4a8a84465b0a3 Mon Sep 17 00:00:00 2001 From: Shefali Date: Mon, 20 Nov 2023 12:39:50 -0800 Subject: [PATCH 06/13] Change parameters to options object --- src/plugins/timelist/TimelistComponent.vue | 7 +++++-- src/utils/duration.js | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/plugins/timelist/TimelistComponent.vue b/src/plugins/timelist/TimelistComponent.vue index 13c75f7f82c..7f91bf09f1c 100644 --- a/src/plugins/timelist/TimelistComponent.vue +++ b/src/plugins/timelist/TimelistComponent.vue @@ -77,9 +77,12 @@ const headerItems = [ format: function (value) { let result; if (value < 0) { - result = `+${getPreciseDuration(Math.abs(value), true, true)}`; + result = `+${getPreciseDuration(Math.abs(value), { + excludeMilliSeconds: true, + useDayFormat: true + })}`; } else if (value > 0) { - result = `-${getPreciseDuration(value, true, true)}`; + result = `-${getPreciseDuration(value, { excludeMilliSeconds: true, useDayFormat: true })}`; } else { result = 'Now'; } diff --git a/src/utils/duration.js b/src/utils/duration.js index 8e93297f24d..c24913a25e7 100644 --- a/src/utils/duration.js +++ b/src/utils/duration.js @@ -63,7 +63,7 @@ export function millisecondsToDHMS(numericDuration) { return `${dhms ? '+' : ''} ${dhms}`; } -export function getPreciseDuration(value, excludeMilliSeconds, useDayFormat) { +export function getPreciseDuration(value, { excludeMilliSeconds, useDayFormat }) { let preciseDuration; const ms = value || 0; From 2bb696a964a1c084e49b536412307d035f98d5e8 Mon Sep 17 00:00:00 2001 From: Shefali Date: Mon, 20 Nov 2023 13:12:07 -0800 Subject: [PATCH 07/13] Fix regression with auto scroll. Improve code readability --- src/plugins/timelist/TimelistComponent.vue | 89 ++++++++++++---------- 1 file changed, 47 insertions(+), 42 deletions(-) diff --git a/src/plugins/timelist/TimelistComponent.vue b/src/plugins/timelist/TimelistComponent.vue index 7f91bf09f1c..a020f3ce1bd 100644 --- a/src/plugins/timelist/TimelistComponent.vue +++ b/src/plugins/timelist/TimelistComponent.vue @@ -351,54 +351,55 @@ export default { return regex.test(name.toLowerCase()); }); }, - applyStyles(activities) { - let firstCurrentActivityIndex = -1; - let firstFutureActivityIndex = -1; - let currentActivitiesCount = 0; - let pastActivitiesCount = 0; - let futureActivitiesCount = 0; - const styledActivities = activities.map((activity, index) => { - if (this.timestamp >= activity.start && this.timestamp <= activity.end) { - activity.cssClass = CURRENT_CSS_SUFFIX; - if (firstCurrentActivityIndex < 0) { - firstCurrentActivityIndex = index; - } - - currentActivitiesCount = currentActivitiesCount + 1; - } else if (this.timestamp < activity.start) { - activity.cssClass = FUTURE_CSS_SUFFIX; - //the index of the first activity that's greater than the current timestamp - if (firstFutureActivityIndex < 0) { - firstFutureActivityIndex = index; - } - futureActivitiesCount = futureActivitiesCount + 1; - } else { - activity.cssClass = PAST_CSS_SUFFIX; - pastActivitiesCount = pastActivitiesCount + 1; + // Add activity classes, increase activity counts by type, + // set indices of the first occurrences of current and future activities - used for scrolling + styleActivity(activity, index) { + if (this.timestamp >= activity.start && this.timestamp <= activity.end) { + activity.cssClass = CURRENT_CSS_SUFFIX; + if (this.firstCurrentActivityIndex < 0) { + this.firstCurrentActivityIndex = index; } - - if (!activity.key) { - activity.key = uuid(); + this.currentActivitiesCount = this.currentActivitiesCount + 1; + } else if (this.timestamp < activity.start) { + activity.cssClass = FUTURE_CSS_SUFFIX; + //the index of the first activity that's greater than the current timestamp + if (this.firstFutureActivityIndex < 0) { + this.firstFutureActivityIndex = index; } + this.futureActivitiesCount = this.futureActivitiesCount + 1; + } else { + activity.cssClass = PAST_CSS_SUFFIX; + this.pastActivitiesCount = this.pastActivitiesCount + 1; + } - 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; - } + if (!activity.key) { + activity.key = uuid(); + } - return activity; - }); + 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; + } - if (firstCurrentActivityIndex > -1) { - this.firstCurrentOrFutureActivityIndex = firstCurrentActivityIndex; - } else if (firstFutureActivityIndex > -1) { - this.firstCurrentOrFutureActivityIndex = firstFutureActivityIndex; + return activity; + }, + applyStyles(activities) { + this.firstCurrentOrFutureActivityIndex = -1; + this.firstCurrentActivityIndex = -1; + this.firstFutureActivityIndex = -1; + this.currentActivitiesCount = 0; + this.pastActivitiesCount = 0; + this.futureActivitiesCount = 0; + + const styledActivities = activities.map(this.styleActivity); + + if (this.firstCurrentActivityIndex > -1) { + this.firstCurrentOrFutureActivityIndex = this.firstCurrentActivityIndex; + } else if (this.firstFutureActivityIndex > -1) { + this.firstCurrentOrFutureActivityIndex = this.firstFutureActivityIndex; } - this.currentActivitiesCount = currentActivitiesCount; - this.pastActivitiesCount = pastActivitiesCount; - this.futureActivitiesCount = futureActivitiesCount; return styledActivities; }, @@ -426,6 +427,10 @@ export default { return; } + if (this.canAutoScroll() === false) { + return; + } + // See #7167 for scrolling algorithm const scrollTop = this.calculateScrollOffset(); From 0c77e9930d23134cf1789a736c535dec6286b93a Mon Sep 17 00:00:00 2001 From: Shefali Date: Tue, 5 Dec 2023 09:20:15 -0800 Subject: [PATCH 08/13] Add defaults for getPreciseDuration options object --- src/utils/duration.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/duration.js b/src/utils/duration.js index c24913a25e7..68f504b2086 100644 --- a/src/utils/duration.js +++ b/src/utils/duration.js @@ -63,7 +63,7 @@ export function millisecondsToDHMS(numericDuration) { return `${dhms ? '+' : ''} ${dhms}`; } -export function getPreciseDuration(value, { excludeMilliSeconds, useDayFormat }) { +export function getPreciseDuration(value, { excludeMilliSeconds = false, useDayFormat = false }) { let preciseDuration; const ms = value || 0; From a99e5223d2ea70579d16f257c32d815af068bdc7 Mon Sep 17 00:00:00 2001 From: Shefali Date: Tue, 5 Dec 2023 09:49:17 -0800 Subject: [PATCH 09/13] Defaults for options --- src/utils/duration.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/duration.js b/src/utils/duration.js index 68f504b2086..5baba426022 100644 --- a/src/utils/duration.js +++ b/src/utils/duration.js @@ -63,7 +63,7 @@ export function millisecondsToDHMS(numericDuration) { return `${dhms ? '+' : ''} ${dhms}`; } -export function getPreciseDuration(value, { excludeMilliSeconds = false, useDayFormat = false }) { +export function getPreciseDuration(value, { excludeMilliSeconds, useDayFormat } = {}) { let preciseDuration; const ms = value || 0; From f309749aeef7bb12b77124d95ebf8946baf4e281 Mon Sep 17 00:00:00 2001 From: Shefali Date: Mon, 11 Dec 2023 10:44:21 -0800 Subject: [PATCH 10/13] Add missing await --- e2e/tests/functional/planning/timelist.e2e.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/tests/functional/planning/timelist.e2e.spec.js b/e2e/tests/functional/planning/timelist.e2e.spec.js index 56daa8cb6f1..2d72b55dbfd 100644 --- a/e2e/tests/functional/planning/timelist.e2e.spec.js +++ b/e2e/tests/functional/planning/timelist.e2e.spec.js @@ -114,7 +114,7 @@ test.describe('Time List', () => { // Verify all events are displayed const eventCount = await page.locator('.js-list-item').count(); - expect(eventCount).toEqual(testPlan.TEST_GROUP.length); + await expect(eventCount).toEqual(testPlan.TEST_GROUP.length); }); await test.step('Does not show milliseconds in times', async () => { From 69a6e2fbfd7d0b6d113461ff01586e590772c285 Mon Sep 17 00:00:00 2001 From: Shefali Date: Wed, 3 Jan 2024 11:28:21 -0800 Subject: [PATCH 11/13] Fix imports for ESM --- e2e/tests/functional/planning/timelist.e2e.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/e2e/tests/functional/planning/timelist.e2e.spec.js b/e2e/tests/functional/planning/timelist.e2e.spec.js index e18ae09783b..4041acbda04 100644 --- a/e2e/tests/functional/planning/timelist.e2e.spec.js +++ b/e2e/tests/functional/planning/timelist.e2e.spec.js @@ -21,9 +21,9 @@ *****************************************************************************/ import { createDomainObjectWithDefaults, createPlanFromJSON } from '../../../appActions.js'; +import { getEarliestStartTime } from '../../../helper/planningUtils'; import { expect, test } from '../../../pluginFixtures.js'; -const { getEarliestStartTime } = require('../../../helper/planningUtils'); -const examplePlanSmall3 = require('../../../test-data/examplePlans/ExamplePlan_Small3.json'); +import examplePlanSmall3 from '../../../test-data/examplePlans/ExamplePlan_Small3.json'; // eslint-disable-next-line no-unused-vars const START_TIME_COLUMN = 0; From 75ba7f9f7fe4d859062fd94b18043f4a3ee2920e Mon Sep 17 00:00:00 2001 From: Shefali Date: Wed, 3 Jan 2024 11:44:27 -0800 Subject: [PATCH 12/13] Fix json import --- e2e/tests/functional/planning/timelist.e2e.spec.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/e2e/tests/functional/planning/timelist.e2e.spec.js b/e2e/tests/functional/planning/timelist.e2e.spec.js index 4041acbda04..5527cdaec67 100644 --- a/e2e/tests/functional/planning/timelist.e2e.spec.js +++ b/e2e/tests/functional/planning/timelist.e2e.spec.js @@ -19,12 +19,17 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ +import fs from 'fs'; import { createDomainObjectWithDefaults, createPlanFromJSON } from '../../../appActions.js'; import { getEarliestStartTime } from '../../../helper/planningUtils'; import { expect, test } from '../../../pluginFixtures.js'; -import examplePlanSmall3 from '../../../test-data/examplePlans/ExamplePlan_Small3.json'; +const examplePlanSmall3 = JSON.parse( + fs.readFileSync( + new URL('../../../test-data/examplePlans/ExamplePlan_Small3.json', import.meta.url) + ) +); // eslint-disable-next-line no-unused-vars const START_TIME_COLUMN = 0; // eslint-disable-next-line no-unused-vars From 1327b88c8d95e4748016ec229187e8e1585eebfd Mon Sep 17 00:00:00 2001 From: Shefali Date: Wed, 3 Jan 2024 14:55:26 -0800 Subject: [PATCH 13/13] Fix broken test --- e2e/tests/functional/planning/timelist.e2e.spec.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/e2e/tests/functional/planning/timelist.e2e.spec.js b/e2e/tests/functional/planning/timelist.e2e.spec.js index 5527cdaec67..72f2e8c5d8e 100644 --- a/e2e/tests/functional/planning/timelist.e2e.spec.js +++ b/e2e/tests/functional/planning/timelist.e2e.spec.js @@ -107,8 +107,6 @@ test.describe('Time List', () => { parent: timelist.uuid }); - await page.goto(timelist.url); - const startBound = testPlan.TEST_GROUP[0].start; const endBound = testPlan.TEST_GROUP[testPlan.TEST_GROUP.length - 1].end; @@ -118,13 +116,14 @@ test.describe('Time List', () => { ); // Verify all events are displayed - const eventCount = await page.locator('.js-list-item').count(); - await expect(eventCount).toEqual(testPlan.TEST_GROUP.length); + const eventCount = await page.getByRole('row').count(); + // subtracting one for the header + await expect(eventCount - 1).toEqual(testPlan.TEST_GROUP.length); }); await test.step('Does not show milliseconds in times', async () => { - // Get the first activity - const row = page.locator('.js-list-item').first(); + // Get an activity + const row = page.getByRole('row').nth(2); // 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