diff --git a/e2e/test-data/examplePlans/ExamplePlan_Small3.json b/e2e/test-data/examplePlans/ExamplePlan_Small3.json index 82b55e0446..2304cf708b 100644 --- a/e2e/test-data/examplePlans/ExamplePlan_Small3.json +++ b/e2e/test-data/examplePlans/ExamplePlan_Small3.json @@ -1,37 +1,37 @@ { "Group 1": [ { - "name": "Group 1 event 1", - "start": 1650320408000, + "name": "Time until birthday", + "start": 1650320402000, "end": 1660343797000, "type": "Group 1", "color": "orange", "textColor": "white" }, { - "name": "Group 1 event 2", - "start": 1650320508000, - "end": 1660343897000, - "type": "Group 1", - "color": "yellow", + "name": "Time until supper", + "start": 1650320402000, + "end": 1650420410000, + "type": "Group 2", + "color": "blue", "textColor": "white" } ], "Group 2": [ { - "name": "Group 2 event 1", - "start": 1650320608000, - "end": 1660343997000, + "name": "Time since the last time I ate", + "start": 1650320102001, + "end": 1650320102001, "type": "Group 2", "color": "green", "textColor": "white" }, { - "name": "Group 2 event 2", - "start": 1650320808000, - "end": 1660344097000, - "type": "Group 2", - "color": "blue", + "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 b65451bd82..7dbe5e34eb 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: [ @@ -69,7 +78,6 @@ const testPlan = { }; test.describe('Time List', () => { - // the first time modifying test -- watch Terminator test('Create a Time List, add a single Plan to it and verify all the activities are displayed with no milliseconds', async ({ page }) => { @@ -118,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 fb4a0cf856..cc3b364473 100644 --- a/e2e/tests/visual/planning.visual.spec.js +++ b/e2e/tests/visual/planning.visual.spec.js @@ -100,41 +100,3 @@ test.describe('Visual - Planning', () => { }); }); }); - -test.describe('Timelist with controlled clock', () => { - test.use({ - clockOptions: { - now: getEarliestStartTime(examplePlanSmall3), - shouldAdvanceTime: true - } - }); - test.beforeEach(async ({ page }) => { - await page.goto('./', { waitUntil: 'domcontentloaded' }); - }); - test('Timelist', async ({ page, theme }) => { - const timelist = await createDomainObjectWithDefaults(page, { - type: 'Time List', - name: 'Time List Visual Test' - }); - await createPlanFromJSON(page, { - json: examplePlanSmall3, - parent: timelist.uuid - }); - await page.goto( - `${timelist.url}?tc.mode=local&tc.startDelta=900000&tc.endDelta=1800000&tc.timeSystem=utc&view=grid` - ); - - // Dismiss each "Save successful" notification (quicker than waiting) - await page.getByLabel('Dismiss').click(); - await page.getByLabel('Dismiss').click(); - await page.getByLabel('Dismiss').click(); - - await percySnapshot(page, `Timelist Countdown 1 (theme: ${theme})`, { - scope: snapshotScope - }); - await page.waitForTimeout(1000); - await percySnapshot(page, `Timelist Countdown 2 (theme: ${theme})`, { - scope: snapshotScope - }); - }); -});