Compare commits
21 Commits
esm-is-wei
...
visual-tes
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26a4f9c7d0 | ||
|
|
87ba9fcbc0 | ||
|
|
ab49f3f3a1 | ||
|
|
df969722d1 | ||
|
|
ef62633df1 | ||
|
|
597dc58eb7 | ||
|
|
5d00d642f3 | ||
|
|
39ab81c3d0 | ||
|
|
0bdd0963a4 | ||
|
|
eae51356c8 | ||
|
|
a36ad3f5e7 | ||
|
|
307ededd19 | ||
|
|
d7ecfdf10f | ||
|
|
a1c36f314d | ||
|
|
86e636cbce | ||
|
|
0d2b36ae82 | ||
|
|
95072da257 | ||
|
|
faa2621e26 | ||
|
|
73eead6b72 | ||
|
|
e449fd0eda | ||
|
|
7d25c967a5 |
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -8,7 +8,7 @@ Closes <!--- Insert Issue Number(s) this PR addresses. Start by typing # will op
|
||||
|
||||
* [ ] Have you followed the guidelines in our [Contributing document](https://github.com/nasa/openmct/blob/master/CONTRIBUTING.md)?
|
||||
* [ ] Have you checked to ensure there aren't other open [Pull Requests](https://github.com/nasa/openmct/pulls) for the same update/change?
|
||||
* [ ] Is this a notable change that will require a special callout in the release notes [Notable Change](../docs/src/process/release.md) ? For example, will this break compatibility with existing APIs or projects which source these plugins?
|
||||
* [ ] Is this a [notable change](../docs/src/process/release.md) that will require a special callout in the release notes? For example, will this break compatibility with existing APIs or projects that consume these plugins?
|
||||
|
||||
### Author Checklist
|
||||
|
||||
|
||||
16
.github/workflows/e2e-flakefinder.yml
vendored
16
.github/workflows/e2e-flakefinder.yml
vendored
@@ -1,15 +1,15 @@
|
||||
name: 'pr:e2e:flakefinder'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: master
|
||||
# push:
|
||||
# branches: master
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
types:
|
||||
- labeled
|
||||
- opened
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
# pull_request:
|
||||
# types:
|
||||
# - labeled
|
||||
# - opened
|
||||
# schedule:
|
||||
# - cron: '0 0 * * *'
|
||||
|
||||
jobs:
|
||||
e2e-flakefinder:
|
||||
|
||||
30
e2e/.percy.mobile.yml
Normal file
30
e2e/.percy.mobile.yml
Normal file
@@ -0,0 +1,30 @@
|
||||
version: 2
|
||||
snapshot:
|
||||
percyCSS: |
|
||||
/* Clock indicator... your days are numbered */
|
||||
.t-indicator-clock > .label {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
.c-input--datetime {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
/* Timer object text */
|
||||
.c-ne__time-and-creator {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
/* Time Conductor ticks */
|
||||
div.c-conductor-axis.c-conductor__ticks > svg {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
/* Embedded timestamp in notebooks */
|
||||
.c-ne__embed__time{
|
||||
opacity: 0 !important;
|
||||
}
|
||||
/* Time Conductor Start Time */
|
||||
.c-compact-tc__setting-value{
|
||||
opacity: 0 !important;
|
||||
}
|
||||
/* Chart Area for Plots */
|
||||
.gl-plot-chart-area{
|
||||
opacity: 0 !important;
|
||||
}
|
||||
@@ -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')`
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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' });
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
import { devices } from '@playwright/test';
|
||||
const MAX_FAILURES = 5;
|
||||
const NUM_WORKERS = 2;
|
||||
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
@@ -20,7 +19,8 @@ const config = {
|
||||
reuseExistingServer: true //This was originally disabled to prevent differences in local debugging vs. CI. However, it significantly speeds up local debugging.
|
||||
},
|
||||
maxFailures: MAX_FAILURES, //Limits failures to 5 to reduce CI Waste
|
||||
workers: NUM_WORKERS, //Limit to 2 for CircleCI Agent
|
||||
workers: 1, //Limit to 1 due to resource constraints similar to https://github.com/percy/cli/discussions/1067
|
||||
|
||||
use: {
|
||||
baseURL: 'http://localhost:8080/',
|
||||
headless: true,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<string>} 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<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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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<string>} 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<CountdownOrUpObject>} 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
|
||||
};
|
||||
}
|
||||
@@ -298,7 +298,7 @@ test.describe('Basic Condition Set Use', () => {
|
||||
}) => {
|
||||
const exampleTelemetry = await createExampleTelemetryObject(page);
|
||||
|
||||
await page.getByTitle('Show selected item in tree').click();
|
||||
await page.getByLabel('Show selected item in tree').click();
|
||||
await page.goto(conditionSet.url);
|
||||
// Change the object to edit mode
|
||||
await page.getByLabel('Edit Object').click();
|
||||
@@ -378,4 +378,83 @@ test.describe('Basic Condition Set Use', () => {
|
||||
await page.goto(conditionSet.url);
|
||||
await expect(outputValue).toHaveText('---');
|
||||
});
|
||||
|
||||
test('ConditionSet has correct outputs when test data is enabled', async ({ page }) => {
|
||||
const exampleTelemetry = await createExampleTelemetryObject(page);
|
||||
|
||||
await page.getByLabel('Show selected item in tree').click();
|
||||
await page.goto(conditionSet.url);
|
||||
// Change the object to edit mode
|
||||
await page.getByLabel('Edit Object').click();
|
||||
|
||||
// Create two conditions
|
||||
await page.locator('#addCondition').click();
|
||||
await page.locator('#addCondition').click();
|
||||
await page.locator('#conditionCollection').getByRole('textbox').nth(0).fill('First Condition');
|
||||
await page.locator('#conditionCollection').getByRole('textbox').nth(1).fill('Second Condition');
|
||||
|
||||
// Add Telemetry to ConditionSet
|
||||
const sineWaveGeneratorTreeItem = page
|
||||
.getByRole('tree', {
|
||||
name: 'Main Tree'
|
||||
})
|
||||
.getByRole('treeitem', {
|
||||
name: exampleTelemetry.name
|
||||
});
|
||||
const conditionCollection = page.locator('#conditionCollection');
|
||||
await sineWaveGeneratorTreeItem.dragTo(conditionCollection);
|
||||
|
||||
// Modify First Criterion
|
||||
const firstCriterionTelemetry = page.locator(
|
||||
'[aria-label="Criterion Telemetry Selection"] >> nth=0'
|
||||
);
|
||||
firstCriterionTelemetry.selectOption({ label: exampleTelemetry.name });
|
||||
const firstCriterionMetadata = page.locator(
|
||||
'[aria-label="Criterion Metadata Selection"] >> nth=0'
|
||||
);
|
||||
firstCriterionMetadata.selectOption({ label: 'Sine' });
|
||||
const firstCriterionComparison = page.locator(
|
||||
'[aria-label="Criterion Comparison Selection"] >> nth=0'
|
||||
);
|
||||
firstCriterionComparison.selectOption({ label: 'is greater than or equal to' });
|
||||
const firstCriterionInput = page.locator('[aria-label="Criterion Input"] >> nth=0');
|
||||
await firstCriterionInput.fill('0');
|
||||
|
||||
// Modify Second Criterion
|
||||
const secondCriterionTelemetry = page.locator(
|
||||
'[aria-label="Criterion Telemetry Selection"] >> nth=1'
|
||||
);
|
||||
await secondCriterionTelemetry.selectOption({ label: exampleTelemetry.name });
|
||||
|
||||
const secondCriterionMetadata = page.locator(
|
||||
'[aria-label="Criterion Metadata Selection"] >> nth=1'
|
||||
);
|
||||
await secondCriterionMetadata.selectOption({ label: 'Sine' });
|
||||
|
||||
const secondCriterionComparison = page.locator(
|
||||
'[aria-label="Criterion Comparison Selection"] >> nth=1'
|
||||
);
|
||||
await secondCriterionComparison.selectOption({ label: 'is less than' });
|
||||
|
||||
const secondCriterionInput = page.locator('[aria-label="Criterion Input"] >> nth=1');
|
||||
await secondCriterionInput.fill('0');
|
||||
|
||||
// Enable test data
|
||||
await page.getByLabel('Apply Test Data').nth(1).click();
|
||||
const testDataTelemetry = page.locator('[aria-label="Test Data Telemetry Selection"] >> nth=0');
|
||||
await testDataTelemetry.selectOption({ label: exampleTelemetry.name });
|
||||
|
||||
const testDataMetadata = page.locator('[aria-label="Test Data Metadata Selection"] >> nth=0');
|
||||
await testDataMetadata.selectOption({ label: 'Sine' });
|
||||
|
||||
const testInput = page.locator('[aria-label="Test Data Input"] >> nth=0');
|
||||
await testInput.fill('0');
|
||||
|
||||
// Validate that the condition set is evaluating and outputting
|
||||
// the correct value when the underlying telemetry subscription is active.
|
||||
let outputValue = page.locator('[aria-label="Current Output Value"]');
|
||||
await expect(outputValue).toHaveText('false');
|
||||
|
||||
await page.goto(exampleTelemetry.url);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -308,7 +308,7 @@ test.describe('Notebook entry tests', () => {
|
||||
await page.goto(notebookObject.url);
|
||||
|
||||
// Reveal the notebook in the tree
|
||||
await page.getByTitle('Show selected item in tree').click();
|
||||
await page.getByLabel('Show selected item in tree').click();
|
||||
|
||||
await page
|
||||
.getByRole('treeitem', { name: overlayPlot.name })
|
||||
@@ -332,7 +332,7 @@ test.describe('Notebook entry tests', () => {
|
||||
await page.goto(notebookObject.url);
|
||||
|
||||
// Reveal the notebook in the tree
|
||||
await page.getByTitle('Show selected item in tree').click();
|
||||
await page.getByLabel('Show selected item in tree').click();
|
||||
|
||||
await nbUtils.enterTextEntry(page, 'Entry to drop into');
|
||||
await page
|
||||
@@ -377,7 +377,7 @@ test.describe('Notebook entry tests', () => {
|
||||
await page.goto(notebookObject.url);
|
||||
|
||||
// Reveal the notebook in the tree
|
||||
await page.getByTitle('Show selected item in tree').click();
|
||||
await page.getByLabel('Show selected item in tree').click();
|
||||
|
||||
await nbUtils.enterTextEntry(page, `This should be a link: ${TEST_LINK} is it?`);
|
||||
|
||||
@@ -404,7 +404,7 @@ test.describe('Notebook entry tests', () => {
|
||||
await page.goto(notebookObject.url);
|
||||
|
||||
// Reveal the notebook in the tree
|
||||
await page.getByTitle('Show selected item in tree').click();
|
||||
await page.getByLabel('Show selected item in tree').click();
|
||||
|
||||
await nbUtils.enterTextEntry(page, `This should NOT be a link: ${TEST_LINK} is it?`);
|
||||
|
||||
@@ -421,7 +421,7 @@ test.describe('Notebook entry tests', () => {
|
||||
await page.goto(notebookObject.url);
|
||||
|
||||
// Reveal the notebook in the tree
|
||||
await page.getByTitle('Show selected item in tree').click();
|
||||
await page.getByLabel('Show selected item in tree').click();
|
||||
|
||||
await nbUtils.enterTextEntry(page, `This should NOT be a link: ${TEST_LINK} is it?`);
|
||||
|
||||
@@ -438,7 +438,7 @@ test.describe('Notebook entry tests', () => {
|
||||
await page.goto(notebookObject.url);
|
||||
|
||||
// Reveal the notebook in the tree
|
||||
await page.getByTitle('Show selected item in tree').click();
|
||||
await page.getByLabel('Show selected item in tree').click();
|
||||
|
||||
await nbUtils.enterTextEntry(page, `This should be a link: ${INVALID_TEST_LINK} is it?`);
|
||||
|
||||
@@ -455,7 +455,7 @@ test.describe('Notebook entry tests', () => {
|
||||
await page.goto(notebookObject.url);
|
||||
|
||||
// Reveal the notebook in the tree
|
||||
await page.getByTitle('Show selected item in tree').click();
|
||||
await page.getByLabel('Show selected item in tree').click();
|
||||
|
||||
await nbUtils.enterTextEntry(page, `This should be a link: ${TEST_LINK} is it?`);
|
||||
|
||||
@@ -483,7 +483,7 @@ test.describe('Notebook entry tests', () => {
|
||||
await page.goto(notebookObject.url);
|
||||
|
||||
// Reveal the notebook in the tree
|
||||
await page.getByTitle('Show selected item in tree').click();
|
||||
await page.getByLabel('Show selected item in tree').click();
|
||||
|
||||
await nbUtils.enterTextEntry(
|
||||
page,
|
||||
|
||||
@@ -189,6 +189,57 @@ test.describe('Overlay Plot', () => {
|
||||
await assertLimitLinesExistAndAreVisible(page);
|
||||
});
|
||||
|
||||
test('Limit lines adjust when series is resized', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/6987'
|
||||
});
|
||||
// Create an Overlay Plot with a default SWG
|
||||
overlayPlot = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Overlay Plot'
|
||||
});
|
||||
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
parent: overlayPlot.uuid
|
||||
});
|
||||
|
||||
await page.goto(overlayPlot.url);
|
||||
|
||||
// Assert that no limit lines are shown by default
|
||||
await page.waitForSelector('.js-limit-area', { state: 'attached' });
|
||||
expect(await page.locator('.c-plot-limit-line').count()).toBe(0);
|
||||
|
||||
// Enter edit mode
|
||||
await page.getByLabel('Edit Object').click();
|
||||
|
||||
// Expand the "Sine Wave Generator" plot series options and enable limit lines
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
await page
|
||||
.getByRole('list', { name: 'Plot Series Properties' })
|
||||
.locator('span')
|
||||
.first()
|
||||
.click();
|
||||
await page
|
||||
.getByRole('list', { name: 'Plot Series Properties' })
|
||||
.getByRole('checkbox', { name: 'Limit lines' })
|
||||
.check();
|
||||
|
||||
await assertLimitLinesExistAndAreVisible(page);
|
||||
|
||||
// Save (exit edit mode)
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.locator('li[title="Save and Finish Editing"]').click();
|
||||
|
||||
const initialCoords = await assertLimitLinesExistAndAreVisible(page);
|
||||
// Resize the chart container by showing the snapshot pane.
|
||||
await page.getByLabel('Show Snapshots').click();
|
||||
|
||||
const newCoords = await assertLimitLinesExistAndAreVisible(page);
|
||||
// We just need to know that the first limit line redrew somewhere lower than the initial y position.
|
||||
expect(newCoords.y).toBeGreaterThan(initialCoords.y);
|
||||
});
|
||||
|
||||
test('The elements pool supports dragging series into multiple y-axis buckets', async ({
|
||||
page
|
||||
}) => {
|
||||
@@ -305,6 +356,10 @@ test.describe('Overlay Plot', () => {
|
||||
type: 'Sine Wave Generator',
|
||||
parent: overlayPlot.uuid
|
||||
});
|
||||
const swgB = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
parent: overlayPlot.uuid
|
||||
});
|
||||
|
||||
await page.goto(overlayPlot.url);
|
||||
// Wait for plot series data to load and be drawn
|
||||
@@ -319,6 +374,23 @@ test.describe('Overlay Plot', () => {
|
||||
await page.getByRole('menuitem', { name: 'Remove' }).click();
|
||||
await page.getByRole('button', { name: 'OK', exact: true }).click();
|
||||
await expect(swgAElementsPoolItem).toBeHidden();
|
||||
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/7530'
|
||||
});
|
||||
await test.step('Verify that the legend is correct after removing a series', async () => {
|
||||
await page.getByLabel('Plot Canvas').hover();
|
||||
await page.mouse.move(50, 0, {
|
||||
steps: 10
|
||||
});
|
||||
await expect(page.getByLabel('Plot Legend Item')).toHaveCount(1);
|
||||
await expect(page.getByLabel(`Plot Legend Item for ${swgA.name}`)).toBeHidden();
|
||||
await expect(page.getByLabel(`Plot Legend Item for ${swgB.name}`)).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -337,4 +409,7 @@ async function assertLimitLinesExistAndAreVisible(page) {
|
||||
for (let i = 0; i < limitLineCount; i++) {
|
||||
await expect(page.locator('.c-plot-limit-line').nth(i)).toBeVisible();
|
||||
}
|
||||
|
||||
const firstLimitLineCoords = await page.locator('.c-plot-limit-line').first().boundingBox();
|
||||
return firstLimitLineCoords;
|
||||
}
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -191,7 +191,7 @@ test.describe('Recent Objects', () => {
|
||||
|
||||
// Navigate to the clock and reveal it in the tree
|
||||
await page.goto(clock.url);
|
||||
await page.getByTitle('Show selected item in tree').click();
|
||||
await page.getByLabel('Show selected item in tree').click();
|
||||
|
||||
// Right click the clock and create an alias using the "link" context menu action
|
||||
const clockTreeItem = page
|
||||
|
||||
@@ -40,7 +40,7 @@ test.describe('Main Tree', () => {
|
||||
type: 'Folder'
|
||||
});
|
||||
|
||||
await page.getByTitle('Show selected item in tree').click();
|
||||
await page.getByLabel('Show selected item in tree').click();
|
||||
|
||||
const clock = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Clock',
|
||||
|
||||
@@ -34,26 +34,76 @@ Make no assumptions about the order that elements appear in the DOM.
|
||||
*/
|
||||
|
||||
import { expect, test } from '../../pluginFixtures.js';
|
||||
import percySnapshot from '@percy/playwright';
|
||||
|
||||
test('Verify that My Items Tree appears @mobile', async ({ page, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
//Go to baseURL
|
||||
await page.goto('./');
|
||||
test.describe('Visual Smoke tests for @mobile', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
//For now, this test is going to be hardcoded against './test-data/display_layout_with_child_layouts.json'
|
||||
await page.goto('./');
|
||||
});
|
||||
|
||||
//My Items to be visible
|
||||
await expect(page.getByRole('treeitem', { name: `${myItemsFolderName}` })).toBeVisible();
|
||||
test.only('Verify that My Items Tree appears @mobile', async ({ page }) => {
|
||||
//My Items to be visible
|
||||
await expect(page.getByRole('treeitem', { name: 'My Items' })).toBeVisible();
|
||||
await percySnapshot(page, `Visual Mobile Smoke Test`);
|
||||
});
|
||||
});
|
||||
|
||||
test('Verify that user can search @mobile', async ({ page }) => {
|
||||
//For now, this test is going to be hardcoded against './test-data/display_layout_with_child_layouts.json'
|
||||
await page.goto('./');
|
||||
test.describe('Smoke tests for @mobile', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
//For now, this test is going to be hardcoded against './test-data/display_layout_with_child_layouts.json'
|
||||
await page.goto('./');
|
||||
});
|
||||
|
||||
await page.getByRole('searchbox', { name: 'Search Input' }).click();
|
||||
await page.getByRole('searchbox', { name: 'Search Input' }).fill('Parent Display Layout');
|
||||
//Search Results appear in search modal
|
||||
await expect(page.getByLabel('Object Results').getByText('Parent Display Layout')).toBeVisible();
|
||||
//Clicking on the search result takes you to the object
|
||||
await page.getByLabel('Object Results').getByText('Parent Display Layout').click();
|
||||
await page.getByTitle('Collapse Browse Pane').click();
|
||||
await expect(page.getByRole('main').getByText('Parent Display Layout')).toBeVisible();
|
||||
test('Verify that My Items Tree appears @mobile', async ({ page }) => {
|
||||
//My Items to be visible
|
||||
await expect(page.getByRole('treeitem', { name: 'My Items' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('Verify that user can search @mobile', async ({ page }) => {
|
||||
await page.getByRole('searchbox', { name: 'Search Input' }).click();
|
||||
await page.getByRole('searchbox', { name: 'Search Input' }).fill('Parent Display Layout');
|
||||
//Search Results appear in search modal
|
||||
await expect(
|
||||
page.getByLabel('Object Results').getByText('Parent Display Layout')
|
||||
).toBeVisible();
|
||||
//Clicking on the search result takes you to the object
|
||||
await page.getByLabel('Object Results').getByText('Parent Display Layout').click();
|
||||
await page.getByTitle('Collapse Browse Pane').click();
|
||||
await expect(page.getByRole('main').getByText('Parent Display Layout')).toBeVisible();
|
||||
});
|
||||
|
||||
test('Verify that user can change time conductor @mobile', async ({ page }) => {
|
||||
//Collapse Browse Pane to get more Time Conductor space
|
||||
await page.getByLabel('Collapse Browse Pane').click();
|
||||
//Open Time Conductor and change to Real Time Mode and set offset hour by 1 hour
|
||||
// Disabling line because we're intentionally obscuring the text
|
||||
// eslint-disable-next-line playwright/no-force-option
|
||||
await page.getByLabel('Time Conductor Mode').click({ force: true });
|
||||
await page.getByLabel('Time Conductor Mode Menu').click();
|
||||
await page.getByLabel('Real-Time').click();
|
||||
await page.getByLabel('Start offset hours').fill('01');
|
||||
await page.getByLabel('Submit time offsets').click();
|
||||
await expect(page.getByLabel('Start offset: 01:30:00')).toBeVisible();
|
||||
});
|
||||
|
||||
test('Remove Object and confirmation dialog @mobile', async ({ page }) => {
|
||||
await page.getByRole('searchbox', { name: 'Search Input' }).click();
|
||||
await page.getByRole('searchbox', { name: 'Search Input' }).fill('Parent Display Layout');
|
||||
//Search Results appear in search modal
|
||||
//Clicking on the search result takes you to the object
|
||||
await page.getByLabel('Object Results').getByText('Parent Display Layout').click();
|
||||
await page.getByTitle('Collapse Browse Pane').click();
|
||||
await expect(page.getByRole('main').getByText('Parent Display Layout')).toBeVisible();
|
||||
//Verify both objects are in view
|
||||
await expect(await page.getByLabel('Child Layout 1 Layout')).toBeVisible();
|
||||
await expect(await page.getByLabel('Child Layout 2 Layout')).toBeVisible();
|
||||
//Remove First Object to bring up confirmation dialog
|
||||
await page.getByLabel('View menu items').nth(1).click();
|
||||
await page.getByLabel('Remove').click();
|
||||
await page.getByRole('button', { name: 'OK' }).click();
|
||||
//Verify that the object is removed
|
||||
await expect(await page.getByLabel('Child Layout 1 Layout')).toBeVisible();
|
||||
expect(await page.getByLabel('Child Layout 2 Layout').count()).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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' });
|
||||
});
|
||||
|
||||
@@ -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' });
|
||||
});
|
||||
|
||||
@@ -93,4 +93,14 @@ test.describe('Visual - Display Layout', () => {
|
||||
await page.getByLabel('Parent Layout Layout', { exact: true }).click();
|
||||
await percySnapshot(page, `Parent outer layout selected (theme: '${theme}')`);
|
||||
});
|
||||
|
||||
test('Toolbar does not overflow into inspector', async ({ page, theme }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/7036'
|
||||
});
|
||||
await page.getByLabel('Expand Inspect Pane').click();
|
||||
await page.getByLabel('Resize Inspect Pane').dragTo(page.getByLabel('X:'));
|
||||
await percySnapshot(page, `Toolbar does not overflow into inspector (theme: '${theme}')`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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.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 utils.navigateToFaultManagementWithStaticExample(page);
|
||||
|
||||
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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 examplePlanSmall = JSON.parse(
|
||||
const examplePlanSmall1 = JSON.parse(
|
||||
fs.readFileSync(new URL('../../test-data/examplePlans/ExamplePlan_Small1.json', import.meta.url))
|
||||
);
|
||||
|
||||
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);
|
||||
// });
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
23
package.json
23
package.json
@@ -5,13 +5,14 @@
|
||||
"type": "module",
|
||||
"main": "dist/openmct.js",
|
||||
"devDependencies": {
|
||||
"@axe-core/playwright": "4.8.2",
|
||||
"@axe-core/playwright": "4.8.5",
|
||||
"@babel/eslint-parser": "7.23.3",
|
||||
"@braintree/sanitize-url": "6.0.4",
|
||||
"@percy/cli": "1.27.4",
|
||||
"@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",
|
||||
@@ -24,8 +25,9 @@
|
||||
"comma-separated-values": "3.6.4",
|
||||
"copy-webpack-plugin": "11.0.0",
|
||||
"cspell": "7.3.8",
|
||||
"css-loader": "6.8.1",
|
||||
"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",
|
||||
@@ -36,7 +38,7 @@
|
||||
"eslint-plugin-prettier": "5.1.3",
|
||||
"eslint-plugin-simple-import-sort": "10.0.0",
|
||||
"eslint-plugin-unicorn": "49.0.0",
|
||||
"eslint-plugin-vue": "9.18.1",
|
||||
"eslint-plugin-vue": "9.22.0",
|
||||
"eslint-plugin-you-dont-need-lodash-underscore": "6.13.0",
|
||||
"eventemitter3": "1.2.0",
|
||||
"file-saver": "2.0.5",
|
||||
@@ -57,7 +59,7 @@
|
||||
"karma-webpack": "5.0.0",
|
||||
"location-bar": "3.0.1",
|
||||
"lodash": "4.17.21",
|
||||
"marked": "11.2.0",
|
||||
"marked": "12.0.0",
|
||||
"mini-css-extract-plugin": "2.7.6",
|
||||
"moment": "2.30.1",
|
||||
"moment-duration-format": "2.3.2",
|
||||
@@ -65,15 +67,15 @@
|
||||
"npm-run-all2": "6.1.1",
|
||||
"nyc": "15.1.0",
|
||||
"painterro": "1.2.87",
|
||||
"plotly.js-basic-dist-min": "2.20.0",
|
||||
"plotly.js-basic-dist-min": "2.29.1",
|
||||
"plotly.js-gl2d-dist-min": "2.20.0",
|
||||
"prettier": "3.2.5",
|
||||
"prettier-eslint": "16.3.0",
|
||||
"printj": "1.3.1",
|
||||
"resolve-url-loader": "5.0.0",
|
||||
"sanitize-html": "2.11.0",
|
||||
"sanitize-html": "2.12.1",
|
||||
"sass": "1.68.0",
|
||||
"sass-loader": "14.0.0",
|
||||
"sass-loader": "14.1.1",
|
||||
"sinon": "17.0.0",
|
||||
"style-loader": "3.3.3",
|
||||
"terser-webpack-plugin": "5.3.9",
|
||||
@@ -81,11 +83,11 @@
|
||||
"typescript": "5.3.3",
|
||||
"uuid": "9.0.1",
|
||||
"vue": "3.4.19",
|
||||
"vue-eslint-parser": "9.3.2",
|
||||
"vue-eslint-parser": "9.4.2",
|
||||
"vue-loader": "16.8.3",
|
||||
"webpack": "5.89.0",
|
||||
"webpack": "5.90.3",
|
||||
"webpack-cli": "5.1.1",
|
||||
"webpack-dev-server": "4.15.1",
|
||||
"webpack-dev-server": "5.0.2",
|
||||
"webpack-merge": "5.10.0"
|
||||
},
|
||||
"scripts": {
|
||||
@@ -108,6 +110,7 @@
|
||||
"test:e2e": "npx playwright test",
|
||||
"test:e2e:a11y": "npx playwright test --config=e2e/playwright-visual-a11y.config.js --project=chrome --grep @a11y",
|
||||
"test:e2e:mobile": "npx playwright test --config=e2e/playwright-mobile.config.js",
|
||||
"test:e2e:mobile:visual": "percy exec --config ./e2e/.percy.mobile.yml -- npx playwright test --config=e2e/playwright-mobile.config.js",
|
||||
"test:e2e:couchdb": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @couchdb --workers=1",
|
||||
"test:e2e:stable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep-invert \"@unstable|@couchdb|@generatedata\"",
|
||||
"test:e2e:unstable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @unstable",
|
||||
|
||||
@@ -29,27 +29,29 @@
|
||||
class="c-click-icon c-overlay__close-button icon-x"
|
||||
@click.stop="destroy"
|
||||
></button>
|
||||
<div
|
||||
ref="element"
|
||||
class="c-overlay__contents js-notebook-snapshot-item-wrapper"
|
||||
tabindex="0"
|
||||
aria-modal="true"
|
||||
aria-label="Overlay"
|
||||
role="dialog"
|
||||
></div>
|
||||
<div v-if="buttons" class="c-overlay__button-bar">
|
||||
<button
|
||||
v-for="(button, index) in buttons"
|
||||
ref="buttons"
|
||||
:key="index"
|
||||
class="c-button js-overlay__button"
|
||||
<div class="c-overlay__content-wrapper">
|
||||
<div
|
||||
ref="element"
|
||||
class="c-overlay__contents js-notebook-snapshot-item-wrapper"
|
||||
tabindex="0"
|
||||
:class="{ 'c-button--major': focusIndex === index }"
|
||||
@focus="focusIndex = index"
|
||||
@click="buttonClickHandler(button.callback)"
|
||||
>
|
||||
{{ button.label }}
|
||||
</button>
|
||||
aria-modal="true"
|
||||
aria-label="Overlay"
|
||||
role="dialog"
|
||||
></div>
|
||||
<div v-if="buttons" class="c-overlay__button-bar">
|
||||
<button
|
||||
v-for="(button, index) in buttons"
|
||||
ref="buttons"
|
||||
:key="index"
|
||||
class="c-button js-overlay__button"
|
||||
tabindex="0"
|
||||
:class="{ 'c-button--major': focusIndex === index }"
|
||||
@focus="focusIndex = index"
|
||||
@click="buttonClickHandler(button.callback)"
|
||||
>
|
||||
{{ button.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
&__icon {
|
||||
// Holds a background SVG graphic
|
||||
$s: 80px;
|
||||
$s: 50px;
|
||||
flex: 0 0 auto;
|
||||
min-width: $s;
|
||||
min-height: $s;
|
||||
|
||||
@@ -19,7 +19,9 @@
|
||||
z-index: 70;
|
||||
|
||||
&__blocker {
|
||||
display: none; // Mobile-first
|
||||
// Mobile-first: use the blocker to create a full look to dialogs
|
||||
@include abs();
|
||||
background: $colorBodyBg;
|
||||
}
|
||||
|
||||
&__outer {
|
||||
@@ -27,7 +29,13 @@
|
||||
background: $colorBodyBg;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: $overlayInnerMargin;
|
||||
|
||||
body.mobile .l-overlay-fit & {
|
||||
// Vertically center small dialogs in mobile
|
||||
top: 50%;
|
||||
bottom: auto;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
&__close-button {
|
||||
@@ -39,12 +47,32 @@
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
&__content-wrapper {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
flex-direction: column;
|
||||
gap: $interiorMargin;
|
||||
|
||||
body.desktop & {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.l-overlay-fit &,
|
||||
.l-overlay-dialog & {
|
||||
margin: $overlayInnerMargin;
|
||||
}
|
||||
}
|
||||
|
||||
&__contents {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
outline: none;
|
||||
overflow: auto;
|
||||
body.mobile & {
|
||||
flex: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__top-bar {
|
||||
@@ -78,6 +106,10 @@
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: $interiorMargin;
|
||||
body.mobile & {
|
||||
justify-content: flex-end;
|
||||
padding-right: $interiorMargin;
|
||||
}
|
||||
|
||||
> * + * {
|
||||
margin-left: $interiorMargin;
|
||||
|
||||
@@ -31,10 +31,10 @@
|
||||
<div class="c-cs__header-label c-section__label">Test Data</div>
|
||||
</div>
|
||||
<div v-if="expanded" class="c-cs__content">
|
||||
<div class="c-cs__test-data__controls c-cdef__controls" :disabled="!telemetry.length">
|
||||
<div :class="['c-cs__test-data__controls c-cdef__controls', { disabled: !telemetry.length }]">
|
||||
<label class="c-toggle-switch">
|
||||
<input type="checkbox" :checked="isApplied" @change="applyTestData" />
|
||||
<span class="c-toggle-switch__slider"></span>
|
||||
<span class="c-toggle-switch__slider" aria-label="Apply Test Data"></span>
|
||||
<span class="c-toggle-switch__label">Apply Test Data</span>
|
||||
</label>
|
||||
</div>
|
||||
@@ -47,7 +47,11 @@
|
||||
<span class="c-cs-test__label">Set</span>
|
||||
<span class="c-cs-test__controls">
|
||||
<span class="c-cdef__control">
|
||||
<select v-model="testInput.telemetry" @change="updateMetadata(testInput)">
|
||||
<select
|
||||
v-model="testInput.telemetry"
|
||||
aria-label="Test Data Telemetry Selection"
|
||||
@change="updateMetadata(testInput)"
|
||||
>
|
||||
<option value="">- Select Telemetry -</option>
|
||||
<option
|
||||
v-for="(telemetryOption, index) in telemetry"
|
||||
@@ -59,7 +63,11 @@
|
||||
</select>
|
||||
</span>
|
||||
<span v-if="testInput.telemetry" class="c-cdef__control">
|
||||
<select v-model="testInput.metadata" @change="updateTestData">
|
||||
<select
|
||||
v-model="testInput.metadata"
|
||||
aria-label="Test Data Metadata Selection"
|
||||
@change="updateTestData"
|
||||
>
|
||||
<option value="">- Select Field -</option>
|
||||
<option
|
||||
v-for="(option, index) in telemetryMetadataOptions[getId(testInput.telemetry)]"
|
||||
@@ -76,6 +84,7 @@
|
||||
placeholder="Enter test input"
|
||||
type="text"
|
||||
class="c-cdef__control__input"
|
||||
aria-label="Test Data Input"
|
||||
@change="updateTestData"
|
||||
/>
|
||||
</span>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<template>
|
||||
<div class="c-fault-mgmt-item-header c-fault-mgmt__list-header c-fault-mgmt__list">
|
||||
<div class="c-fault-mgmt-item-header c-fault-mgmt__checkbox">
|
||||
<input type="checkbox" :checked="isSelectAll" @input="selectAll" />
|
||||
<input type="checkbox" :checked="isSelectAll" @change="selectAll" />
|
||||
</div>
|
||||
<div
|
||||
class="c-fault-mgmt-item-header c-fault-mgmt__list-header-results c-fault-mgmt__list-severity"
|
||||
|
||||
@@ -23,7 +23,12 @@
|
||||
<template>
|
||||
<div class="c-fault-mgmt__list data-selectable" :class="classesFromState">
|
||||
<div class="c-fault-mgmt-item c-fault-mgmt__list-checkbox">
|
||||
<input type="checkbox" :checked="isSelected" @input="toggleSelected" />
|
||||
<input
|
||||
type="checkbox"
|
||||
:aria-label="checkBoxAriaLabel"
|
||||
:checked="isSelected"
|
||||
@change="toggleSelected"
|
||||
/>
|
||||
</div>
|
||||
<div class="c-fault-mgmt-item">
|
||||
<div
|
||||
@@ -60,6 +65,7 @@
|
||||
<button
|
||||
class="c-fault-mgmt__list-action-button l-browse-bar__actions c-icon-button icon-3-dots"
|
||||
title="Disposition Actions"
|
||||
aria-label="Disposition Actions"
|
||||
@click="showActionMenu"
|
||||
></button>
|
||||
</div>
|
||||
@@ -86,13 +92,14 @@ export default {
|
||||
},
|
||||
isSelected: {
|
||||
type: Boolean,
|
||||
default: () => {
|
||||
return false;
|
||||
}
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: ['acknowledge-selected', 'shelve-selected', 'toggle-selected'],
|
||||
emits: ['acknowledge-selected', 'shelve-selected', 'toggle-selected', 'clear-all-selected'],
|
||||
computed: {
|
||||
checkBoxAriaLabel() {
|
||||
return `Select fault: ${this.fault.name}`;
|
||||
},
|
||||
classesFromState() {
|
||||
const exclusiveStates = [
|
||||
{
|
||||
@@ -171,6 +178,7 @@ export default {
|
||||
name: 'Acknowledge',
|
||||
description: '',
|
||||
onItemClicked: (e) => {
|
||||
this.clearAllSelected();
|
||||
this.$emit('acknowledge-selected', [this.fault]);
|
||||
}
|
||||
},
|
||||
@@ -179,6 +187,7 @@ export default {
|
||||
name: 'Shelve',
|
||||
description: '',
|
||||
onItemClicked: () => {
|
||||
this.clearAllSelected();
|
||||
this.$emit('shelve-selected', [this.fault], { shelved: true });
|
||||
}
|
||||
},
|
||||
@@ -188,6 +197,7 @@ export default {
|
||||
name: 'Unshelve',
|
||||
description: '',
|
||||
onItemClicked: () => {
|
||||
this.clearAllSelected();
|
||||
this.$emit('shelve-selected', [this.fault], { shelved: false });
|
||||
}
|
||||
}
|
||||
@@ -202,6 +212,9 @@ export default {
|
||||
};
|
||||
|
||||
this.$emit('toggle-selected', faultData);
|
||||
},
|
||||
clearAllSelected() {
|
||||
this.$emit('clear-all-selected');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,307 +0,0 @@
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="c-faults-list-view">
|
||||
<FaultManagementSearch
|
||||
:search-term="searchTerm"
|
||||
@filter-changed="updateFilter"
|
||||
@update-search-term="updateSearchTerm"
|
||||
/>
|
||||
|
||||
<FaultManagementToolbar
|
||||
v-if="showToolbar"
|
||||
:selected-faults="selectedFaults"
|
||||
@acknowledge-selected="toggleAcknowledgeSelected"
|
||||
@shelve-selected="toggleShelveSelected"
|
||||
/>
|
||||
|
||||
<div class="c-faults-list-view-header-item-container-wrapper">
|
||||
<div class="c-faults-list-view-header-item-container">
|
||||
<FaultManagementListHeader
|
||||
class="header"
|
||||
:selected-faults="Object.values(selectedFaults)"
|
||||
:total-faults-count="filteredFaultsList.length"
|
||||
@select-all="selectAll"
|
||||
@sort-changed="sortChanged"
|
||||
/>
|
||||
|
||||
<div class="c-faults-list-view-item-body">
|
||||
<template v-if="filteredFaultsList.length > 0">
|
||||
<FaultManagementListItem
|
||||
v-for="fault of filteredFaultsList"
|
||||
:key="fault.id"
|
||||
:fault="fault"
|
||||
:is-selected="isSelected(fault)"
|
||||
@toggle-selected="toggleSelected"
|
||||
@acknowledge-selected="toggleAcknowledgeSelected"
|
||||
@shelve-selected="toggleShelveSelected"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS, FILTER_ITEMS, SORT_ITEMS } from './constants.js';
|
||||
import FaultManagementListHeader from './FaultManagementListHeader.vue';
|
||||
import FaultManagementListItem from './FaultManagementListItem.vue';
|
||||
import FaultManagementSearch from './FaultManagementSearch.vue';
|
||||
import FaultManagementToolbar from './FaultManagementToolbar.vue';
|
||||
|
||||
const SEARCH_KEYS = [
|
||||
'id',
|
||||
'triggerValueInfo',
|
||||
'currentValueInfo',
|
||||
'triggerTime',
|
||||
'severity',
|
||||
'name',
|
||||
'shortDescription',
|
||||
'namespace'
|
||||
];
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FaultManagementListHeader,
|
||||
FaultManagementListItem,
|
||||
FaultManagementSearch,
|
||||
FaultManagementToolbar
|
||||
},
|
||||
inject: ['openmct', 'domainObject'],
|
||||
props: {
|
||||
faultsList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
filterIndex: 0,
|
||||
searchTerm: '',
|
||||
selectedFaults: {},
|
||||
sortBy: Object.values(SORT_ITEMS)[0].value
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
filteredFaultsList() {
|
||||
const filterName = FILTER_ITEMS[this.filterIndex];
|
||||
let list = this.faultsList;
|
||||
|
||||
// Exclude shelved alarms from all views except the Shelved view
|
||||
if (filterName !== 'Shelved') {
|
||||
list = list.filter((fault) => fault.shelved !== true);
|
||||
}
|
||||
|
||||
if (filterName === 'Acknowledged') {
|
||||
list = list.filter((fault) => fault.acknowledged);
|
||||
} else if (filterName === 'Unacknowledged') {
|
||||
list = list.filter((fault) => !fault.acknowledged);
|
||||
} else if (filterName === 'Shelved') {
|
||||
list = list.filter((fault) => fault.shelved);
|
||||
}
|
||||
|
||||
if (this.searchTerm.length > 0) {
|
||||
list = list.filter(this.filterUsingSearchTerm);
|
||||
}
|
||||
|
||||
list.sort(SORT_ITEMS[this.sortBy].sortFunction);
|
||||
|
||||
return list;
|
||||
},
|
||||
showToolbar() {
|
||||
return this.openmct.faults.supportsActions();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
filterUsingSearchTerm(fault) {
|
||||
if (!fault) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let match = false;
|
||||
|
||||
SEARCH_KEYS.forEach((key) => {
|
||||
if (fault[key]?.toString().toLowerCase().includes(this.searchTerm)) {
|
||||
match = true;
|
||||
}
|
||||
});
|
||||
|
||||
return match;
|
||||
},
|
||||
isSelected(fault) {
|
||||
return Boolean(this.selectedFaults[fault.id]);
|
||||
},
|
||||
selectAll(toggle = false) {
|
||||
this.faultsList.forEach((fault) => {
|
||||
const faultData = {
|
||||
fault,
|
||||
selected: toggle
|
||||
};
|
||||
this.toggleSelected(faultData);
|
||||
});
|
||||
},
|
||||
sortChanged(sort) {
|
||||
this.sortBy = sort.value;
|
||||
},
|
||||
toggleSelected({ fault, selected = false }) {
|
||||
if (selected) {
|
||||
this.selectedFaults[fault.id] = fault;
|
||||
} else {
|
||||
delete this.selectedFaults[fault.id];
|
||||
}
|
||||
|
||||
const selectedFaults = Object.values(this.selectedFaults);
|
||||
this.openmct.selection.select(
|
||||
[
|
||||
{
|
||||
element: this.$el,
|
||||
context: {
|
||||
item: this.openmct.router.path[0]
|
||||
}
|
||||
},
|
||||
{
|
||||
element: this.$el,
|
||||
context: {
|
||||
selectedFaults
|
||||
}
|
||||
}
|
||||
],
|
||||
false
|
||||
);
|
||||
},
|
||||
toggleAcknowledgeSelected(faults = Object.values(this.selectedFaults)) {
|
||||
let title = '';
|
||||
if (faults.length > 1) {
|
||||
title = `Acknowledge ${faults.length} selected faults`;
|
||||
} else {
|
||||
title = `Acknowledge fault: ${faults[0].name}`;
|
||||
}
|
||||
|
||||
const formStructure = {
|
||||
title,
|
||||
sections: [
|
||||
{
|
||||
rows: [
|
||||
{
|
||||
key: 'comment',
|
||||
control: 'textarea',
|
||||
name: 'Optional comment',
|
||||
pattern: '\\S+',
|
||||
required: false,
|
||||
cssClass: 'l-input-lg',
|
||||
value: ''
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
buttons: {
|
||||
submit: {
|
||||
label: 'Acknowledge'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.openmct.forms.showForm(formStructure).then((data) => {
|
||||
Object.values(faults).forEach((selectedFault) => {
|
||||
this.openmct.faults.acknowledgeFault(selectedFault, data);
|
||||
});
|
||||
});
|
||||
|
||||
this.selectedFaults = {};
|
||||
},
|
||||
async toggleShelveSelected(faults = Object.values(this.selectedFaults), shelveData = {}) {
|
||||
const { shelved = true } = shelveData;
|
||||
if (shelved) {
|
||||
let title =
|
||||
faults.length > 1
|
||||
? `Shelve ${faults.length} selected faults`
|
||||
: `Shelve fault: ${faults[0].name}`;
|
||||
const formStructure = {
|
||||
title,
|
||||
sections: [
|
||||
{
|
||||
rows: [
|
||||
{
|
||||
key: 'comment',
|
||||
control: 'textarea',
|
||||
name: 'Optional comment',
|
||||
pattern: '\\S+',
|
||||
required: false,
|
||||
cssClass: 'l-input-lg',
|
||||
value: ''
|
||||
},
|
||||
{
|
||||
key: 'shelveDuration',
|
||||
control: 'select',
|
||||
name: 'Shelve duration',
|
||||
options: FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS,
|
||||
required: false,
|
||||
cssClass: 'l-input-lg',
|
||||
value: FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS[0].value
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
buttons: {
|
||||
submit: {
|
||||
label: 'Shelve'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = await this.openmct.forms.showForm(formStructure);
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
|
||||
shelveData.comment = data.comment || '';
|
||||
shelveData.shelveDuration =
|
||||
data.shelveDuration !== undefined
|
||||
? data.shelveDuration
|
||||
: FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS[0].value;
|
||||
} else {
|
||||
shelveData = {
|
||||
shelved: false
|
||||
};
|
||||
}
|
||||
|
||||
Object.values(faults).forEach((selectedFault) => {
|
||||
this.openmct.faults.shelveFault(selectedFault, shelveData);
|
||||
});
|
||||
|
||||
this.selectedFaults = {};
|
||||
},
|
||||
updateFilter(filter) {
|
||||
this.selectAll();
|
||||
|
||||
this.filterIndex = filter.model.options.findIndex((option) => option.value === filter.value);
|
||||
},
|
||||
updateSearchTerm(term = '') {
|
||||
this.searchTerm = term.toLowerCase();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -24,20 +24,22 @@
|
||||
<div class="c-fault-mgmt__toolbar">
|
||||
<button
|
||||
class="c-icon-button icon-check"
|
||||
title="Acknowledge selected faults"
|
||||
:title="acknowledgeButtonLabel"
|
||||
:aria-label="acknowledgeButtonLabel"
|
||||
:disabled="disableAcknowledge"
|
||||
@click="acknowledgeSelected"
|
||||
>
|
||||
<div title="Acknowledge selected faults" class="c-icon-button__label">Acknowledge</div>
|
||||
<div class="c-icon-button__label">Acknowledge</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="c-icon-button icon-timer"
|
||||
title="Shelve selected faults"
|
||||
:title="shelveButtonLabel"
|
||||
:aria-label="shelveButtonLabel"
|
||||
:disabled="disableShelve"
|
||||
@click="shelveSelected"
|
||||
>
|
||||
<div title="Shelve selected items" class="c-icon-button__label">Shelve</div>
|
||||
<div class="c-icon-button__label">Shelve</div>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -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);
|
||||
|
||||
@@ -21,23 +21,123 @@
|
||||
-->
|
||||
|
||||
<template>
|
||||
<FaultManagementListView :faults-list="faultsList" />
|
||||
<div class="c-faults-list-view">
|
||||
<FaultManagementSearch
|
||||
:search-term="searchTerm"
|
||||
@filter-changed="updateFilter"
|
||||
@update-search-term="updateSearchTerm"
|
||||
/>
|
||||
|
||||
<FaultManagementToolbar
|
||||
v-if="showToolbar"
|
||||
:selected-faults="selectedFaults"
|
||||
@acknowledge-selected="toggleAcknowledgeSelected"
|
||||
@shelve-selected="toggleShelveSelected"
|
||||
/>
|
||||
|
||||
<div class="c-faults-list-view-header-item-container-wrapper">
|
||||
<div class="c-faults-list-view-header-item-container">
|
||||
<FaultManagementListHeader
|
||||
class="header"
|
||||
:selected-faults="selectedFaults"
|
||||
:total-faults-count="filteredFaultsList.length"
|
||||
@select-all="selectAll"
|
||||
@sort-changed="sortChanged"
|
||||
/>
|
||||
|
||||
<div class="c-faults-list-view-item-body">
|
||||
<template v-if="filteredFaultsList.length > 0">
|
||||
<FaultManagementListItem
|
||||
v-for="fault of filteredFaultsList"
|
||||
:key="fault.id"
|
||||
:fault="fault"
|
||||
:is-selected="isSelected(fault)"
|
||||
@toggle-selected="toggleSelected"
|
||||
@acknowledge-selected="toggleAcknowledgeSelected"
|
||||
@shelve-selected="toggleShelveSelected"
|
||||
@clear-all-selected="resetSelectedFaultMap"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { FAULT_MANAGEMENT_ALARMS, FAULT_MANAGEMENT_GLOBAL_ALARMS } from './constants.js';
|
||||
import FaultManagementListView from './FaultManagementListView.vue';
|
||||
import {
|
||||
FAULT_MANAGEMENT_ALARMS,
|
||||
FAULT_MANAGEMENT_GLOBAL_ALARMS,
|
||||
FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS,
|
||||
FILTER_ITEMS,
|
||||
SORT_ITEMS
|
||||
} from './constants.js';
|
||||
import FaultManagementListHeader from './FaultManagementListHeader.vue';
|
||||
import FaultManagementListItem from './FaultManagementListItem.vue';
|
||||
import FaultManagementSearch from './FaultManagementSearch.vue';
|
||||
import FaultManagementToolbar from './FaultManagementToolbar.vue';
|
||||
|
||||
const SEARCH_KEYS = [
|
||||
'id',
|
||||
'triggerValueInfo',
|
||||
'currentValueInfo',
|
||||
'triggerTime',
|
||||
'severity',
|
||||
'name',
|
||||
'shortDescription',
|
||||
'namespace'
|
||||
];
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FaultManagementListView
|
||||
FaultManagementListHeader,
|
||||
FaultManagementListItem,
|
||||
FaultManagementSearch,
|
||||
FaultManagementToolbar
|
||||
},
|
||||
inject: ['openmct', 'domainObject'],
|
||||
data() {
|
||||
return {
|
||||
faultsList: []
|
||||
faultsList: [],
|
||||
filterIndex: 0,
|
||||
searchTerm: '',
|
||||
selectedFaultMap: {},
|
||||
sortBy: Object.values(SORT_ITEMS)[0].value
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
selectedFaults() {
|
||||
return Object.values(this.selectedFaultMap);
|
||||
},
|
||||
filteredFaultsList() {
|
||||
const filterName = FILTER_ITEMS[this.filterIndex];
|
||||
let list = this.faultsList;
|
||||
|
||||
// Exclude shelved alarms from all views except the Shelved view
|
||||
if (filterName !== 'Shelved') {
|
||||
list = list.filter((fault) => fault.shelved !== true);
|
||||
}
|
||||
|
||||
if (filterName === 'Acknowledged') {
|
||||
list = list.filter((fault) => fault.acknowledged);
|
||||
} else if (filterName === 'Unacknowledged') {
|
||||
list = list.filter((fault) => !fault.acknowledged);
|
||||
} else if (filterName === 'Shelved') {
|
||||
list = list.filter((fault) => fault.shelved);
|
||||
}
|
||||
|
||||
if (this.searchTerm.length > 0) {
|
||||
list = list.filter(this.filterUsingSearchTerm);
|
||||
}
|
||||
|
||||
list.sort(SORT_ITEMS[this.sortBy].sortFunction);
|
||||
|
||||
return list;
|
||||
},
|
||||
showToolbar() {
|
||||
return this.openmct.faults.supportsActions();
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.unsubscribe = this.openmct.faults.subscribe(this.domainObject, this.updateFault);
|
||||
},
|
||||
@@ -66,6 +166,181 @@ export default {
|
||||
this.faultsList = [];
|
||||
}
|
||||
});
|
||||
},
|
||||
filterUsingSearchTerm(fault) {
|
||||
if (!fault) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let match = false;
|
||||
|
||||
SEARCH_KEYS.forEach((key) => {
|
||||
if (fault[key]?.toString().toLowerCase().includes(this.searchTerm)) {
|
||||
match = true;
|
||||
}
|
||||
});
|
||||
|
||||
return match;
|
||||
},
|
||||
isSelected(fault) {
|
||||
return Boolean(this.selectedFaultMap[fault.id]);
|
||||
},
|
||||
selectAll(toggle = false) {
|
||||
this.faultsList.forEach((fault) => {
|
||||
const faultData = {
|
||||
fault,
|
||||
selected: toggle
|
||||
};
|
||||
this.toggleSelected(faultData);
|
||||
});
|
||||
},
|
||||
sortChanged(sort) {
|
||||
this.sortBy = sort.value;
|
||||
},
|
||||
toggleSelected({ fault, selected = false }) {
|
||||
if (selected) {
|
||||
this.selectedFaultMap[fault.id] = fault;
|
||||
} else {
|
||||
delete this.selectedFaultMap[fault.id];
|
||||
}
|
||||
|
||||
this.openmct.selection.select(
|
||||
[
|
||||
{
|
||||
element: this.$el,
|
||||
context: {
|
||||
item: this.openmct.router.path[0]
|
||||
}
|
||||
},
|
||||
{
|
||||
element: this.$el,
|
||||
context: {
|
||||
selectedFaults: this.selectedFaults
|
||||
}
|
||||
}
|
||||
],
|
||||
false
|
||||
);
|
||||
},
|
||||
async toggleAcknowledgeSelected(faults = this.selectedFaults) {
|
||||
let title = '';
|
||||
if (faults.length > 1) {
|
||||
title = `Acknowledge ${faults.length} selected faults`;
|
||||
} else if (faults.length === 1) {
|
||||
title = `Acknowledge fault: ${faults[0].name}`;
|
||||
}
|
||||
|
||||
const formStructure = {
|
||||
title,
|
||||
sections: [
|
||||
{
|
||||
rows: [
|
||||
{
|
||||
key: 'comment',
|
||||
control: 'textarea',
|
||||
name: 'Optional comment',
|
||||
pattern: '\\S+',
|
||||
required: false,
|
||||
cssClass: 'l-input-lg',
|
||||
value: ''
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
buttons: {
|
||||
submit: {
|
||||
label: 'Acknowledge'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const data = await this.openmct.forms.showForm(formStructure);
|
||||
faults.forEach((fault) => {
|
||||
this.openmct.faults.acknowledgeFault(fault, data);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
this.resetSelectedFaultMap();
|
||||
}
|
||||
},
|
||||
resetSelectedFaultMap() {
|
||||
Object.keys(this.selectedFaultMap).forEach((key) => {
|
||||
delete this.selectedFaultMap[key];
|
||||
});
|
||||
},
|
||||
async toggleShelveSelected(faults = this.selectedFaults, shelveData = {}) {
|
||||
const { shelved = true } = shelveData;
|
||||
if (shelved) {
|
||||
let title =
|
||||
faults.length > 1
|
||||
? `Shelve ${faults.length} selected faults`
|
||||
: `Shelve fault: ${faults[0].name}`;
|
||||
const formStructure = {
|
||||
title,
|
||||
sections: [
|
||||
{
|
||||
rows: [
|
||||
{
|
||||
key: 'comment',
|
||||
control: 'textarea',
|
||||
name: 'Optional comment',
|
||||
pattern: '\\S+',
|
||||
required: false,
|
||||
cssClass: 'l-input-lg',
|
||||
value: ''
|
||||
},
|
||||
{
|
||||
key: 'shelveDuration',
|
||||
control: 'select',
|
||||
name: 'Shelve duration',
|
||||
options: FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS,
|
||||
required: false,
|
||||
cssClass: 'l-input-lg',
|
||||
value: FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS[0].value
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
buttons: {
|
||||
submit: {
|
||||
label: 'Shelve'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = await this.openmct.forms.showForm(formStructure);
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
|
||||
shelveData.comment = data.comment || '';
|
||||
shelveData.shelveDuration =
|
||||
data.shelveDuration !== undefined
|
||||
? data.shelveDuration
|
||||
: FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS[0].value;
|
||||
} else {
|
||||
shelveData = {
|
||||
shelved: false
|
||||
};
|
||||
}
|
||||
|
||||
Object.values(faults).forEach((selectedFault) => {
|
||||
this.openmct.faults.shelveFault(selectedFault, shelveData);
|
||||
});
|
||||
|
||||
this.selectedFaultMap = {};
|
||||
},
|
||||
updateFilter(filter) {
|
||||
this.selectAll();
|
||||
|
||||
this.filterIndex = filter.model.options.findIndex((option) => option.value === filter.value);
|
||||
},
|
||||
updateSearchTerm(term = '') {
|
||||
this.searchTerm = term.toLowerCase();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -201,10 +201,15 @@ export default {
|
||||
|
||||
if (this.compositionCollection) {
|
||||
this.compositionCollection.on('add', this.subscribeToStaleness);
|
||||
this.compositionCollection.on('remove', this.triggerUnsubscribeFromStaleness);
|
||||
this.compositionCollection.on('remove', this.removeSubscription);
|
||||
this.compositionCollection.load();
|
||||
}
|
||||
},
|
||||
removeSubscription(identifier) {
|
||||
this.triggerUnsubscribeFromStaleness({
|
||||
identifier
|
||||
});
|
||||
},
|
||||
loadingUpdated(loading) {
|
||||
this.loading = loading;
|
||||
this.$emit('loading-updated', ...arguments);
|
||||
@@ -212,7 +217,7 @@ export default {
|
||||
destroy() {
|
||||
if (this.compositionCollection) {
|
||||
this.compositionCollection.off('add', this.subscribeToStaleness);
|
||||
this.compositionCollection.off('remove', this.triggerUnsubscribeFromStaleness);
|
||||
this.compositionCollection.off('remove', this.removeSubscription);
|
||||
}
|
||||
|
||||
this.imageExporter = null;
|
||||
|
||||
@@ -39,13 +39,13 @@
|
||||
<div ref="limitArea" class="js-limit-area" aria-hidden="true">
|
||||
<limit-label
|
||||
v-for="(limitLabel, index) in visibleLimitLabels"
|
||||
:key="index"
|
||||
:key="`limitLabel-${limitLabel.limit.seriesKey}-${index}`"
|
||||
:point="limitLabel.point"
|
||||
:limit="limitLabel.limit"
|
||||
></limit-label>
|
||||
<limit-line
|
||||
v-for="(limitLine, index) in visibleLimitLines"
|
||||
:key="index"
|
||||
:key="`limitLine-${limitLine.limit.seriesKey}${index}`"
|
||||
:point="limitLine.point"
|
||||
:limit="limitLine.limit"
|
||||
></limit-line>
|
||||
@@ -201,9 +201,8 @@ export default {
|
||||
handler() {
|
||||
this.hiddenYAxisIds.forEach((id) => {
|
||||
this.resetYOffsetAndSeriesDataForYAxis(id);
|
||||
this.updateLimitLines();
|
||||
});
|
||||
this.scheduleDraw();
|
||||
this.scheduleDraw(true);
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
@@ -271,12 +270,19 @@ export default {
|
||||
this.listenTo(this.config.xAxis, 'change', this.redrawIfNotAlreadyHandled);
|
||||
this.config.series.forEach(this.onSeriesAdd, this);
|
||||
this.$emit('chart-loaded');
|
||||
|
||||
this.handleWindowResize = _.debounce(this.handleWindowResize, 250);
|
||||
this.chartResizeObserver = new ResizeObserver(this.handleWindowResize);
|
||||
this.chartResizeObserver.observe(this.$parent.$refs.chartContainer);
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.destroy();
|
||||
this.visibilityObserver.unobserve(this.chartContainer);
|
||||
},
|
||||
methods: {
|
||||
handleWindowResize() {
|
||||
this.scheduleDraw(true);
|
||||
},
|
||||
getConfig() {
|
||||
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
let config = configStore.get(configId);
|
||||
@@ -445,7 +451,6 @@ export default {
|
||||
|
||||
this.makeLimitLines(series);
|
||||
this.updateLimitLines();
|
||||
this.scheduleDraw();
|
||||
},
|
||||
resetAxisAndRedraw(newYAxisId, oldYAxisId, series) {
|
||||
if (!oldYAxisId) {
|
||||
@@ -459,8 +464,7 @@ export default {
|
||||
//Make the chart elements again for the new y-axis and offset
|
||||
this.makeChartElement(series);
|
||||
this.makeLimitLines(series);
|
||||
this.updateLimitLines();
|
||||
this.scheduleDraw();
|
||||
this.scheduleDraw(true);
|
||||
},
|
||||
destroy() {
|
||||
this.destroyCanvas();
|
||||
@@ -469,6 +473,10 @@ export default {
|
||||
this.limitLines.forEach((line) => line.destroy());
|
||||
this.pointSets.forEach((pointSet) => pointSet.destroy());
|
||||
this.alarmSets.forEach((alarmSet) => alarmSet.destroy());
|
||||
DrawLoader.releaseDrawAPI(this.drawAPI);
|
||||
if (this.chartResizeObserver) {
|
||||
this.chartResizeObserver.disconnect();
|
||||
}
|
||||
},
|
||||
resetYOffsetAndSeriesDataForYAxis(yAxisId) {
|
||||
delete this.offset[yAxisId].y;
|
||||
@@ -703,12 +711,11 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateLimitLines();
|
||||
this.scheduleDraw();
|
||||
this.scheduleDraw(true);
|
||||
},
|
||||
scheduleDraw() {
|
||||
scheduleDraw(updateLimitLines) {
|
||||
if (!this.drawScheduled) {
|
||||
const called = this.renderWhenVisible(this.draw);
|
||||
const called = this.renderWhenVisible(this.draw.bind(this, updateLimitLines));
|
||||
this.drawScheduled = called;
|
||||
if (!this.drawnOnce && called) {
|
||||
this.drawnOnce = true;
|
||||
@@ -716,7 +723,7 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
draw() {
|
||||
draw(updateLimitLines) {
|
||||
this.drawScheduled = false;
|
||||
if (this.isDestroyed || !this.chartVisible) {
|
||||
return;
|
||||
@@ -744,6 +751,11 @@ export default {
|
||||
this.prepareToDrawAnnotationSelections(id);
|
||||
}
|
||||
});
|
||||
// We must do the limit line drawing after the drawAPI has been cleared (which sets the height and width of the draw API)
|
||||
// and the viewport is updated so that we have the right height/width for limit line x and y calculations
|
||||
if (updateLimitLines) {
|
||||
this.updateLimitLines();
|
||||
}
|
||||
},
|
||||
updateViewport(yAxisId) {
|
||||
if (!this.chartVisible) {
|
||||
@@ -799,9 +811,12 @@ export default {
|
||||
pointSets.forEach(this.drawPoints, this);
|
||||
const alarmSets = this.alarmSets.filter(this.matchByYAxisId.bind(this, id));
|
||||
alarmSets.forEach(this.drawAlarmPoints, this);
|
||||
//console.timeEnd('📈 drawSeries');
|
||||
},
|
||||
updateLimitLines() {
|
||||
//reset
|
||||
this.visibleLimitLabels = [];
|
||||
this.visibleLimitLines = [];
|
||||
|
||||
this.config.series.models.forEach((series) => {
|
||||
const yAxisId = series.get('yAxisId');
|
||||
|
||||
@@ -820,11 +835,7 @@ export default {
|
||||
if (!this.drawAPI.origin) {
|
||||
return;
|
||||
}
|
||||
|
||||
let limitPointOverlap = [];
|
||||
//reset
|
||||
this.visibleLimitLabels = [];
|
||||
this.visibleLimitLines = [];
|
||||
|
||||
this.limitLines.forEach((limitLine) => {
|
||||
limitLine.limits.forEach((limit) => {
|
||||
|
||||
@@ -91,9 +91,16 @@
|
||||
</div>
|
||||
</li>
|
||||
<li class="grid-row">
|
||||
<div class="grid-cell label" title="Display limit lines">Limit lines</div>
|
||||
<div id="limit-lines-checkbox" class="grid-cell label" title="Display limit lines">
|
||||
Limit lines
|
||||
</div>
|
||||
<div class="grid-cell value">
|
||||
<input v-model="limitLines" type="checkbox" @change="updateForm('limitLines')" />
|
||||
<input
|
||||
v-model="limitLines"
|
||||
aria-labelledby="limit-lines-checkbox"
|
||||
type="checkbox"
|
||||
@change="updateForm('limitLines')"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
<li v-show="markers || alarmMarkers" class="grid-row">
|
||||
|
||||
@@ -165,7 +165,6 @@ export default {
|
||||
this.registerListeners(this.config);
|
||||
}
|
||||
this.listenTo(this.config.legend, 'change:expandByDefault', this.changeExpandDefault, this);
|
||||
this.initialize();
|
||||
},
|
||||
mounted() {
|
||||
this.loaded = true;
|
||||
@@ -182,16 +181,6 @@ export default {
|
||||
this.stopListening();
|
||||
},
|
||||
methods: {
|
||||
initialize() {
|
||||
if (this.domainObject.type === 'telemetry.plot.stacked') {
|
||||
this.objectComposition = this.openmct.composition.get(this.domainObject);
|
||||
this.objectComposition.on('add', this.addTelemetryObject);
|
||||
this.objectComposition.on('remove', this.removeTelemetryObject);
|
||||
this.objectComposition.load();
|
||||
} else {
|
||||
this.registerListeners(this.config);
|
||||
}
|
||||
},
|
||||
changeExpandDefault() {
|
||||
this.isLegendExpanded = this.config.legend.model.expandByDefault;
|
||||
this.legend.set('expanded', this.isLegendExpanded);
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<template>
|
||||
<div
|
||||
class="plot-legend-item"
|
||||
:aria-label="`Plot Legend Item for ${domainObject?.name}`"
|
||||
:aria-label="`Plot Legend Item for ${seriesName}`"
|
||||
:class="{
|
||||
'is-stale': isStale,
|
||||
'is-status--missing': isMissing
|
||||
@@ -36,9 +36,8 @@
|
||||
@mouseover.ctrl="showToolTip"
|
||||
@mouseleave="hideToolTip"
|
||||
>
|
||||
<span class="plot-series-color-swatch" :style="{ 'background-color': colorAsHexString }">
|
||||
</span>
|
||||
<span class="is-status__indicator" title="This item is missing or suspect"></span>
|
||||
<span class="plot-series-color-swatch" :style="{ 'background-color': colorAsHexString }" />
|
||||
<span class="is-status__indicator" title="This item is missing or suspect" />
|
||||
<span class="plot-series-name">{{ nameWithUnit }}</span>
|
||||
</div>
|
||||
<div
|
||||
@@ -89,6 +88,7 @@ export default {
|
||||
isMissing: false,
|
||||
colorAsHexString: '',
|
||||
nameWithUnit: '',
|
||||
seriesName: '',
|
||||
formattedYValue: '',
|
||||
formattedXValue: '',
|
||||
mctLimitStateClass: '',
|
||||
@@ -206,6 +206,9 @@ export default {
|
||||
const seriesIndexToRemove = this.seriesModels.findIndex(
|
||||
(series) => series.keyString === seriesToRemove.keyString
|
||||
);
|
||||
if (seriesIndexToRemove === -1) {
|
||||
return;
|
||||
}
|
||||
this.seriesModels.splice(seriesIndexToRemove, 1);
|
||||
},
|
||||
getSeries(keyStringToFind) {
|
||||
@@ -220,6 +223,7 @@ export default {
|
||||
|
||||
this.isMissing = seriesObject.domainObject.status === 'missing';
|
||||
this.colorAsHexString = seriesObject.get('color').asHexString();
|
||||
this.seriesName = seriesObject.domainObject.name;
|
||||
this.nameWithUnit = seriesObject.nameWithUnit();
|
||||
|
||||
const closest = seriesObject.closest;
|
||||
|
||||
@@ -135,7 +135,7 @@ export default {
|
||||
this.openmct.editor.off('isEditing', this.setEditState);
|
||||
if (this.composition) {
|
||||
this.composition.off('add', this.subscribeToStaleness);
|
||||
this.composition.off('remove', this.triggerUnsubscribeFromStaleness);
|
||||
this.composition.off('remove', this.removeSubscription);
|
||||
}
|
||||
|
||||
if (this.removeSelectable) {
|
||||
@@ -161,6 +161,11 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
removeSubscription(identifier) {
|
||||
this.triggerUnsubscribeFromStaleness({
|
||||
identifier
|
||||
});
|
||||
},
|
||||
updateView() {
|
||||
//If this object is not persistable, then package it with it's parent
|
||||
const plotObject = this.getPlotObject();
|
||||
@@ -172,7 +177,7 @@ export default {
|
||||
this.composition = this.openmct.composition.get(plotObject);
|
||||
|
||||
this.composition.on('add', this.subscribeToStaleness);
|
||||
this.composition.on('remove', this.triggerUnsubscribeFromStaleness);
|
||||
this.composition.on('remove', this.removeSubscription);
|
||||
this.composition.load();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<template>
|
||||
<form ref="fixedDeltaInput">
|
||||
<div class="c-tc-input-popup__input-grid">
|
||||
<div class="pr-time-label"><em>Start</em> Date</div>
|
||||
<div class="pr-time-label">Time</div>
|
||||
<div class="pr-time-label"></div>
|
||||
<div class="pr-time-label"><em>End</em> Date</div>
|
||||
<div class="pr-time-label">Time</div>
|
||||
<div class="pr-time-label"></div>
|
||||
<div class="pr-time-label pr-time-label-start-date"><em>Start</em> Date</div>
|
||||
<div class="pr-time-label pr-time-label-start-time">Time</div>
|
||||
<div class="pr-time-label pr-time-label-end-date"><em>End</em> Date</div>
|
||||
<div class="pr-time-label pr-time-label-end-time">Time</div>
|
||||
|
||||
<div class="pr-time-input pr-time-input--date pr-time-input--input-and-button">
|
||||
<div
|
||||
class="pr-time-input pr-time-input--date pr-time-input--input-and-button pr-time-input-start-date"
|
||||
>
|
||||
<input
|
||||
ref="startDate"
|
||||
v-model="formattedBounds.start"
|
||||
@@ -28,7 +28,7 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="pr-time-input pr-time-input--time">
|
||||
<div class="pr-time-input pr-time-input--time pr-time-input-start-time">
|
||||
<input
|
||||
ref="startTime"
|
||||
v-model="formattedBounds.startTime"
|
||||
@@ -43,7 +43,9 @@
|
||||
|
||||
<div class="pr-time-input pr-time-input__start-end-sep icon-arrows-right-left"></div>
|
||||
|
||||
<div class="pr-time-input pr-time-input--date pr-time-input--input-and-button">
|
||||
<div
|
||||
class="pr-time-input pr-time-input--date pr-time-input--input-and-button pr-time-input-end-date"
|
||||
>
|
||||
<input
|
||||
ref="endDate"
|
||||
v-model="formattedBounds.end"
|
||||
@@ -63,7 +65,7 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="pr-time-input pr-time-input--time">
|
||||
<div class="pr-time-input pr-time-input--time pr-time-input-end-time">
|
||||
<input
|
||||
ref="endTime"
|
||||
v-model="formattedBounds.endTime"
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
<template>
|
||||
<form ref="deltaInput">
|
||||
<div class="c-tc-input-popup__input-grid">
|
||||
<div class="pr-time-label icon-minus">Hrs</div>
|
||||
<div class="pr-time-label">Mins</div>
|
||||
<div class="pr-time-label">Secs</div>
|
||||
<div class="pr-time-label"></div>
|
||||
<div class="pr-time-label icon-plus">Hrs</div>
|
||||
<div class="pr-time-label">Mins</div>
|
||||
<div class="pr-time-label">Secs</div>
|
||||
<div class="pr-time-label"></div>
|
||||
<div class="pr-time-label icon-minus pr-time-label-minus-hrs">Hrs</div>
|
||||
<div class="pr-time-label pr-time-label-minus-mins">Mins</div>
|
||||
<div class="pr-time-label pr-time-label-minus-secs">Secs</div>
|
||||
<div class="pr-time-label icon-plus pr-time-label-plus-hrs">Hrs</div>
|
||||
<div class="pr-time-label pr-time-label-plus-mins">Mins</div>
|
||||
<div class="pr-time-label pr-time-label-plus-secs">Secs</div>
|
||||
|
||||
<div class="pr-time-input">
|
||||
<div class="pr-time-input pr-time-input-minus-hrs">
|
||||
<input
|
||||
ref="startInputHrs"
|
||||
v-model="startInputHrs"
|
||||
@@ -29,7 +27,7 @@
|
||||
/>
|
||||
<b>:</b>
|
||||
</div>
|
||||
<div class="pr-time-input">
|
||||
<div class="pr-time-input pr-time-input-minus-mins">
|
||||
<input
|
||||
ref="startInputMins"
|
||||
v-model="startInputMins"
|
||||
@@ -48,7 +46,7 @@
|
||||
/>
|
||||
<b>:</b>
|
||||
</div>
|
||||
<div class="pr-time-input">
|
||||
<div class="pr-time-input pr-time-input-minus-secs">
|
||||
<input
|
||||
ref="startInputSecs"
|
||||
v-model="startInputSecs"
|
||||
@@ -69,7 +67,7 @@
|
||||
|
||||
<div class="pr-time-input pr-time-input__start-end-sep icon-arrows-right-left"></div>
|
||||
|
||||
<div class="pr-time-input">
|
||||
<div class="pr-time-input pr-time-input-plus-hrs">
|
||||
<input
|
||||
ref="endInputHrs"
|
||||
v-model="endInputHrs"
|
||||
@@ -88,7 +86,7 @@
|
||||
/>
|
||||
<b>:</b>
|
||||
</div>
|
||||
<div class="pr-time-input">
|
||||
<div class="pr-time-input pr-time-input-plus-mins">
|
||||
<input
|
||||
ref="endInputMins"
|
||||
v-model="endInputMins"
|
||||
@@ -106,7 +104,7 @@
|
||||
/>
|
||||
<b>:</b>
|
||||
</div>
|
||||
<div class="pr-time-input">
|
||||
<div class="pr-time-input pr-time-input-plus-secs">
|
||||
<input
|
||||
ref="endInputSecs"
|
||||
v-model="endInputSecs"
|
||||
|
||||
@@ -604,23 +604,170 @@
|
||||
padding: cButtonPadding($compact: true);
|
||||
}
|
||||
}
|
||||
.pr-time{
|
||||
// FIXED TIME MODE
|
||||
&-label-start-date{
|
||||
grid-area: sDate;
|
||||
}
|
||||
&-label-start-time{
|
||||
grid-area: sTime;
|
||||
}
|
||||
&-input-start-date{
|
||||
grid-area: sDateInput;
|
||||
}
|
||||
&-input-start-time{
|
||||
grid-area: sTimeInput;
|
||||
}
|
||||
&-label-end-date{
|
||||
grid-area: eDate;
|
||||
}
|
||||
&-label-end-time{
|
||||
grid-area: eTime;
|
||||
|
||||
}
|
||||
&-input-end-date{
|
||||
grid-area: eDateInput;
|
||||
}
|
||||
&-input-end-time{
|
||||
grid-area: eTimeInput;
|
||||
}
|
||||
&-label-blank-grid{
|
||||
grid-area: blank;
|
||||
}
|
||||
|
||||
//REAL TIME MODE
|
||||
&-label-minus-hrs{
|
||||
grid-area: labelMinusHrs;
|
||||
}
|
||||
&-label-minus-mins{
|
||||
grid-area: labelMinusMins;
|
||||
}
|
||||
&-label-minus-secs{
|
||||
grid-area: labelMinusSecs;
|
||||
}
|
||||
&-label-plus-hrs{
|
||||
grid-area: labelPlusHrs;
|
||||
}
|
||||
&-label-plus-mins{
|
||||
grid-area: labelPlusMins;
|
||||
}
|
||||
&-label-plus-secs{
|
||||
grid-area: labelPlusSecs;
|
||||
}
|
||||
&-input-minus-hrs{
|
||||
grid-area: inputMinusHrs;
|
||||
}
|
||||
&-input-minus-mins{
|
||||
grid-area: inputMinusMins;
|
||||
}
|
||||
&-input-minus-secs{
|
||||
grid-area: inputMinusSecs;
|
||||
}
|
||||
&-input-plus-hrs{
|
||||
grid-area: inputPlusHrs;
|
||||
}
|
||||
&-input-plus-mins{
|
||||
grid-area: inputPlusMins;
|
||||
}
|
||||
&-input-plus-secs{
|
||||
grid-area: inputPlusSecs;
|
||||
}
|
||||
// USED FOR BOTH
|
||||
&-label-blank-grid{
|
||||
grid-area: empty;
|
||||
}
|
||||
&-input__start-end-sep{
|
||||
grid-area: arrowIcon;
|
||||
}
|
||||
&-input--buttons{
|
||||
grid-area: buttons;
|
||||
}
|
||||
}
|
||||
|
||||
&--fixed-mode {
|
||||
.c-tc-input-popup__input-grid {
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 2fr;
|
||||
grid-template-areas:
|
||||
"sDate sTime . eDate eTime ."
|
||||
"sDateInput sTimeInput arrowIcon eDateInput eTimeInput buttons";
|
||||
}
|
||||
@include phonePortrait(){
|
||||
.c-tc-input-popup__input-grid {
|
||||
grid-template-columns: repeat(2, max-content) 1fr;
|
||||
grid-template-areas:
|
||||
"sDate sTime ."
|
||||
"sDateInput sTimeInput ."
|
||||
"eDate eTime ."
|
||||
"eDateInput eTimeInput buttons";
|
||||
padding: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&--realtime-mode {
|
||||
.c-tc-input-popup__input-grid {
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 2fr;
|
||||
grid-template-areas:
|
||||
"labelMinusHrs labelMinusMins labelMinusSecs . labelPlusHrs labelPlusMins labelPlusSecs ."
|
||||
"inputMinusHrs inputMinusMins inputMinusSecs arrowIcon inputPlusHrs inputPlusMins inputPlusSecs buttons";
|
||||
}
|
||||
@include phonePortrait(){
|
||||
.c-tc-input-popup__input-grid {
|
||||
grid-template-columns: repeat(3, max-content) 1fr;
|
||||
grid-template-areas:
|
||||
"labelMinusHrs labelMinusMins labelMinusSecs ."
|
||||
"inputMinusHrs inputMinusMins inputMinusSecs ."
|
||||
"labelPlusHrs labelPlusMins labelPlusSecs ."
|
||||
"inputPlusHrs inputPlusMins inputPlusSecs buttons";
|
||||
padding: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__input-grid {
|
||||
display: grid;
|
||||
grid-column-gap: 3px;
|
||||
grid-row-gap: $interiorMargin;
|
||||
grid-column-gap: $interiorMarginSm;
|
||||
align-items: start;
|
||||
}
|
||||
}
|
||||
@include phonePortrait(){ // Additional styling for mobile portrait.
|
||||
.c-tc-input-popup{
|
||||
width: 100%;
|
||||
&__options{
|
||||
> * {
|
||||
overflow: hidden;
|
||||
[class*= 'ctrl-wrapper']{
|
||||
[class*='--menu'] {
|
||||
width: 100%;
|
||||
[class*='__label'] {
|
||||
@include ellipsize();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pr-time-input-end-time, .pr-time-input-start-time{
|
||||
> * {
|
||||
margin-right: $interiorMargin;
|
||||
}
|
||||
}
|
||||
.pr-time-input--buttons{
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.pr-time-input__start-end-sep{
|
||||
margin: auto;
|
||||
}
|
||||
.pr-time-input__start-end-sep{
|
||||
display: none;
|
||||
}
|
||||
.pr-time-input-start-date, .pr-time-input-end-date{
|
||||
width: max-content;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,18 @@
|
||||
</div>
|
||||
<div class="c-tli__graphic">
|
||||
<svg viewBox="0 0 100 100">
|
||||
<g aria-label="Activity in progress" class="c-tli__graphic__pie">
|
||||
<circle class="c-svg-progress__bg" r="50" cx="50" cy="50"></circle>
|
||||
<path ref="progressElement" class="c-svg-progress__progress"></path>
|
||||
<circle
|
||||
class="c-svg-progress__ticks"
|
||||
r="40"
|
||||
cx="50"
|
||||
cy="50"
|
||||
stroke-dasharray="3 7.472"
|
||||
></circle>
|
||||
<rect class="c-svg-progress__sweep-hand" x="48" y="18" width="4" height="27"></rect>
|
||||
</g>
|
||||
<path
|
||||
aria-label="Activity complete"
|
||||
class="c-tli__graphic__check"
|
||||
@@ -80,6 +92,7 @@ import _ from 'lodash';
|
||||
|
||||
import { TIME_CONTEXT_EVENTS } from '../../api/time/constants.js';
|
||||
import { CURRENT_CSS_SUFFIX, FUTURE_CSS_SUFFIX, PAST_CSS_SUFFIX } from './constants.js';
|
||||
import { updateProgress } from './svg-progress.js';
|
||||
|
||||
const ITEM_COLORS = {
|
||||
[CURRENT_CSS_SUFFIX]: '#ffcc00',
|
||||
@@ -212,6 +225,7 @@ export default {
|
||||
},
|
||||
followTimeContext() {
|
||||
this.timeContext.on(TIME_CONTEXT_EVENTS.tick, this.updateTimestamp);
|
||||
this.updateTimestamp(this.timeContext.now());
|
||||
},
|
||||
stopFollowingTimeContext() {
|
||||
if (this.timeContext) {
|
||||
@@ -220,6 +234,10 @@ export default {
|
||||
},
|
||||
updateTimestamp(time) {
|
||||
this.timestamp = time;
|
||||
const progressElement = this.$refs.progressElement;
|
||||
if (this.isInProgress && progressElement) {
|
||||
updateProgress(this.start, this.end, this.timestamp, progressElement);
|
||||
}
|
||||
this.formatItemLabel();
|
||||
},
|
||||
formatItemLabel() {
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
<list-item
|
||||
v-for="item in sortedItems"
|
||||
:key="item.key"
|
||||
:class="{ '--is-in-progress': persistedActivityStates[item.id] === 'in-progress' }"
|
||||
:item="item"
|
||||
:item-properties="itemProperties"
|
||||
@click.stop="setSelectionForActivity(item, $event.currentTarget)"
|
||||
|
||||
49
src/plugins/timelist/svg-progress.js
Normal file
49
src/plugins/timelist/svg-progress.js
Normal file
@@ -0,0 +1,49 @@
|
||||
const PI = Math.PI; // Use the built-in constant directly
|
||||
const DEGREES_TO_RADIANS = PI / 180; // Calculate the conversion factor
|
||||
|
||||
import { arc } from 'd3-shape';
|
||||
|
||||
const SVG_VB_SIZE = 100;
|
||||
const UPDATE_RATE_MS = 1000; // 1 Hz
|
||||
|
||||
function progToDegrees(progVal) {
|
||||
return (progVal / 100) * 360;
|
||||
}
|
||||
|
||||
function renderProgress(progressPercent, element) {
|
||||
let startAngleInDegrees = 0;
|
||||
let endAngleInDegrees = progToDegrees(progressPercent);
|
||||
|
||||
// Convert angles to radians for calculations
|
||||
const startAngleInRadians = startAngleInDegrees * DEGREES_TO_RADIANS;
|
||||
const endAngleInRadians = endAngleInDegrees * DEGREES_TO_RADIANS;
|
||||
|
||||
// d3's arc API does the work for us
|
||||
const progressArc = arc();
|
||||
progressArc.innerRadius(0);
|
||||
progressArc.outerRadius(SVG_VB_SIZE / 2);
|
||||
progressArc.startAngle(startAngleInRadians);
|
||||
progressArc.endAngle(endAngleInRadians);
|
||||
element.setAttribute('d', progressArc());
|
||||
}
|
||||
|
||||
export function updateProgress(start, end, timestamp, element) {
|
||||
const duration = end - start;
|
||||
const update_per_cycle = 100 / (duration / UPDATE_RATE_MS);
|
||||
let progressPercent = 0;
|
||||
if (timestamp > start) {
|
||||
// Now is after activity start datetime
|
||||
if (timestamp > end) {
|
||||
progressPercent = 100;
|
||||
} else {
|
||||
progressPercent = (1 - (end - timestamp) / duration) * 100;
|
||||
}
|
||||
}
|
||||
if (progressPercent < 100 && progressPercent > 0) {
|
||||
// If the remaining percent is less than update_per_cycle, round up to 100%.
|
||||
// Otherwise, increment by update_per_cycle.
|
||||
progressPercent =
|
||||
100 - progressPercent < update_per_cycle ? 100 : (progressPercent += update_per_cycle);
|
||||
}
|
||||
renderProgress(progressPercent, element);
|
||||
}
|
||||
@@ -29,7 +29,15 @@
|
||||
}
|
||||
|
||||
.c-list-item {
|
||||
/* Time Lists */
|
||||
/* Compact Time Lists; is a <tr> element */
|
||||
|
||||
@mixin sSelected($bgColor, $fgColor) {
|
||||
&[s-selected] {
|
||||
background: $bgColor !important;
|
||||
border: 1px solid $colorSelectedFg !important;
|
||||
color: $fgColor !important;
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
$p: $interiorMarginSm;
|
||||
@@ -37,19 +45,26 @@
|
||||
padding-bottom: $p;
|
||||
}
|
||||
|
||||
&.--is-past {
|
||||
@include sSelected(transparent, $colorPastFgEm);
|
||||
}
|
||||
|
||||
&.--is-current {
|
||||
@include sSelected($colorCurrentBg, $colorCurrentFgEm);
|
||||
background-color: $colorCurrentBg;
|
||||
border-top: 1px solid $colorCurrentBorder !important;
|
||||
color: $colorCurrentFgEm;
|
||||
}
|
||||
|
||||
&.--is-future {
|
||||
@include sSelected($colorFutureBg, $colorFutureFgEm);
|
||||
background-color: $colorFutureBg;
|
||||
border-top-color: $colorFutureBorder !important;
|
||||
color: $colorFutureFgEm;
|
||||
}
|
||||
|
||||
&.--is-in-progress {
|
||||
@include sSelected($colorInProgressBg, $colorInProgressFgEm);
|
||||
background-color: $colorInProgressBg;
|
||||
}
|
||||
|
||||
@@ -105,9 +120,10 @@
|
||||
grid-column-gap: $interiorMargin;
|
||||
|
||||
&[s-selected] {
|
||||
background: $colorSelectedBg !important;
|
||||
box-shadow: inset rgba($colorSelectedFg, 0.1) 0 0 0 1px;
|
||||
box-shadow: inset rgba($colorSelectedFg, 0.8) 0 0 0 1px;
|
||||
color: $colorSelectedFg !important;
|
||||
|
||||
@include styleTliEm($colorSelectedFg);
|
||||
}
|
||||
|
||||
@include styleTliEm($baseFgEm);
|
||||
@@ -308,6 +324,7 @@
|
||||
|
||||
&__progress {
|
||||
fill: $colorInProgressFgEm;
|
||||
transform: translateX(50%) translateY(50%);
|
||||
}
|
||||
|
||||
&__sweep-hand {
|
||||
|
||||
@@ -21,7 +21,13 @@
|
||||
-->
|
||||
<template>
|
||||
<div class="l-pane" :class="paneClasses">
|
||||
<div v-if="handle" class="l-pane__handle" @mousedown.prevent="startResizing"></div>
|
||||
<div
|
||||
v-if="handle"
|
||||
class="l-pane__handle"
|
||||
:aria-label="handleLabel"
|
||||
:aria-grabbed="resizing"
|
||||
@mousedown.prevent="startResizing"
|
||||
></div>
|
||||
<div class="l-pane__header">
|
||||
<span v-if="label" class="l-pane__label">{{ label }}</span>
|
||||
<slot name="controls"></slot>
|
||||
@@ -88,6 +94,9 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
handleLabel() {
|
||||
return `Resize ${this.label} Pane`;
|
||||
},
|
||||
isCollapsable() {
|
||||
return this.hideParam?.length > 0;
|
||||
},
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
transition: opacity 150ms ease;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
overflow: hidden; // Prevents toolbar from extending into Inspector
|
||||
|
||||
> * {
|
||||
min-width: 0 !important;
|
||||
@@ -93,6 +93,7 @@
|
||||
&__contents {
|
||||
flex: 1 1 100%;
|
||||
opacity: 1;
|
||||
overflow: hidden;
|
||||
pointer-events: inherit;
|
||||
transition: opacity 250ms ease 250ms;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user