-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Timelist centering algorithm change and duration formatting change #7194
Changes from 5 commits
d2df694
c5713f3
cc824e0
755054e
c13b375
cc7223b
2bb696a
3e47056
e591933
c467e85
0c77e99
6382172
a99e522
031799a
3411565
f309749
ef6b39d
6ba1052
5bf252a
c79741b
69a6e2f
75ba7f9
1327b88
cdc3b9a
c9000cf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" | ||
} | ||
] | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -22,6 +22,18 @@ | |||||||||||||
|
||||||||||||||
const { test, expect } = require('../../../pluginFixtures'); | ||||||||||||||
const { createDomainObjectWithDefaults, createPlanFromJSON } = require('../../../appActions'); | ||||||||||||||
const { getEarliestStartTime } = require('../../../helper/planningUtils'); | ||||||||||||||
const examplePlanSmall3 = require('../../../test-data/examplePlans/ExamplePlan_Small3.json'); | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We're all ESM now, so we need to update these imports:
Suggested change
|
||||||||||||||
|
||||||||||||||
// 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; | ||||||||||||||
Check notice Code scanning / CodeQL Unused variable, import, function or class
Unused variable ACTIVITY_COLUMN.
|
||||||||||||||
const HEADER_ROW = 0; | ||||||||||||||
const NUM_COLUMNS = 4; | ||||||||||||||
|
||||||||||||||
const testPlan = { | ||||||||||||||
TEST_GROUP: [ | ||||||||||||||
|
@@ -84,22 +96,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']"); | ||||||||||||||
|
||||||||||||||
ozyx marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||
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 +129,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})/; | ||||||||||||||
ozyx marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||
|
||||||||||||||
/** | ||||||||||||||
* @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 | ||||||||||||||
ozyx marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||
} | ||||||||||||||
}); | ||||||||||||||
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 = [ | ||||||||||||||
getCellByIndex(page, 1, TIME_TO_FROM_COLUMN), | ||||||||||||||
getCellByIndex(page, 2, TIME_TO_FROM_COLUMN) | ||||||||||||||
]; | ||||||||||||||
const countdownCells = [ | ||||||||||||||
getCellByIndex(page, 3, TIME_TO_FROM_COLUMN), | ||||||||||||||
getCellByIndex(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 () => { | ||||||||||||||
ozyx marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||
const countdownCell = countdownCells[i]; | ||||||||||||||
// Get the initial countdown timestamp object | ||||||||||||||
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 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 count-up cells are counting up | ||||||||||||||
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 count-up timestamp object | ||||||||||||||
const beforeCountdown = await getAndAssertCountdownObject(page, i + 1); | ||||||||||||||
// Wait until it changes | ||||||||||||||
await expect(countdownCell).not.toHaveText(beforeCountdown.toString()); | ||||||||||||||
// 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)); | ||||||||||||||
}); | ||||||||||||||
} | ||||||||||||||
}); | ||||||||||||||
}); | ||||||||||||||
|
||||||||||||||
/** | ||||||||||||||
* 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 getCellByIndex(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<string>} text | ||||||||||||||
*/ | ||||||||||||||
async function getCellTextByIndex(page, rowIndex, columnIndex) { | ||||||||||||||
const text = await getCellByIndex(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>} countdownObject | ||||||||||||||
*/ | ||||||||||||||
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); | ||||||||||||||
|
||||||||||||||
return { | ||||||||||||||
sign: match[COUNTDOWN.SIGN], | ||||||||||||||
days: match[COUNTDOWN.DAYS], | ||||||||||||||
hours: match[COUNTDOWN.HOURS], | ||||||||||||||
minutes: match[COUNTDOWN.MINUTES], | ||||||||||||||
seconds: match[COUNTDOWN.SECONDS], | ||||||||||||||
toString: () => timeToFrom | ||||||||||||||
}; | ||||||||||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lol