Skip to content
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

d3 implementation of progress pie chart #7485

Merged
merged 31 commits into from
Mar 5, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
f7cfa3e
d3 implementation of progress pie chart
shefalijoshi Feb 7, 2024
cf64869
Handle 0% and 100% cases
shefalijoshi Feb 12, 2024
77a3692
PR #7485
charlesh88 Feb 12, 2024
42f45af
add in-progress class for compact view
shefalijoshi Feb 12, 2024
b1f1cf3
Merge branch 'redo-pie-chart' of https://github.com/nasa/openmct into…
shefalijoshi Feb 12, 2024
6cbcf7a
Merge branch 'master' into redo-pie-chart
shefalijoshi Feb 13, 2024
729e2b4
Merge branch 'master' into redo-pie-chart
ozyx Feb 21, 2024
5f8aaab
Merge branch 'master' of https://github.com/nasa/openmct into redo-pi…
shefalijoshi Feb 29, 2024
557f8f9
Fix issue where updating progress for pie chart wasn't working till a…
shefalijoshi Mar 1, 2024
651665f
update documentation for clock annotation
unlikelyzero Mar 4, 2024
3acd37c
Update clock annotation in tests
unlikelyzero Mar 4, 2024
a96fbbe
split long testfile
unlikelyzero Mar 4, 2024
c8944dc
driveby missing test
unlikelyzero Mar 4, 2024
8682974
driveby fix flake
unlikelyzero Mar 4, 2024
b68a35a
temp: fix flake and prep for visual test
unlikelyzero Mar 4, 2024
1b4e368
Merge branch 'redo-pie-chart' of https://github.com/nasa/openmct into…
unlikelyzero Mar 4, 2024
20e0973
Fix linting errors
shefalijoshi Mar 4, 2024
08dd7c1
Merge branch 'master' into redo-pie-chart
akhenry Mar 4, 2024
6129034
this should be resolved
unlikelyzero Mar 4, 2024
f516a76
Merge branch 'redo-pie-chart' of https://github.com/nasa/openmct into…
unlikelyzero Mar 4, 2024
4107a9b
these keep popping up
unlikelyzero Mar 4, 2024
998279c
moving some of this around
unlikelyzero Mar 4, 2024
b326577
moving this around
unlikelyzero Mar 4, 2024
217b430
the test
unlikelyzero Mar 4, 2024
7284c47
Merge branch 'master' of https://github.com/nasa/openmct into redo-pi…
shefalijoshi Mar 5, 2024
f891c23
Merge branch 'master' of https://github.com/nasa/openmct into redo-pi…
shefalijoshi Mar 5, 2024
ead0127
Fix imports for tests
shefalijoshi Mar 5, 2024
289465d
no longer need constant
unlikelyzero Mar 5, 2024
3853d17
move to front
unlikelyzero Mar 5, 2024
910b683
Stabalize name
unlikelyzero Mar 5, 2024
b7628e7
test(missionStatus): fix visual test
ozyx Mar 5, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ Current list of test tags:
|`@unstable` | A new test or test which is known to be flaky.|
|`@2p` | Indicates that multiple users are involved, or multiple tabs/pages are used. Useful for testing multi-user interactivity.|
|`@generatedata` | Indicates that a test is used to generate testdata or test the generated test data. Usually to be associated with localstorage, but this may grow over time.|
|`@clock` | A test which modifies the clock. These have expanded out of the visual tests and into the functional tests.

### Continuous Integration

Expand Down Expand Up @@ -447,6 +448,7 @@ By adhering to this principle, we can create tests that are both robust and refl
- Utilize `percyCSS` to ignore time-based elements. For more details, consult our [percyCSS file](./.percy.ci.yml).
- Use Open MCT's fixed-time mode unless explicitly testing realtime clock
- Employ the `createExampleTelemetryObject` appAction to source telemetry and specify a `name` to avoid autogenerated names.
- Avoid creating objects with a time component like timers and clocks.

5. **Hide the Tree and Inspector**: Generally, your test will not require comparisons involving the tree and inspector. These aspects are covered in component-specific tests (explained below). To exclude them from the comparison by default, navigate to the root of the main view with the tree and inspector hidden:
- `await page.goto('./#/browse/mine?hideTree=true&hideInspector=true')`
Expand Down
12 changes: 12 additions & 0 deletions e2e/helper/planningUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,18 @@ export function getLatestEndTime(planJson) {
return Math.max(...activities.map((activity) => activity.end));
}

