diff --git a/e2e/README.md b/e2e/README.md index ee0debe9dd6..22c41d86d5f 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -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 @@ -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')` diff --git a/e2e/helper/faultUtils.js b/e2e/helper/faultUtils.js index aecbee2ba9c..54c30e23c57 100644 --- a/e2e/helper/faultUtils.js +++ b/e2e/helper/faultUtils.js @@ -21,10 +21,12 @@ *****************************************************************************/ import { fileURLToPath } from 'url'; +import { expect } from '../pluginFixtures.js'; + /** * @param {import('@playwright/test').Page} page */ -async function navigateToFaultManagementWithExample(page) { +export async function navigateToFaultManagementWithExample(page) { await page.addInitScript({ path: fileURLToPath(new URL('./addInitExampleFaultProvider.js', import.meta.url)) }); @@ -35,7 +37,7 @@ async function navigateToFaultManagementWithExample(page) { /** * @param {import('@playwright/test').Page} page */ -async function navigateToFaultManagementWithStaticExample(page) { +export async function navigateToFaultManagementWithStaticExample(page) { await page.addInitScript({ path: fileURLToPath(new URL('./addInitExampleFaultProviderStatic.js', import.meta.url)) }); @@ -46,7 +48,7 @@ async function navigateToFaultManagementWithStaticExample(page) { /** * @param {import('@playwright/test').Page} page */ -async function navigateToFaultManagementWithoutExample(page) { +export async function navigateToFaultManagementWithoutExample(page) { await page.addInitScript({ path: fileURLToPath(new URL('./addInitFaultManagementPlugin.js', import.meta.url)) }); @@ -57,7 +59,7 @@ async function navigateToFaultManagementWithoutExample(page) { /** * @param {import('@playwright/test').Page} page */ -async function navigateToFaultItemInTree(page) { +export async function navigateToFaultItemInTree(page) { await page.goto('./', { waitUntil: 'networkidle' }); const faultManagementTreeItem = page @@ -75,88 +77,95 @@ async function navigateToFaultItemInTree(page) { /** * @param {import('@playwright/test').Page} page */ -async function acknowledgeFault(page, rowNumber) { +export async function acknowledgeFault(page, rowNumber) { await openFaultRowMenu(page, rowNumber); - await page.locator('.c-menu >> text="Acknowledge"').click(); - // Click [aria-label="Save"] - await page.locator('[aria-label="Save"]').click(); + await page.getByLabel('Acknowledge', { exact: true }).click(); + await page.getByLabel('Save').click(); } /** * @param {import('@playwright/test').Page} page */ -async function shelveMultipleFaults(page, ...nums) { +export async function shelveMultipleFaults(page, ...nums) { const selectRows = nums.map((num) => { return selectFaultItem(page, num); }); await Promise.all(selectRows); - await page.locator('button:has-text("Shelve")').click(); - await page.locator('[aria-label="Save"]').click(); + await page.getByLabel('Shelve selected faults').click(); + await page.getByLabel('Save').click(); } /** * @param {import('@playwright/test').Page} page */ -async function acknowledgeMultipleFaults(page, ...nums) { +export async function acknowledgeMultipleFaults(page, ...nums) { const selectRows = nums.map((num) => { return selectFaultItem(page, num); }); await Promise.all(selectRows); await page.locator('button:has-text("Acknowledge")').click(); - await page.locator('[aria-label="Save"]').click(); + await page.getByLabel('Save').click(); } /** * @param {import('@playwright/test').Page} page */ -async function shelveFault(page, rowNumber) { +export async function shelveFault(page, rowNumber) { await openFaultRowMenu(page, rowNumber); await page.locator('.c-menu >> text="Shelve"').click(); // Click [aria-label="Save"] - await page.locator('[aria-label="Save"]').click(); + await page.getByLabel('Save').click(); } /** * @param {import('@playwright/test').Page} page */ -async function changeViewTo(page, view) { +export async function changeViewTo(page, view) { await page.locator('.c-fault-mgmt__search-row select').first().selectOption(view); } /** * @param {import('@playwright/test').Page} page */ -async function sortFaultsBy(page, sort) { +export async function sortFaultsBy(page, sort) { await page.locator('.c-fault-mgmt__list-header-sortButton select').selectOption(sort); } /** * @param {import('@playwright/test').Page} page */ -async function enterSearchTerm(page, term) { +export async function enterSearchTerm(page, term) { await page.locator('.c-fault-mgmt-search [aria-label="Search Input"]').fill(term); } /** * @param {import('@playwright/test').Page} page */ -async function clearSearch(page) { +export async function clearSearch(page) { await enterSearchTerm(page, ''); } /** * @param {import('@playwright/test').Page} page */ -async function selectFaultItem(page, rowNumber) { - await page.locator(`.c-fault-mgmt-item > input >> nth=${rowNumber - 1}`).check(); +export async function selectFaultItem(page, rowNumber) { + await page + .getByLabel('Select fault') + .nth(rowNumber - 1) + .check({ + // Need force here because checkbox state is changed by an event emitted by the checkbox + // eslint-disable-next-line playwright/no-force-option + force: true + }); + await expect(page.getByLabel('Select fault').nth(rowNumber - 1)).toBeChecked(); } /** * @param {import('@playwright/test').Page} page */ -async function getHighestSeverity(page) { +export async function getHighestSeverity(page) { const criticalCount = await page.locator('[title=CRITICAL]').count(); const warningCount = await page.locator('[title=WARNING]').count(); @@ -172,7 +181,7 @@ async function getHighestSeverity(page) { /** * @param {import('@playwright/test').Page} page */ -async function getLowestSeverity(page) { +export async function getLowestSeverity(page) { const warningCount = await page.locator('[title=WARNING]').count(); const watchCount = await page.locator('[title=WATCH]').count(); @@ -188,7 +197,7 @@ async function getLowestSeverity(page) { /** * @param {import('@playwright/test').Page} page */ -async function getFaultResultCount(page) { +export async function getFaultResultCount(page) { const count = await page.locator('.c-faults-list-view-item-body > .c-fault-mgmt__list').count(); return count; @@ -197,7 +206,7 @@ async function getFaultResultCount(page) { /** * @param {import('@playwright/test').Page} page */ -function getFault(page, rowNumber) { +export function getFault(page, rowNumber) { const fault = page.locator( `.c-faults-list-view-item-body > .c-fault-mgmt__list >> nth=${rowNumber - 1}` ); @@ -208,7 +217,7 @@ function getFault(page, rowNumber) { /** * @param {import('@playwright/test').Page} page */ -function getFaultByName(page, name) { +export function getFaultByName(page, name) { const fault = page.locator(`.c-fault-mgmt__list-faultname:has-text("${name}")`); return fault; @@ -217,7 +226,7 @@ function getFaultByName(page, name) { /** * @param {import('@playwright/test').Page} page */ -async function getFaultName(page, rowNumber) { +export async function getFaultName(page, rowNumber) { const faultName = await page .locator(`.c-fault-mgmt__list-faultname >> nth=${rowNumber - 1}`) .textContent(); @@ -228,7 +237,7 @@ async function getFaultName(page, rowNumber) { /** * @param {import('@playwright/test').Page} page */ -async function getFaultSeverity(page, rowNumber) { +export async function getFaultSeverity(page, rowNumber) { const faultSeverity = await page .locator(`.c-faults-list-view-item-body .c-fault-mgmt__list-severity >> nth=${rowNumber - 1}`) .getAttribute('title'); @@ -239,7 +248,7 @@ async function getFaultSeverity(page, rowNumber) { /** * @param {import('@playwright/test').Page} page */ -async function getFaultNamespace(page, rowNumber) { +export async function getFaultNamespace(page, rowNumber) { const faultNamespace = await page .locator(`.c-fault-mgmt__list-path >> nth=${rowNumber - 1}`) .textContent(); @@ -250,7 +259,7 @@ async function getFaultNamespace(page, rowNumber) { /** * @param {import('@playwright/test').Page} page */ -async function getFaultTriggerTime(page, rowNumber) { +export async function getFaultTriggerTime(page, rowNumber) { const faultTriggerTime = await page .locator(`.c-fault-mgmt__list-trigTime >> nth=${rowNumber - 1} >> .c-fault-mgmt-item__value`) .textContent(); @@ -261,35 +270,10 @@ async function getFaultTriggerTime(page, rowNumber) { /** * @param {import('@playwright/test').Page} page */ -async function openFaultRowMenu(page, rowNumber) { +export async function openFaultRowMenu(page, rowNumber) { // select await page - .locator(`.c-fault-mgmt-item > .c-fault-mgmt__list-action-button >> nth=${rowNumber - 1}`) + .getByLabel('Disposition actions') + .nth(rowNumber - 1) .click(); } - -export { - acknowledgeFault, - acknowledgeMultipleFaults, - changeViewTo, - clearSearch, - enterSearchTerm, - getFault, - getFaultByName, - getFaultName, - getFaultNamespace, - getFaultResultCount, - getFaultSeverity, - getFaultTriggerTime, - getHighestSeverity, - getLowestSeverity, - navigateToFaultItemInTree, - navigateToFaultManagementWithExample, - navigateToFaultManagementWithoutExample, - navigateToFaultManagementWithStaticExample, - openFaultRowMenu, - selectFaultItem, - shelveFault, - shelveMultipleFaults, - sortFaultsBy -}; diff --git a/e2e/helper/planningUtils.js b/e2e/helper/planningUtils.js index 3b68f0efad9..2a4cb157a9c 100644 --- a/e2e/helper/planningUtils.js +++ b/e2e/helper/planningUtils.js @@ -20,6 +20,7 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ +import { createDomainObjectWithDefaults, createPlanFromJSON } from '../appActions.js'; import { expect } from '../pluginFixtures.js'; /** @@ -142,6 +143,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 @@ -172,3 +185,55 @@ export async function addPlanGetInterceptor(page) { }); }); } + +/** + * Create a Plan from JSON and add it to a Timelist and Navigate to the Plan view + * @param {import('@playwright/test').Page} page + */ +export async function createTimelistWithPlanAndSetActivityInProgress(page, planJson) { + await page.goto('./', { waitUntil: 'domcontentloaded' }); + + const timelist = await createDomainObjectWithDefaults(page, { + name: 'Time List', + type: 'Time List' + }); + + await createPlanFromJSON(page, { + name: 'Test Plan', + json: planJson, + parent: timelist.uuid + }); + + // Ensure that all activities are shown in the expanded view + const groups = Object.keys(planJson); + const firstGroupKey = groups[0]; + const firstGroupItems = planJson[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' }); +} diff --git a/e2e/tests/framework/generateLocalStorageData.e2e.spec.js b/e2e/tests/framework/generateLocalStorageData.e2e.spec.js index 4b28e6009a8..b01aba37c83 100644 --- a/e2e/tests/framework/generateLocalStorageData.e2e.spec.js +++ b/e2e/tests/framework/generateLocalStorageData.e2e.spec.js @@ -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, diff --git a/e2e/tests/functional/planning/timelist.e2e.spec.js b/e2e/tests/functional/planning/timelist.e2e.spec.js index 4cd5cc12b1f..a0e600d63fe 100644 --- a/e2e/tests/functional/planning/timelist.e2e.spec.js +++ b/e2e/tests/functional/planning/timelist.e2e.spec.js @@ -22,29 +22,13 @@ import fs from 'fs'; import { createDomainObjectWithDefaults, createPlanFromJSON } from '../../../appActions.js'; -import { getEarliestStartTime } from '../../../helper/planningUtils'; 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) ) ); -// 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 = 5; - 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 @@ -161,7 +145,7 @@ test("View a timelist in expanded view, verify all the activities are displayed 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 @@ -171,167 +155,10 @@ test("View a timelist in expanded view, verify all the activities are displayed '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 -}); - -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 = [ - 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)); - }); - } - - // 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)); - }); - } + 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); }); }); - -/** - * 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} text - */ -async function getCellTextByIndex(page, rowIndex, columnIndex) { - const text = await getCellByIndex(page, rowIndex, columnIndex).innerText(); - return text; -} - -/** - * 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} 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 - }; -} diff --git a/e2e/tests/functional/planning/timelistControlledClock.e2e.spec.js b/e2e/tests/functional/planning/timelistControlledClock.e2e.spec.js new file mode 100644 index 00000000000..4baec667184 --- /dev/null +++ b/e2e/tests/functional/planning/timelistControlledClock.e2e.spec.js @@ -0,0 +1,290 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2024, 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. + *****************************************************************************/ + +/* +Collection of Time List tests set to run with browser clock manipulate made possible with the +clockOptions plugin fixture. +*/ + +import fs from 'fs'; + +import { createDomainObjectWithDefaults, createPlanFromJSON } from '../../../appActions.js'; +import { + createTimelistWithPlanAndSetActivityInProgress, + getEarliestStartTime, + getFirstActivity +} from '../../../helper/planningUtils'; +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) + ) +); + +const TIME_TO_FROM_COLUMN = 2; +const HEADER_ROW = 0; +const NUM_COLUMNS = 5; +const FULL_CIRCLE_PATH = + 'M3.061616997868383e-15,-50A50,50,0,1,1,-3.061616997868383e-15,50A50,50,0,1,1,3.061616997868383e-15,-50Z'; + +/** + * 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 +}); + +test.describe('Time List with controlled clock @clock', () => { + test.use({ + clockOptions: { + now: getEarliestStartTime(examplePlanSmall3), + shouldAdvanceTime: true + } + }); + test.beforeEach(async ({ page }) => { + await page.goto('./', { waitUntil: 'domcontentloaded' }); + // 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` + ); + + //Expand the viewport to show the entire time list + await page.getByLabel('Collapse Inspect Pane').click(); + await page.getByLabel('Collapse Browse Pane').click(); + }); + test('Time List shows current events and counts down correctly in real-time mode', async ({ + page + }) => { + const countUpCells = [ + getTimeListCellByIndex(page, 1, TIME_TO_FROM_COLUMN), + getTimeListCellByIndex(page, 2, TIME_TO_FROM_COLUMN) + ]; + const countdownCells = [ + getTimeListCellByIndex(page, 3, TIME_TO_FROM_COLUMN), + getTimeListCellByIndex(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)); + }); + } + + // 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.describe('Activity progress when activity is in the future @clock', () => { + const firstActivity = getFirstActivity(examplePlanSmall1); + + test.use({ + clockOptions: { + now: firstActivity.start - 1, + shouldAdvanceTime: true + } + }); + + test.beforeEach(async ({ page }) => { + await createTimelistWithPlanAndSetActivityInProgress(page, examplePlanSmall1); + }); + + 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' + ); + }); +}); + +test.describe('Activity progress when now is between start and end of the activity @clock', () => { + const firstActivity = getFirstActivity(examplePlanSmall1); + test.beforeEach(async ({ page }) => { + await createTimelistWithPlanAndSetActivityInProgress(page, examplePlanSmall1); + }); + + test.use({ + clockOptions: { + now: firstActivity.start + 50000, + shouldAdvanceTime: true + } + }); + + 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 @clock', () => { + const firstActivity = getFirstActivity(examplePlanSmall1); + + test.use({ + clockOptions: { + now: firstActivity.end + 10000, + shouldAdvanceTime: true + } + }); + + test.beforeEach(async ({ page }) => { + await createTimelistWithPlanAndSetActivityInProgress(page, examplePlanSmall1); + }); + + 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 getTimeListCellByIndex(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 getTimeListCellTextByIndex(page, rowIndex, columnIndex) { + const text = await getTimeListCellByIndex(page, rowIndex, columnIndex).innerText(); + return text; +} + +/** + * 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} The countdown (or countup) object + */ +async function getAndAssertCountdownOrUpObject(page, rowIndex) { + const timeToFrom = await getTimeListCellTextByIndex( + 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/functional/plugins/faultManagement/faultManagement.e2e.spec.js b/e2e/tests/functional/plugins/faultManagement/faultManagement.e2e.spec.js index 08090c0ff37..7b3898a34cf 100644 --- a/e2e/tests/functional/plugins/faultManagement/faultManagement.e2e.spec.js +++ b/e2e/tests/functional/plugins/faultManagement/faultManagement.e2e.spec.js @@ -20,25 +20,46 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import * as utils from '../../../../helper/faultUtils.js'; +import { + acknowledgeFault, + acknowledgeMultipleFaults, + changeViewTo, + clearSearch, + enterSearchTerm, + getFault, + getFaultByName, + getFaultName, + getFaultNamespace, + getFaultResultCount, + getFaultSeverity, + getFaultTriggerTime, + getHighestSeverity, + getLowestSeverity, + navigateToFaultManagementWithExample, + navigateToFaultManagementWithoutExample, + selectFaultItem, + shelveFault, + shelveMultipleFaults, + sortFaultsBy +} from '../../../../helper/faultUtils.js'; import { expect, test } from '../../../../pluginFixtures.js'; test.describe('The Fault Management Plugin using example faults', () => { test.beforeEach(async ({ page }) => { - await utils.navigateToFaultManagementWithExample(page); + await navigateToFaultManagementWithExample(page); }); - test('Shows a criticality icon for every fault @unstable', async ({ page }) => { + test('Shows a criticality icon for every fault', async ({ page }) => { const faultCount = await page.locator('c-fault-mgmt__list').count(); const criticalityIconCount = await page.locator('c-fault-mgmt__list-severity').count(); - expect.soft(faultCount).toEqual(criticalityIconCount); + expect(faultCount).toEqual(criticalityIconCount); }); - test('When selecting a fault, it has an "is-selected" class and it\'s information shows in the inspector @unstable', async ({ + test('When selecting a fault, it has an "is-selected" class and it\'s information shows in the inspector', async ({ page }) => { - await utils.selectFaultItem(page, 1); + await selectFaultItem(page, 1); await page.getByRole('tab', { name: 'Config' }).click(); const selectedFaultName = await page @@ -48,22 +69,22 @@ test.describe('The Fault Management Plugin using example faults', () => { .locator(`.c-inspector__properties >> :text("${selectedFaultName}")`) .count(); - await expect - .soft(page.locator('.c-faults-list-view-item-body > .c-fault-mgmt__list').first()) - .toHaveClass(/is-selected/); - expect.soft(inspectorFaultNameCount).toEqual(1); + await expect( + page.locator('.c-faults-list-view-item-body > .c-fault-mgmt__list').first() + ).toHaveClass(/is-selected/); + expect(inspectorFaultNameCount).toEqual(1); }); - test('When selecting multiple faults, no specific fault information is shown in the inspector @unstable', async ({ + test('When selecting multiple faults, no specific fault information is shown in the inspector', async ({ page }) => { - await utils.selectFaultItem(page, 1); - await utils.selectFaultItem(page, 2); + await selectFaultItem(page, 1); + await selectFaultItem(page, 2); const selectedRows = page.locator( '.c-fault-mgmt__list.is-selected .c-fault-mgmt__list-faultname' ); - expect.soft(await selectedRows.count()).toEqual(2); + expect(await selectedRows.count()).toEqual(2); await page.getByRole('tab', { name: 'Config' }).click(); const firstSelectedFaultName = await selectedRows.nth(0).textContent(); @@ -75,180 +96,180 @@ test.describe('The Fault Management Plugin using example faults', () => { .locator(`.c-inspector__properties >> :text("${secondSelectedFaultName}")`) .count(); - expect.soft(firstNameInInspectorCount).toEqual(0); - expect.soft(secondNameInInspectorCount).toEqual(0); + expect(firstNameInInspectorCount).toEqual(0); + expect(secondNameInInspectorCount).toEqual(0); }); - test('Allows you to shelve a fault @unstable', async ({ page }) => { - const shelvedFaultName = await utils.getFaultName(page, 2); - const beforeShelvedFault = utils.getFaultByName(page, shelvedFaultName); + test('Allows you to shelve a fault', async ({ page }) => { + const shelvedFaultName = await getFaultName(page, 2); + const beforeShelvedFault = getFaultByName(page, shelvedFaultName); - expect.soft(await beforeShelvedFault.count()).toBe(1); + await expect(beforeShelvedFault).toHaveCount(1); - await utils.shelveFault(page, 2); + await shelveFault(page, 2); // check it is removed from standard view - const afterShelvedFault = utils.getFaultByName(page, shelvedFaultName); - expect.soft(await afterShelvedFault.count()).toBe(0); + const afterShelvedFault = getFaultByName(page, shelvedFaultName); + expect(await afterShelvedFault.count()).toBe(0); - await utils.changeViewTo(page, 'shelved'); + await changeViewTo(page, 'shelved'); - const shelvedViewFault = utils.getFaultByName(page, shelvedFaultName); + const shelvedViewFault = getFaultByName(page, shelvedFaultName); - expect.soft(await shelvedViewFault.count()).toBe(1); + expect(await shelvedViewFault.count()).toBe(1); }); - test('Allows you to acknowledge a fault @unstable', async ({ page }) => { - const acknowledgedFaultName = await utils.getFaultName(page, 3); + test('Allows you to acknowledge a fault', async ({ page }) => { + const acknowledgedFaultName = await getFaultName(page, 3); - await utils.acknowledgeFault(page, 3); + await acknowledgeFault(page, 3); - const fault = utils.getFault(page, 3); - await expect.soft(fault).toHaveClass(/is-acknowledged/); + const fault = getFault(page, 3); + await expect(fault).toHaveClass(/is-acknowledged/); - await utils.changeViewTo(page, 'acknowledged'); + await changeViewTo(page, 'acknowledged'); - const acknowledgedViewFaultName = await utils.getFaultName(page, 1); - expect.soft(acknowledgedFaultName).toEqual(acknowledgedViewFaultName); + const acknowledgedViewFaultName = await getFaultName(page, 1); + expect(acknowledgedFaultName).toEqual(acknowledgedViewFaultName); }); - test('Allows you to shelve multiple faults @unstable', async ({ page }) => { - const shelvedFaultNameOne = await utils.getFaultName(page, 1); - const shelvedFaultNameFour = await utils.getFaultName(page, 4); + test('Allows you to shelve multiple faults', async ({ page }) => { + const shelvedFaultNameOne = await getFaultName(page, 1); + const shelvedFaultNameFour = await getFaultName(page, 4); - const beforeShelvedFaultOne = utils.getFaultByName(page, shelvedFaultNameOne); - const beforeShelvedFaultFour = utils.getFaultByName(page, shelvedFaultNameFour); + const beforeShelvedFaultOne = getFaultByName(page, shelvedFaultNameOne); + const beforeShelvedFaultFour = getFaultByName(page, shelvedFaultNameFour); - expect.soft(await beforeShelvedFaultOne.count()).toBe(1); - expect.soft(await beforeShelvedFaultFour.count()).toBe(1); + await expect(beforeShelvedFaultOne).toHaveCount(1); + await expect(beforeShelvedFaultFour).toHaveCount(1); - await utils.shelveMultipleFaults(page, 1, 4); + await shelveMultipleFaults(page, 1, 4); // check it is removed from standard view - const afterShelvedFaultOne = utils.getFaultByName(page, shelvedFaultNameOne); - const afterShelvedFaultFour = utils.getFaultByName(page, shelvedFaultNameFour); - expect.soft(await afterShelvedFaultOne.count()).toBe(0); - expect.soft(await afterShelvedFaultFour.count()).toBe(0); + const afterShelvedFaultOne = getFaultByName(page, shelvedFaultNameOne); + const afterShelvedFaultFour = getFaultByName(page, shelvedFaultNameFour); + await expect(afterShelvedFaultOne).toHaveCount(0); + await expect(afterShelvedFaultFour).toHaveCount(0); - await utils.changeViewTo(page, 'shelved'); + await changeViewTo(page, 'shelved'); - const shelvedViewFaultOne = utils.getFaultByName(page, shelvedFaultNameOne); - const shelvedViewFaultFour = utils.getFaultByName(page, shelvedFaultNameFour); + const shelvedViewFaultOne = getFaultByName(page, shelvedFaultNameOne); + const shelvedViewFaultFour = getFaultByName(page, shelvedFaultNameFour); - expect.soft(await shelvedViewFaultOne.count()).toBe(1); - expect.soft(await shelvedViewFaultFour.count()).toBe(1); + await expect(shelvedViewFaultOne).toHaveCount(1); + await expect(shelvedViewFaultFour).toHaveCount(1); }); - test('Allows you to acknowledge multiple faults @unstable', async ({ page }) => { - const acknowledgedFaultNameTwo = await utils.getFaultName(page, 2); - const acknowledgedFaultNameFive = await utils.getFaultName(page, 5); + test('Allows you to acknowledge multiple faults', async ({ page }) => { + const acknowledgedFaultNameTwo = await getFaultName(page, 2); + const acknowledgedFaultNameFive = await getFaultName(page, 5); - await utils.acknowledgeMultipleFaults(page, 2, 5); + await acknowledgeMultipleFaults(page, 2, 5); - const faultTwo = utils.getFault(page, 2); - const faultFive = utils.getFault(page, 5); + const faultTwo = getFault(page, 2); + const faultFive = getFault(page, 5); // check they have been acknowledged - await expect.soft(faultTwo).toHaveClass(/is-acknowledged/); - await expect.soft(faultFive).toHaveClass(/is-acknowledged/); + await expect(faultTwo).toHaveClass(/is-acknowledged/); + await expect(faultFive).toHaveClass(/is-acknowledged/); - await utils.changeViewTo(page, 'acknowledged'); + await changeViewTo(page, 'acknowledged'); - const acknowledgedViewFaultTwo = utils.getFaultByName(page, acknowledgedFaultNameTwo); - const acknowledgedViewFaultFive = utils.getFaultByName(page, acknowledgedFaultNameFive); + const acknowledgedViewFaultTwo = getFaultByName(page, acknowledgedFaultNameTwo); + const acknowledgedViewFaultFive = getFaultByName(page, acknowledgedFaultNameFive); - expect.soft(await acknowledgedViewFaultTwo.count()).toBe(1); - expect.soft(await acknowledgedViewFaultFive.count()).toBe(1); + await expect(acknowledgedViewFaultTwo).toHaveCount(1); + await expect(acknowledgedViewFaultFive).toHaveCount(1); }); - test('Allows you to search faults @unstable', async ({ page }) => { - const faultThreeNamespace = await utils.getFaultNamespace(page, 3); - const faultTwoName = await utils.getFaultName(page, 2); - const faultFiveTriggerTime = await utils.getFaultTriggerTime(page, 5); + test('Allows you to search faults', async ({ page }) => { + const faultThreeNamespace = await getFaultNamespace(page, 3); + const faultTwoName = await getFaultName(page, 2); + const faultFiveTriggerTime = await getFaultTriggerTime(page, 5); // should be all faults (5) - let faultResultCount = await utils.getFaultResultCount(page); - expect.soft(faultResultCount).toEqual(5); + let faultResultCount = await getFaultResultCount(page); + expect(faultResultCount).toEqual(5); // search namespace - await utils.enterSearchTerm(page, faultThreeNamespace); + await enterSearchTerm(page, faultThreeNamespace); - faultResultCount = await utils.getFaultResultCount(page); - expect.soft(faultResultCount).toEqual(1); - expect.soft(await utils.getFaultNamespace(page, 1)).toEqual(faultThreeNamespace); + faultResultCount = await getFaultResultCount(page); + expect(faultResultCount).toEqual(1); + expect(await getFaultNamespace(page, 1)).toEqual(faultThreeNamespace); // all faults - await utils.clearSearch(page); - faultResultCount = await utils.getFaultResultCount(page); - expect.soft(faultResultCount).toEqual(5); + await clearSearch(page); + faultResultCount = await getFaultResultCount(page); + expect(faultResultCount).toEqual(5); // search name - await utils.enterSearchTerm(page, faultTwoName); + await enterSearchTerm(page, faultTwoName); - faultResultCount = await utils.getFaultResultCount(page); - expect.soft(faultResultCount).toEqual(1); - expect.soft(await utils.getFaultName(page, 1)).toEqual(faultTwoName); + faultResultCount = await getFaultResultCount(page); + expect(faultResultCount).toEqual(1); + expect(await getFaultName(page, 1)).toEqual(faultTwoName); // all faults - await utils.clearSearch(page); - faultResultCount = await utils.getFaultResultCount(page); - expect.soft(faultResultCount).toEqual(5); + await clearSearch(page); + faultResultCount = await getFaultResultCount(page); + expect(faultResultCount).toEqual(5); // search triggerTime - await utils.enterSearchTerm(page, faultFiveTriggerTime); + await enterSearchTerm(page, faultFiveTriggerTime); - faultResultCount = await utils.getFaultResultCount(page); - expect.soft(faultResultCount).toEqual(1); - expect.soft(await utils.getFaultTriggerTime(page, 1)).toEqual(faultFiveTriggerTime); + faultResultCount = await getFaultResultCount(page); + expect(faultResultCount).toEqual(1); + expect(await getFaultTriggerTime(page, 1)).toEqual(faultFiveTriggerTime); }); - test('Allows you to sort faults @unstable', async ({ page }) => { - const highestSeverity = await utils.getHighestSeverity(page); - const lowestSeverity = await utils.getLowestSeverity(page); + test('Allows you to sort faults', async ({ page }) => { + const highestSeverity = await getHighestSeverity(page); + const lowestSeverity = await getLowestSeverity(page); const faultOneName = 'Example Fault 1'; const faultFiveName = 'Example Fault 5'; - let firstFaultName = await utils.getFaultName(page, 1); + let firstFaultName = await getFaultName(page, 1); - expect.soft(firstFaultName).toEqual(faultOneName); + expect(firstFaultName).toEqual(faultOneName); - await utils.sortFaultsBy(page, 'oldest-first'); + await sortFaultsBy(page, 'oldest-first'); - firstFaultName = await utils.getFaultName(page, 1); - expect.soft(firstFaultName).toEqual(faultFiveName); + firstFaultName = await getFaultName(page, 1); + expect(firstFaultName).toEqual(faultFiveName); - await utils.sortFaultsBy(page, 'severity'); + await sortFaultsBy(page, 'severity'); - const sortedHighestSeverity = await utils.getFaultSeverity(page, 1); - const sortedLowestSeverity = await utils.getFaultSeverity(page, 5); - expect.soft(sortedHighestSeverity).toEqual(highestSeverity); - expect.soft(sortedLowestSeverity).toEqual(lowestSeverity); + const sortedHighestSeverity = await getFaultSeverity(page, 1); + const sortedLowestSeverity = await getFaultSeverity(page, 5); + expect(sortedHighestSeverity).toEqual(highestSeverity); + expect(sortedLowestSeverity).toEqual(lowestSeverity); }); }); test.describe('The Fault Management Plugin without using example faults', () => { test.beforeEach(async ({ page }) => { - await utils.navigateToFaultManagementWithoutExample(page); + await navigateToFaultManagementWithoutExample(page); }); - test('Shows no faults when no faults are provided @unstable', async ({ page }) => { + test('Shows no faults when no faults are provided', async ({ page }) => { const faultCount = await page.locator('c-fault-mgmt__list').count(); - expect.soft(faultCount).toEqual(0); + expect(faultCount).toEqual(0); - await utils.changeViewTo(page, 'acknowledged'); + await changeViewTo(page, 'acknowledged'); const acknowledgedCount = await page.locator('c-fault-mgmt__list').count(); - expect.soft(acknowledgedCount).toEqual(0); + expect(acknowledgedCount).toEqual(0); - await utils.changeViewTo(page, 'shelved'); + await changeViewTo(page, 'shelved'); const shelvedCount = await page.locator('c-fault-mgmt__list').count(); - expect.soft(shelvedCount).toEqual(0); + expect(shelvedCount).toEqual(0); }); - test('Will return no faults when searching @unstable', async ({ page }) => { - await utils.enterSearchTerm(page, 'fault'); + test('Will return no faults when searching', async ({ page }) => { + await enterSearchTerm(page, 'fault'); const faultCount = await page.locator('c-fault-mgmt__list').count(); - expect.soft(faultCount).toEqual(0); + expect(faultCount).toEqual(0); }); }); diff --git a/e2e/tests/functional/plugins/timer/timer.e2e.spec.js b/e2e/tests/functional/plugins/timer/timer.e2e.spec.js index 1502909c8c9..1e0393545fe 100644 --- a/e2e/tests/functional/plugins/timer/timer.e2e.spec.js +++ b/e2e/tests/functional/plugins/timer/timer.e2e.spec.js @@ -66,7 +66,7 @@ test.describe('Timer', () => { }); }); -test.describe('Timer with target date', () => { +test.describe('Timer with target date @clock', () => { let timer; test.beforeEach(async ({ page }) => { diff --git a/e2e/tests/visual-a11y/components/header.visual.spec.js b/e2e/tests/visual-a11y/components/header.visual.spec.js index ba9320a83bb..a42e7cea8d4 100644 --- a/e2e/tests/visual-a11y/components/header.visual.spec.js +++ b/e2e/tests/visual-a11y/components/header.visual.spec.js @@ -25,11 +25,12 @@ Tests the branding associated with the default deployment. At least the about mo */ import percySnapshot from '@percy/playwright'; +import { fileURLToPath } from 'url'; import { expect, test } from '../../../avpFixtures.js'; import { VISUAL_URL } from '../../../constants.js'; -//Declare the scope of the visual test +//Declare the component scope of the visual test for Percy const header = '.l-shell__head'; test.describe('Visual - Header @a11y', () => { @@ -78,6 +79,26 @@ test.describe('Visual - Header @a11y', () => { await expect(await page.getByLabel('Show Snapshots')).toBeVisible(); }); }); + +//Header test with all mission status options. Right now, this is just Mission Status, but should grow over time +test.describe('Mission Header @a11y', () => { + test.beforeEach(async ({ page }) => { + await page.addInitScript({ + path: fileURLToPath(new URL('../../../helper/addInitExampleUser.js', import.meta.url)) + }); + await page.goto('./', { waitUntil: 'domcontentloaded' }); + await expect(page.getByText('Select Role')).toBeVisible(); + // set role + await page.getByRole('button', { name: 'Select', exact: true }).click(); + // dismiss role confirmation popup + await page.getByRole('button', { name: 'Dismiss' }).click(); + }); + test('Mission status panel', async ({ page, theme }) => { + await percySnapshot(page, `Header default with Mission Header (theme: '${theme}')`, { + scope: header + }); + }); +}); // Skipping for https://github.com/nasa/openmct/issues/7421 // test.afterEach(async ({ page }, testInfo) => { // await scanForA11yViolations(page, testInfo.title); diff --git a/e2e/tests/visual-a11y/components/inspector.visual.spec.js b/e2e/tests/visual-a11y/components/inspector.visual.spec.js index 799978ea478..edd64aec748 100644 --- a/e2e/tests/visual-a11y/components/inspector.visual.spec.js +++ b/e2e/tests/visual-a11y/components/inspector.visual.spec.js @@ -28,7 +28,7 @@ import { MISSION_TIME, VISUAL_URL } from '../../../constants.js'; //Declare the scope of the visual test const inspectorPane = '.l-shell__pane-inspector'; -test.describe('Visual - Inspector @ally', () => { +test.describe('Visual - Inspector @ally @clock', () => { test.beforeEach(async ({ page }) => { await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' }); }); diff --git a/e2e/tests/visual-a11y/controlledClock.visual.spec.js b/e2e/tests/visual-a11y/controlledClock.visual.spec.js index 72e4979f960..6e25b2e980c 100644 --- a/e2e/tests/visual-a11y/controlledClock.visual.spec.js +++ b/e2e/tests/visual-a11y/controlledClock.visual.spec.js @@ -30,7 +30,7 @@ import percySnapshot from '@percy/playwright'; import { MISSION_TIME, VISUAL_URL } from '../../constants.js'; import { expect, test } from '../../pluginFixtures.js'; -test.describe('Visual - Controlled Clock', () => { +test.describe('Visual - Controlled Clock @clock', () => { test.beforeEach(async ({ page }) => { await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' }); }); diff --git a/e2e/tests/visual-a11y/faultManagement.visual.spec.js b/e2e/tests/visual-a11y/faultManagement.visual.spec.js index f038e6d37d2..f3f5be314cd 100644 --- a/e2e/tests/visual-a11y/faultManagement.visual.spec.js +++ b/e2e/tests/visual-a11y/faultManagement.visual.spec.js @@ -20,18 +20,26 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ import percySnapshot from '@percy/playwright'; -import { fileURLToPath } from 'url'; -import * as utils from '../../helper/faultUtils.js'; +import { + acknowledgeFault, + changeViewTo, + navigateToFaultManagementWithoutExample, + navigateToFaultManagementWithStaticExample, + openFaultRowMenu, + selectFaultItem, + shelveFault +} from '../../helper/faultUtils.js'; import { expect, test } from '../../pluginFixtures.js'; -test.describe('Fault Management Visual Tests', () => { - test('icon test', async ({ page, theme }) => { - await page.addInitScript({ - path: fileURLToPath(new URL('../../helper/addInitFaultManagementPlugin.js', import.meta.url)) - }); - await page.goto('./', { waitUntil: 'domcontentloaded' }); +test.describe('Fault Management Visual Tests - without example', () => { + test.beforeEach(async ({ page }) => { + await navigateToFaultManagementWithoutExample(page); + await page.getByLabel('Collapse Inspect Pane').click(); + await page.getByLabel('Click to collapse items').click(); + }); + test('fault management icon appears in tree', async ({ page, theme }) => { // Wait for status bar to load await expect( page.getByRole('status', { @@ -51,14 +59,20 @@ test.describe('Fault Management Visual Tests', () => { await percySnapshot(page, `Fault Management icon appears in tree (theme: '${theme}')`); }); +}); - test('fault list and acknowledged faults', async ({ page, theme }) => { - await utils.navigateToFaultManagementWithStaticExample(page); +test.describe('Fault Management Visual Tests', () => { + test.beforeEach(async ({ page }) => { + await navigateToFaultManagementWithStaticExample(page); + await page.getByLabel('Collapse Inspect Pane').click(); + await page.getByLabel('Click to collapse items').click(); + }); + test('fault list and acknowledged faults', async ({ page, theme }) => { await percySnapshot(page, `Shows a list of faults in the standard view (theme: '${theme}')`); - await utils.acknowledgeFault(page, 1); - await utils.changeViewTo(page, 'acknowledged'); + await acknowledgeFault(page, 1); + await changeViewTo(page, 'acknowledged'); await percySnapshot( page, @@ -67,14 +81,12 @@ test.describe('Fault Management Visual Tests', () => { }); test('shelved faults', async ({ page, theme }) => { - await utils.navigateToFaultManagementWithStaticExample(page); - - await utils.shelveFault(page, 1); - await utils.changeViewTo(page, 'shelved'); + await shelveFault(page, 1); + await changeViewTo(page, 'shelved'); await percySnapshot(page, `Shelved faults appear in the shelved view (theme: '${theme}')`); - await utils.openFaultRowMenu(page, 1); + await openFaultRowMenu(page, 1); await percySnapshot( page, @@ -83,9 +95,7 @@ test.describe('Fault Management Visual Tests', () => { }); test('3-dot menu for fault', async ({ page, theme }) => { - await utils.navigateToFaultManagementWithStaticExample(page); - - await utils.openFaultRowMenu(page, 1); + await openFaultRowMenu(page, 1); await percySnapshot( page, @@ -94,9 +104,7 @@ test.describe('Fault Management Visual Tests', () => { }); test('ability to acknowledge or shelve', async ({ page, theme }) => { - await utils.navigateToFaultManagementWithStaticExample(page); - - await utils.selectFaultItem(page, 1); + await selectFaultItem(page, 1); await percySnapshot( page, diff --git a/e2e/tests/visual-a11y/missionStatus.visual.spec.js b/e2e/tests/visual-a11y/missionStatus.visual.spec.js index 774b8fdef9e..2d4600838af 100644 --- a/e2e/tests/visual-a11y/missionStatus.visual.spec.js +++ b/e2e/tests/visual-a11y/missionStatus.visual.spec.js @@ -32,12 +32,12 @@ test.describe('Mission Status Visual Tests @a11y', () => { }); await page.goto('./', { waitUntil: 'domcontentloaded' }); await expect(page.getByText('Select Role')).toBeVisible(); - // Description should be empty https://github.com/nasa/openmct/issues/6978 - await expect(page.locator('c-message__action-text')).toBeHidden(); // set role await page.getByRole('button', { name: 'Select', exact: true }).click(); // dismiss role confirmation popup await page.getByRole('button', { name: 'Dismiss' }).click(); + await page.getByLabel('Collapse Inspect Pane').click(); + await page.getByLabel('Collapse Browse Pane').click(); }); test('Mission status panel', async ({ page, theme }) => { await page.getByLabel('Toggle Mission Status Panel').click(); diff --git a/e2e/tests/visual-a11y/planning.visual.spec.js b/e2e/tests/visual-a11y/planning.visual.spec.js index 51783b7cf11..e2a5a17ff1f 100644 --- a/e2e/tests/visual-a11y/planning.visual.spec.js +++ b/e2e/tests/visual-a11y/planning.visual.spec.js @@ -26,13 +26,41 @@ import fs from 'fs'; import { createDomainObjectWithDefaults, createPlanFromJSON } from '../../appActions.js'; import { test } from '../../avpFixtures.js'; import { VISUAL_URL } from '../../constants.js'; -import { setBoundsToSpanAllActivities, setDraftStatusForPlan } from '../../helper/planningUtils.js'; +import { + createTimelistWithPlanAndSetActivityInProgress, + getFirstActivity, + setBoundsToSpanAllActivities, + setDraftStatusForPlan +} from '../../helper/planningUtils.js'; + +const examplePlanSmall1 = JSON.parse( + fs.readFileSync(new URL('../../test-data/examplePlans/ExamplePlan_Small1.json', import.meta.url)) +); -const examplePlanSmall = JSON.parse( +const examplePlanSmall2 = JSON.parse( fs.readFileSync(new URL('../../test-data/examplePlans/ExamplePlan_Small2.json', import.meta.url)) ); -const snapshotScope = '.l-shell__pane-main .l-pane__contents'; +test.describe('Visual - Timelist progress bar @clock', () => { + const firstActivity = getFirstActivity(examplePlanSmall1); + + test.use({ + clockOptions: { + now: firstActivity.end + 10000, + shouldAdvanceTime: true + } + }); + + test.beforeEach(async ({ page }) => { + await createTimelistWithPlanAndSetActivityInProgress(page, examplePlanSmall1); + await page.getByLabel('Click to collapse items').click(); + }); + + test('progress pie is full', async ({ page, theme }) => { + // Progress pie is completely full and doesn't update if now is greater than the end time + await percySnapshot(page, `Time List with Activity in Progress (theme: ${theme})`); + }); +}); test.describe('Visual - Planning', () => { test.beforeEach(async ({ page }) => { @@ -42,42 +70,41 @@ test.describe('Visual - Planning', () => { test('Plan View', async ({ page, theme }) => { const plan = await createPlanFromJSON(page, { name: 'Plan Visual Test', - json: examplePlanSmall + json: examplePlanSmall2 }); - await setBoundsToSpanAllActivities(page, examplePlanSmall, plan.url); - await percySnapshot(page, `Plan View (theme: ${theme})`, { - scope: snapshotScope - }); + await setBoundsToSpanAllActivities(page, examplePlanSmall2, plan.url); + await percySnapshot(page, `Plan View (theme: ${theme})`); }); test('Plan View w/ draft status', async ({ page, theme }) => { const plan = await createPlanFromJSON(page, { name: 'Plan Visual Test (Draft)', - json: examplePlanSmall + json: examplePlanSmall2 }); await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' }); await setDraftStatusForPlan(page, plan); - await setBoundsToSpanAllActivities(page, examplePlanSmall, plan.url); - await percySnapshot(page, `Plan View w/ draft status (theme: ${theme})`, { - scope: snapshotScope - }); + await setBoundsToSpanAllActivities(page, examplePlanSmall2, plan.url); + await percySnapshot(page, `Plan View w/ draft status (theme: ${theme})`); }); +}); +test.describe('Visual - Gantt Chart', () => { + test.beforeEach(async ({ page }) => { + await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' }); + }); test('Gantt Chart View', async ({ page, theme }) => { const ganttChart = await createDomainObjectWithDefaults(page, { type: 'Gantt Chart', name: 'Gantt Chart Visual Test' }); await createPlanFromJSON(page, { - json: examplePlanSmall, + json: examplePlanSmall2, parent: ganttChart.uuid }); - await setBoundsToSpanAllActivities(page, examplePlanSmall, ganttChart.url); - await percySnapshot(page, `Gantt Chart View (theme: ${theme}) - Clipped Activity Names`, { - scope: snapshotScope - }); + await setBoundsToSpanAllActivities(page, examplePlanSmall2, ganttChart.url); + await percySnapshot(page, `Gantt Chart View (theme: ${theme}) - Clipped Activity Names`); // Expand the inspect pane and uncheck the 'Clip Activity Names' option await page.getByRole('button', { name: 'Expand Inspect Pane' }).click(); @@ -93,9 +120,7 @@ test.describe('Visual - Planning', () => { // Dismiss the notification await page.getByLabel('Dismiss').click(); - await percySnapshot(page, `Gantt Chart View (theme: ${theme}) - Unclipped Activity Names`, { - scope: snapshotScope - }); + await percySnapshot(page, `Gantt Chart View (theme: ${theme}) - Unclipped Activity Names`); }); test('Gantt Chart View w/ draft status', async ({ page, theme }) => { @@ -104,7 +129,7 @@ test.describe('Visual - Planning', () => { name: 'Gantt Chart Visual Test (Draft)' }); const plan = await createPlanFromJSON(page, { - json: examplePlanSmall, + json: examplePlanSmall2, parent: ganttChart.uuid }); @@ -112,10 +137,8 @@ test.describe('Visual - Planning', () => { await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' }); - await setBoundsToSpanAllActivities(page, examplePlanSmall, ganttChart.url); - await percySnapshot(page, `Gantt Chart View w/ draft status (theme: ${theme})`, { - scope: snapshotScope - }); + await setBoundsToSpanAllActivities(page, examplePlanSmall2, ganttChart.url); + await percySnapshot(page, `Gantt Chart View w/ draft status (theme: ${theme})`); // Expand the inspect pane and uncheck the 'Clip Activity Names' option await page.getByRole('button', { name: 'Expand Inspect Pane' }).click(); @@ -133,14 +156,12 @@ test.describe('Visual - Planning', () => { await percySnapshot( page, - `Gantt Chart View w/ draft status (theme: ${theme}) - Unclipped Activity Names`, - { - scope: snapshotScope - } + `Gantt Chart View w/ draft status (theme: ${theme}) - Unclipped Activity Names` ); }); - // Skipping for https://github.com/nasa/openmct/issues/7421 - // test.afterEach(async ({ page }, testInfo) => { - // await scanForA11yViolations(page, testInfo.title); - // }); }); + +// Skipping for https://github.com/nasa/openmct/issues/7421 +// test.afterEach(async ({ page }, testInfo) => { +// await scanForA11yViolations(page, testInfo.title); +// }); diff --git a/example/faultManagement/exampleFaultSource.js b/example/faultManagement/exampleFaultSource.js index 1215549eb9b..17592ba4b9d 100644 --- a/example/faultManagement/exampleFaultSource.js +++ b/example/faultManagement/exampleFaultSource.js @@ -20,13 +20,13 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import utils from './utils.js'; +import { acknowledgeFault, randomFaults, shelveFault } from './utils.js'; export default function (staticFaults = false) { return function install(openmct) { openmct.install(openmct.plugins.FaultManagement()); - const faultsData = utils.randomFaults(staticFaults); + const faultsData = randomFaults(staticFaults); openmct.faults.addProvider({ request(domainObject, options) { @@ -44,14 +44,14 @@ export default function (staticFaults = false) { return domainObject.type === 'faultManagement'; }, acknowledgeFault(fault, { comment = '' }) { - utils.acknowledgeFault(fault); + acknowledgeFault(fault); return Promise.resolve({ success: true }); }, shelveFault(fault, duration) { - utils.shelveFault(fault, duration); + shelveFault(fault, duration); return Promise.resolve({ success: true diff --git a/example/faultManagement/utils.js b/example/faultManagement/utils.js index 4a38cd9e71c..ed5de3f0e06 100644 --- a/example/faultManagement/utils.js +++ b/example/faultManagement/utils.js @@ -43,7 +43,7 @@ const getRandom = { } }; -function shelveFault( +export function shelveFault( fault, opts = { shelved: true, @@ -58,11 +58,11 @@ function shelveFault( }, opts.shelveDuration); } -function acknowledgeFault(fault) { +export function acknowledgeFault(fault) { fault.acknowledged = true; } -function randomFaults(staticFaults, count = 5) { +export function randomFaults(staticFaults, count = 5) { let faults = []; for (let x = 1, y = count + 1; x < y; x++) { @@ -71,9 +71,3 @@ function randomFaults(staticFaults, count = 5) { return faults; } - -export default { - randomFaults, - shelveFault, - acknowledgeFault -}; diff --git a/package.json b/package.json index c90b9b56f00..1377810faad 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "@percy/playwright": "1.0.4", "@playwright/test": "1.39.0", "@types/d3-axis": "3.0.6", + "@types/d3-shape": "3.0.0", "@types/d3-scale": "4.0.8", "@types/d3-selection": "3.0.10", "@types/eventemitter3": "1.2.0", @@ -26,6 +27,7 @@ "cspell": "7.3.8", "css-loader": "6.10.0", "d3-axis": "3.0.0", + "d3-shape": "3.0.0", "d3-scale": "4.0.2", "d3-selection": "3.0.0", "eslint": "8.56.0", diff --git a/src/plugins/faultManagement/FaultManagementListHeader.vue b/src/plugins/faultManagement/FaultManagementListHeader.vue index cc40af58a61..e020fbedbb9 100644 --- a/src/plugins/faultManagement/FaultManagementListHeader.vue +++ b/src/plugins/faultManagement/FaultManagementListHeader.vue @@ -23,7 +23,7 @@ @@ -60,6 +62,14 @@ export default { disableShelve: true }; }, + computed: { + acknowledgeButtonLabel() { + return 'Acknowledge selected faults'; + }, + shelveButtonLabel() { + return 'Shelve selected faults'; + } + }, watch: { selectedFaults(newSelectedFaults) { const selectedfaults = Object.values(newSelectedFaults); diff --git a/src/plugins/faultManagement/FaultManagementView.vue b/src/plugins/faultManagement/FaultManagementView.vue index 0eeba1a9bda..97c47eefd4b 100644 --- a/src/plugins/faultManagement/FaultManagementView.vue +++ b/src/plugins/faultManagement/FaultManagementView.vue @@ -21,23 +21,123 @@ -->