/**
*
* @param {object} planJson
* @returns {object}
*/
export function getFirstActivity(planJson) {
const groups = Object.keys(planJson);
const firstGroupKey = groups[0];
const firstGroupItems = planJson[firstGroupKey];
return firstGroupItems[0];
}

/**
* Uses the Open MCT API to set the status of a plan to 'draft'.
* @param {import('@playwright/test').Page} page
Expand Down
2 changes: 1 addition & 1 deletion e2e/tests/framework/generateLocalStorageData.e2e.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import { expect, test } from '../../pluginFixtures.js';

const overlayPlotName = 'Overlay Plot with Telemetry Object';

test.describe('Generate Visual Test Data @localStorage @generatedata', () => {
test.describe('Generate Visual Test Data @localStorage @generatedata @clock', () => {
test.use({
clockOptions: {
now: MISSION_TIME,
Expand Down
259 changes: 109 additions & 150 deletions e2e/tests/functional/planning/timelist.e2e.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,9 @@
import fs from 'fs';

import { createDomainObjectWithDefaults, createPlanFromJSON } from '../../../appActions.js';
import { getEarliestStartTime } from '../../../helper/planningUtils';
import { getEarliestStartTime, getFirstActivity } from '../../../helper/planningUtils';
Fixed Show fixed Hide fixed
import { expect, test } from '../../../pluginFixtures.js';

const examplePlanSmall3 = JSON.parse(
fs.readFileSync(
new URL('../../../test-data/examplePlans/ExamplePlan_Small3.json', import.meta.url)
)
);
const examplePlanSmall1 = JSON.parse(
fs.readFileSync(
new URL('../../../test-data/examplePlans/ExamplePlan_Small1.json', import.meta.url)
Expand All @@ -44,7 +39,8 @@
const ACTIVITY_COLUMN = 3;
const HEADER_ROW = 0;
const NUM_COLUMNS = 5;

const FULL_CIRCLE_PATH =
Fixed Show fixed Hide fixed
'M3.061616997868383e-15,-50A50,50,0,1,1,-3.061616997868383e-15,50A50,50,0,1,1,3.061616997868383e-15,-50Z';
test.describe('Time List', () => {
test("Create a Time List, add a single Plan to it, verify all the activities are displayed with no milliseconds and selecting an activity shows it's properties", async ({
page
Expand Down Expand Up @@ -161,7 +157,7 @@
await expect(eventCount).toEqual(firstGroupItems.length);
});

await test.step('Shows activity properties when a row is selected', async () => {
await test.step('Shows activity properties when a row is selected in the expanded view', async () => {
await page.getByRole('row').nth(2).click();

// Find the activity state section in the inspector
Expand All @@ -171,167 +167,130 @@
'Not started'
);
});
});

/**
* 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} CountdownOrUpObject
* @property {string} sign - The sign of the countdown ('-' if the countdown is negative, '+' otherwise).
* @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
await test.step("Verify absence of progress indication for an activity that's not in progress", async () => {
// When an activity is not in progress, the progress pie is not visible
const hidden = await page.getByRole('row').locator('path').nth(1).isHidden();
await expect(hidden).toBe(true);
});
});

test.describe('Time List with controlled clock', () => {
test.describe('Activity progress when activity is in the future', () => {
const firstActivity = getFirstActivity(examplePlanSmall1);

test.use({
clockOptions: {
now: getEarliestStartTime(examplePlanSmall3),
now: firstActivity.start - 1,
shouldAdvanceTime: true
}
});

test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
await createTimelistWithPlanAndSetActivityInProgress(page);
});
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
});
test('progress pie is empty', async ({ page }) => {
const anActivity = page.getByRole('row').nth(0);
// Progress pie shows no progress when now is less than the start time
await expect(anActivity.getByLabel('Activity in progress').locator('path')).not.toHaveAttribute(
'd'
);
});
});

// 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`
);
});
test.describe('Activity progress when now is between start and end of the activity', () => {
const firstActivity = getFirstActivity(examplePlanSmall1);

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 () => {
const countdownCell = countdownCells[i];
// Get the initial countdown timestamp object
const beforeCountdown = await getAndAssertCountdownOrUpObject(page, i + 3);
// should not have a '-' sign
await expect(countdownCell).not.toHaveText('-');
// Wait until it changes
await expect(countdownCell).not.toHaveText(beforeCountdown.toString());
// Get the new countdown timestamp object
const afterCountdown = await getAndAssertCountdownOrUpObject(page, i + 3);
// Verify that the new countdown timestamp object is less than the old one
expect(Number(afterCountdown.seconds)).toBeLessThan(Number(beforeCountdown.seconds));
});
test.use({
clockOptions: {
now: firstActivity.start + 50000,
shouldAdvanceTime: true
}
});

// 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 countUpCell = countUpCells[i];
// Get the initial count-up timestamp object
const beforeCountUp = await getAndAssertCountdownOrUpObject(page, i + 1);
// should not have a '+' sign
await expect(countUpCell).not.toHaveText('+');
// Wait until it changes
await expect(countUpCell).not.toHaveText(beforeCountUp.toString());
// Get the new count-up timestamp object
const afterCountUp = await getAndAssertCountdownOrUpObject(page, i + 1);
// Verify that the new count-up timestamp object is greater than the old one
expect(Number(afterCountUp.seconds)).toBeGreaterThan(Number(beforeCountUp.seconds));
});
test.beforeEach(async ({ page }) => {
await createTimelistWithPlanAndSetActivityInProgress(page);
});

test('progress pie is partially filled', async ({ page }) => {
const anActivity = page.getByRole('row').nth(0);
const pathElement = anActivity.getByLabel('Activity in progress').locator('path');
// Progress pie shows progress when now is greater than the start time
await expect(pathElement).toHaveAttribute('d');
});
});

test.describe('Activity progress when now is after end of the activity', () => {
const firstActivity = getFirstActivity(examplePlanSmall1);

test.use({
clockOptions: {
now: firstActivity.end + 10000,
shouldAdvanceTime: true
}
});

test.beforeEach(async ({ page }) => {
await createTimelistWithPlanAndSetActivityInProgress(page);
});

test('progress pie is full', async ({ page }) => {
const anActivity = page.getByRole('row').nth(0);
// Progress pie is completely full and doesn't update if now is greater than the end time
await expect(anActivity.getByLabel('Activity in progress').locator('path')).toHaveAttribute(
'd',
FULL_CIRCLE_PATH
);
});
});

/**
* 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);
}
async function createTimelistWithPlanAndSetActivityInProgress(page) {
await page.goto('./', { waitUntil: 'domcontentloaded' });

/**
* 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;
}
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 createPlanFromJSON(page, {
name: 'Test Plan',
json: examplePlanSmall1,
parent: timelist.uuid
});

/**
* Get the text from the countdown (or countup) cell in the given row, assert that it matches the countdown/countup
* regex, and return an object representing the countdown.
* @param {import('@playwright/test').Page} page
* @param {number} rowIndex the row index
* @returns {Promise<CountdownOrUpObject>} The countdown (or countup) object
*/
async function getAndAssertCountdownOrUpObject(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
};
// Ensure that all activities are shown in the expanded view
const groups = Object.keys(examplePlanSmall1);
const firstGroupKey = groups[0];
const firstGroupItems = examplePlanSmall1[firstGroupKey];
const firstActivityForPlan = firstGroupItems[0];
const lastActivity = firstGroupItems[firstGroupItems.length - 1];
const startBound = firstActivityForPlan.start;
const endBound = lastActivity.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`
);

// Change the object to edit mode
await page.getByRole('button', { name: 'Edit Object' }).click();

// Find the display properties section in the inspector
await page.getByRole('tab', { name: 'View Properties' }).click();
// Switch to expanded view and save the setting
await page.getByLabel('Display Style').selectOption({ label: 'Expanded' });

// Click on the "Save" button
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();

const anActivity = page.getByRole('row').nth(0);

// Set the activity to in progress
await anActivity.click();
await page.getByRole('tab', { name: 'Activity' }).click();
await page.getByLabel('Activity Status', { exact: true }).selectOption({ label: 'In progress' });
}
Loading
Loading