mirror of
https://github.com/nasa/openmct.git
synced 2025-06-13 04:38:15 +00:00
Merge remote-tracking branch 'origin/master' into telemetry-comps
This commit is contained in:
@ -5,7 +5,7 @@ orbs:
|
|||||||
executors:
|
executors:
|
||||||
pw-focal-development:
|
pw-focal-development:
|
||||||
docker:
|
docker:
|
||||||
- image: mcr.microsoft.com/playwright:v1.47.2-focal
|
- image: mcr.microsoft.com/playwright:v1.48.1-focal
|
||||||
environment:
|
environment:
|
||||||
NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed
|
NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed
|
||||||
PERCY_POSTINSTALL_BROWSER: 'true' # Needed to store the percy browser in cache deps
|
PERCY_POSTINSTALL_BROWSER: 'true' # Needed to store the percy browser in cache deps
|
||||||
@ -198,7 +198,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- build_and_install:
|
- build_and_install:
|
||||||
node-version: lts/hydrogen
|
node-version: lts/hydrogen
|
||||||
- run: npx playwright@1.47.2 install #Necessary for bare ubuntu machine
|
- run: npx playwright@1.48.1 install #Necessary for bare ubuntu machine
|
||||||
- run: |
|
- run: |
|
||||||
export $(cat src/plugins/persistence/couch/.env.ci | xargs)
|
export $(cat src/plugins/persistence/couch/.env.ci | xargs)
|
||||||
docker compose -f src/plugins/persistence/couch/couchdb-compose.yaml up --detach
|
docker compose -f src/plugins/persistence/couch/couchdb-compose.yaml up --detach
|
||||||
@ -286,8 +286,8 @@ workflows:
|
|||||||
overall-circleci-commit-status: #These jobs run on every commit
|
overall-circleci-commit-status: #These jobs run on every commit
|
||||||
jobs:
|
jobs:
|
||||||
- lint:
|
- lint:
|
||||||
name: node20-lint
|
name: node22-lint
|
||||||
node-version: lts/iron
|
node-version: '22'
|
||||||
- unit-test:
|
- unit-test:
|
||||||
name: node18-chrome
|
name: node18-chrome
|
||||||
node-version: lts/hydrogen
|
node-version: lts/hydrogen
|
||||||
@ -304,8 +304,8 @@ workflows:
|
|||||||
the-nightly: #These jobs do not run on PRs, but against master at night
|
the-nightly: #These jobs do not run on PRs, but against master at night
|
||||||
jobs:
|
jobs:
|
||||||
- unit-test:
|
- unit-test:
|
||||||
name: node20-chrome-nightly
|
name: node22-chrome-nightly
|
||||||
node-version: lts/iron
|
node-version: '22'
|
||||||
- unit-test:
|
- unit-test:
|
||||||
name: node18-chrome
|
name: node18-chrome
|
||||||
node-version: lts/hydrogen
|
node-version: lts/hydrogen
|
||||||
|
2
.github/workflows/e2e-couchdb.yml
vendored
2
.github/workflows/e2e-couchdb.yml
vendored
@ -37,7 +37,7 @@ jobs:
|
|||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- run: npx playwright@1.47.2 install
|
- run: npx playwright@1.48.1 install
|
||||||
|
|
||||||
- name: Start CouchDB Docker Container and Init with Setup Scripts
|
- name: Start CouchDB Docker Container and Init with Setup Scripts
|
||||||
run: |
|
run: |
|
||||||
|
2
.github/workflows/e2e-flakefinder.yml
vendored
2
.github/workflows/e2e-flakefinder.yml
vendored
@ -30,7 +30,7 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-node-
|
${{ runner.os }}-node-
|
||||||
|
|
||||||
- run: npx playwright@1.47.2 install
|
- run: npx playwright@1.48.1 install
|
||||||
- run: npm ci --no-audit --progress=false
|
- run: npm ci --no-audit --progress=false
|
||||||
|
|
||||||
- name: Run E2E Tests (Repeated 10 Times)
|
- name: Run E2E Tests (Repeated 10 Times)
|
||||||
|
2
.github/workflows/e2e-perf.yml
vendored
2
.github/workflows/e2e-perf.yml
vendored
@ -28,7 +28,7 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-node-
|
${{ runner.os }}-node-
|
||||||
|
|
||||||
- run: npx playwright@1.47.2 install
|
- run: npx playwright@1.48.1 install
|
||||||
- run: npm ci --no-audit --progress=false
|
- run: npm ci --no-audit --progress=false
|
||||||
- run: npm run test:perf:localhost
|
- run: npm run test:perf:localhost
|
||||||
- run: npm run test:perf:contract
|
- run: npm run test:perf:contract
|
||||||
|
116
.github/workflows/release.yml
vendored
116
.github/workflows/release.yml
vendored
@ -1,116 +0,0 @@
|
|||||||
# GitHub Actions Workflow for Automated Releases
|
|
||||||
|
|
||||||
name: Automated Release Workflow
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
# Nightly builds at 6 PM PST every day
|
|
||||||
- cron: '0 2 * * *'
|
|
||||||
release:
|
|
||||||
types:
|
|
||||||
- created
|
|
||||||
- published
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
nightly-build:
|
|
||||||
if: github.event_name == 'schedule'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: Nightly Build and Release
|
|
||||||
steps:
|
|
||||||
- name: Checkout Repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set Up Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 'lts/iron' # Specify your Node.js version
|
|
||||||
registry-url: 'https://registry.npmjs.org/'
|
|
||||||
|
|
||||||
- name: Install Dependencies
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Bump Version for Nightly
|
|
||||||
id: bump_version
|
|
||||||
run: |
|
|
||||||
PACKAGE_VERSION=$(node -p "require('./package.json').version")
|
|
||||||
DATE=$(date +%Y%m%d)
|
|
||||||
NIGHTLY_VERSION=$(echo $PACKAGE_VERSION | awk -F. -v OFS=. '{$NF+=1; print}')-nightly-$DATE
|
|
||||||
echo "NIGHTLY_VERSION=${NIGHTLY_VERSION}" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Update package.json
|
|
||||||
run: |
|
|
||||||
npm version $NIGHTLY_VERSION --no-git-tag-version
|
|
||||||
git config user.name "github-actions[bot]"
|
|
||||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
||||||
git add package.json
|
|
||||||
git commit -m "chore: bump version to $NIGHTLY_VERSION for nightly build"
|
|
||||||
|
|
||||||
- name: Push Changes
|
|
||||||
uses: ad-m/github-push-action@v0.6.0
|
|
||||||
with:
|
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
branch: ${{ github.ref }}
|
|
||||||
|
|
||||||
- name: Build Project
|
|
||||||
run: npm run build:prod
|
|
||||||
|
|
||||||
- name: Publish Nightly to NPM
|
|
||||||
run: |
|
|
||||||
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
|
|
||||||
npm publish --access public --tag nightly
|
|
||||||
env:
|
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
||||||
|
|
||||||
prerelease-build:
|
|
||||||
if: github.event.release.prerelease == true
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: Pre-release (Beta) Build and Publish
|
|
||||||
steps:
|
|
||||||
- name: Checkout Repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set Up Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '16' # Specify your Node.js version
|
|
||||||
registry-url: 'https://registry.npmjs.org/'
|
|
||||||
|
|
||||||
- name: Install Dependencies
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Build Project
|
|
||||||
run: npm run build:prod
|
|
||||||
|
|
||||||
- name: Publish Beta to NPM
|
|
||||||
run: |
|
|
||||||
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
|
|
||||||
npm publish --access public --tag beta
|
|
||||||
env:
|
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
||||||
|
|
||||||
stable-release-build:
|
|
||||||
if: github.event.release.prerelease == false
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: Stable Release Build and Publish
|
|
||||||
steps:
|
|
||||||
- name: Checkout Repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set Up Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '16' # Specify your Node.js version
|
|
||||||
registry-url: 'https://registry.npmjs.org/'
|
|
||||||
|
|
||||||
- name: Install Dependencies
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Build Project
|
|
||||||
run: npm run build:prod
|
|
||||||
|
|
||||||
- name: Publish to NPM
|
|
||||||
run: |
|
|
||||||
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
|
|
||||||
npm publish --access public
|
|
||||||
env:
|
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
@ -510,6 +510,10 @@ async function setTimeConductorBounds(page, { submitChanges = true, ...bounds })
|
|||||||
// Open the time conductor popup
|
// Open the time conductor popup
|
||||||
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
|
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
|
||||||
|
|
||||||
|
// FIXME: https://github.com/nasa/openmct/pull/7818
|
||||||
|
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
if (startDate) {
|
if (startDate) {
|
||||||
await page.getByLabel('Start date').fill(startDate);
|
await page.getByLabel('Start date').fill(startDate);
|
||||||
}
|
}
|
||||||
|
@ -129,6 +129,7 @@ export async function setBoundsToSpanAllActivities(page, planJson, planObjectUrl
|
|||||||
*/
|
*/
|
||||||
export function getEarliestStartTime(planJson) {
|
export function getEarliestStartTime(planJson) {
|
||||||
const activities = Object.values(planJson).flat();
|
const activities = Object.values(planJson).flat();
|
||||||
|
|
||||||
return Math.min(...activities.map((activity) => activity.start));
|
return Math.min(...activities.map((activity) => activity.start));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,6 +140,7 @@ export function getEarliestStartTime(planJson) {
|
|||||||
*/
|
*/
|
||||||
export function getLatestEndTime(planJson) {
|
export function getLatestEndTime(planJson) {
|
||||||
const activities = Object.values(planJson).flat();
|
const activities = Object.values(planJson).flat();
|
||||||
|
|
||||||
return Math.max(...activities.map((activity) => activity.end));
|
return Math.max(...activities.map((activity) => activity.end));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -151,6 +153,7 @@ export function getFirstActivity(planJson) {
|
|||||||
const groups = Object.keys(planJson);
|
const groups = Object.keys(planJson);
|
||||||
const firstGroupKey = groups[0];
|
const firstGroupKey = groups[0];
|
||||||
const firstGroupItems = planJson[firstGroupKey];
|
const firstGroupItems = planJson[firstGroupKey];
|
||||||
|
|
||||||
return firstGroupItems[0];
|
return firstGroupItems[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@percy/cli": "1.27.4",
|
"@percy/cli": "1.27.4",
|
||||||
"@percy/playwright": "1.0.4",
|
"@percy/playwright": "1.0.4",
|
||||||
"@playwright/test": "1.47.2",
|
"@playwright/test": "1.48.1",
|
||||||
"@axe-core/playwright": "4.8.5"
|
"@axe-core/playwright": "4.8.5"
|
||||||
},
|
},
|
||||||
"author": {
|
"author": {
|
||||||
|
BIN
e2e/test-data/rick space roll.jpg
Normal file
BIN
e2e/test-data/rick space roll.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
@ -96,9 +96,6 @@ test.describe('Example Imagery Object', () => {
|
|||||||
expect(newPage.url()).toContain('.jpg');
|
expect(newPage.url()).toContain('.jpg');
|
||||||
});
|
});
|
||||||
|
|
||||||
// this requires CORS to be enabled in some fashion
|
|
||||||
test.fixme('Can right click on image and save it as a file', async ({ page }) => {});
|
|
||||||
|
|
||||||
test('Can adjust image brightness/contrast by dragging the sliders', async ({
|
test('Can adjust image brightness/contrast by dragging the sliders', async ({
|
||||||
page,
|
page,
|
||||||
browserName
|
browserName
|
||||||
|
@ -0,0 +1,93 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This test suite verifies modifying the image location of the example imagery object.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createDomainObjectWithDefaults } from '../../../../appActions.js';
|
||||||
|
import { expect, test } from '../../../../pluginFixtures.js';
|
||||||
|
|
||||||
|
test.describe('Example Imagery Object Custom Images', () => {
|
||||||
|
let exampleImagery;
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
//Go to baseURL
|
||||||
|
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||||
|
|
||||||
|
// Create a default 'Example Imagery' object
|
||||||
|
exampleImagery = await createDomainObjectWithDefaults(page, {
|
||||||
|
name: 'Example Imagery',
|
||||||
|
type: 'Example Imagery'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify that the created object is focused
|
||||||
|
await expect(page.locator('.l-browse-bar__object-name')).toContainText(exampleImagery.name);
|
||||||
|
await page.getByLabel('Focused Image Element').hover({ trial: true });
|
||||||
|
|
||||||
|
// Wait for image thumbnail auto-scroll to complete
|
||||||
|
await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport();
|
||||||
|
});
|
||||||
|
// this requires CORS to be enabled in some fashion
|
||||||
|
test.fixme('Can right click on image and save it as a file', async ({ page }) => {});
|
||||||
|
test('Can provide a custom image location for the example imagery object', async ({ page }) => {
|
||||||
|
// Modify Example Imagery to create a really stable image which will never let us down
|
||||||
|
await page.getByRole('button', { name: 'More actions' }).click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Edit Properties...' }).click();
|
||||||
|
await page
|
||||||
|
.locator('#imageLocation-textarea')
|
||||||
|
.fill(
|
||||||
|
'https://raw.githubusercontent.com/nasa/openmct/554f77c42fec81cf0f63e62b278012cb08d82af9/e2e/test-data/rick.jpg,https://raw.githubusercontent.com/nasa/openmct/554f77c42fec81cf0f63e62b278012cb08d82af9/e2e/test-data/rick.jpg'
|
||||||
|
);
|
||||||
|
await page.getByRole('button', { name: 'Save' }).click();
|
||||||
|
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||||
|
|
||||||
|
// Wait for the thumbnails to finish their scroll animation
|
||||||
|
// (Wait until the rightmost thumbnail is in view)
|
||||||
|
await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport();
|
||||||
|
|
||||||
|
await expect(page.getByLabel('Image Wrapper')).toBeVisible();
|
||||||
|
});
|
||||||
|
test.fixme('Can provide a custom image with spaces in name', async ({ page }) => {
|
||||||
|
test.info().annotations.push({
|
||||||
|
type: 'issue',
|
||||||
|
description: 'https://github.com/nasa/openmct/issues/7903'
|
||||||
|
});
|
||||||
|
await page.goto(exampleImagery.url, { waitUntil: 'domcontentloaded' });
|
||||||
|
|
||||||
|
// Modify Example Imagery to create a really stable image which will never let us down
|
||||||
|
await page.getByRole('button', { name: 'More actions' }).click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Edit Properties...' }).click();
|
||||||
|
await page
|
||||||
|
.locator('#imageLocation-textarea')
|
||||||
|
.fill(
|
||||||
|
'https://raw.githubusercontent.com/nasa/openmct/d8c64f183400afb70137221fc1a035e091bea912/e2e/test-data/rick%20space%20roll.jpg'
|
||||||
|
);
|
||||||
|
await page.getByRole('button', { name: 'Save' }).click();
|
||||||
|
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||||
|
|
||||||
|
// Wait for the thumbnails to finish their scroll animation
|
||||||
|
// (Wait until the rightmost thumbnail is in view)
|
||||||
|
await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport();
|
||||||
|
|
||||||
|
await expect(page.getByLabel('Image Wrapper')).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
@ -117,7 +117,8 @@ test.describe('Telemetry Table', () => {
|
|||||||
|
|
||||||
endTimeStamp.setUTCMinutes(endTimeStamp.getUTCMinutes() - 5);
|
endTimeStamp.setUTCMinutes(endTimeStamp.getUTCMinutes() - 5);
|
||||||
const endDate = endTimeStamp.toISOString().split('T')[0];
|
const endDate = endTimeStamp.toISOString().split('T')[0];
|
||||||
const endTime = endTimeStamp.toISOString().split('T')[1];
|
const milliseconds = endTimeStamp.getMilliseconds();
|
||||||
|
const endTime = endTimeStamp.toISOString().split('T')[1].replace(`.${milliseconds}Z`, '');
|
||||||
|
|
||||||
await setTimeConductorBounds(page, { endDate, endTime });
|
await setTimeConductorBounds(page, { endDate, endTime });
|
||||||
|
|
||||||
|
@ -24,65 +24,210 @@ import {
|
|||||||
setEndOffset,
|
setEndOffset,
|
||||||
setFixedTimeMode,
|
setFixedTimeMode,
|
||||||
setRealTimeMode,
|
setRealTimeMode,
|
||||||
setStartOffset,
|
setStartOffset
|
||||||
setTimeConductorBounds
|
|
||||||
} from '../../../../appActions.js';
|
} from '../../../../appActions.js';
|
||||||
import { expect, test } from '../../../../pluginFixtures.js';
|
import { expect, test } from '../../../../pluginFixtures.js';
|
||||||
|
|
||||||
test.describe('Time conductor operations', () => {
|
test.describe('Time conductor operations', () => {
|
||||||
test('validate start time does not exceed end time', async ({ page }) => {
|
const DAY = '2024-01-01';
|
||||||
|
const DAY_AFTER = '2024-01-02';
|
||||||
|
const ONE_O_CLOCK = '01:00:00';
|
||||||
|
const TWO_O_CLOCK = '02:00:00';
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
// Go to baseURL
|
// Go to baseURL
|
||||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||||
const year = new Date().getFullYear();
|
});
|
||||||
|
|
||||||
// Set initial valid time bounds
|
test('validate date and time inputs are validated on input event', async ({ page }) => {
|
||||||
const startDate = `${year}-01-01`;
|
const submitButtonLocator = page.getByLabel('Submit time bounds');
|
||||||
const startTime = '01:00:00';
|
|
||||||
const endDate = `${year}-01-01`;
|
|
||||||
const endTime = '02:00:00';
|
|
||||||
await setTimeConductorBounds(page, { startDate, startTime, endDate, endTime });
|
|
||||||
|
|
||||||
// Open the time conductor popup
|
// Open the time conductor popup
|
||||||
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
|
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
|
||||||
|
|
||||||
// Test invalid start date
|
await test.step('invalid start date disables submit button', async () => {
|
||||||
const invalidStartDate = `${year}-01-02`;
|
const initialStartDate = await page.getByLabel('Start date').inputValue();
|
||||||
|
const invalidStartDate = `${initialStartDate.substring(0, 5)}${initialStartDate.substring(6)}`;
|
||||||
|
|
||||||
await page.getByLabel('Start date').fill(invalidStartDate);
|
await page.getByLabel('Start date').fill(invalidStartDate);
|
||||||
await expect(page.getByLabel('Submit time bounds')).toBeDisabled();
|
await expect(submitButtonLocator).toBeDisabled();
|
||||||
await page.getByLabel('Start date').fill(startDate);
|
await page.getByLabel('Start date').fill(initialStartDate);
|
||||||
await expect(page.getByLabel('Submit time bounds')).toBeEnabled();
|
await expect(submitButtonLocator).toBeEnabled();
|
||||||
|
});
|
||||||
|
|
||||||
// Test invalid end date
|
await test.step('invalid start time disables submit button', async () => {
|
||||||
const invalidEndDate = `${year - 1}-12-31`;
|
const initialStartTime = await page.getByLabel('Start time').inputValue();
|
||||||
await page.getByLabel('End date').fill(invalidEndDate);
|
const invalidStartTime = `${initialStartTime.substring(0, 5)}${initialStartTime.substring(6)}`;
|
||||||
await expect(page.getByLabel('Submit time bounds')).toBeDisabled();
|
|
||||||
await page.getByLabel('End date').fill(endDate);
|
|
||||||
await expect(page.getByLabel('Submit time bounds')).toBeEnabled();
|
|
||||||
|
|
||||||
// Test invalid start time
|
|
||||||
const invalidStartTime = '42:00:00';
|
|
||||||
await page.getByLabel('Start time').fill(invalidStartTime);
|
await page.getByLabel('Start time').fill(invalidStartTime);
|
||||||
await expect(page.getByLabel('Submit time bounds')).toBeDisabled();
|
await expect(submitButtonLocator).toBeDisabled();
|
||||||
await page.getByLabel('Start time').fill(startTime);
|
await page.getByLabel('Start time').fill(initialStartTime);
|
||||||
await expect(page.getByLabel('Submit time bounds')).toBeEnabled();
|
await expect(submitButtonLocator).toBeEnabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('disable/enable submit button also works with multiple invalid inputs', async () => {
|
||||||
|
const initialEndDate = await page.getByLabel('End date').inputValue();
|
||||||
|
const invalidEndDate = `${initialEndDate.substring(0, 5)}${initialEndDate.substring(6)}`;
|
||||||
|
const initialStartTime = await page.getByLabel('Start time').inputValue();
|
||||||
|
const invalidStartTime = `${initialStartTime.substring(0, 5)}${initialStartTime.substring(6)}`;
|
||||||
|
|
||||||
|
await page.getByLabel('Start time').fill(invalidStartTime);
|
||||||
|
await expect(submitButtonLocator).toBeDisabled();
|
||||||
|
await page.getByLabel('End date').fill(invalidEndDate);
|
||||||
|
await expect(submitButtonLocator).toBeDisabled();
|
||||||
|
await page.getByLabel('End date').fill(initialEndDate);
|
||||||
|
await expect(submitButtonLocator).toBeDisabled();
|
||||||
|
await page.getByLabel('Start time').fill(initialStartTime);
|
||||||
|
await expect(submitButtonLocator).toBeEnabled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('validate date and time inputs validation is reported on change event', async ({ page }) => {
|
||||||
|
// Open the time conductor popup
|
||||||
|
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
|
||||||
|
|
||||||
|
await test.step('invalid start date is reported on change event, not on input event', async () => {
|
||||||
|
const initialStartDate = await page.getByLabel('Start date').inputValue();
|
||||||
|
const invalidStartDate = `${initialStartDate.substring(0, 5)}${initialStartDate.substring(6)}`;
|
||||||
|
|
||||||
|
await page.getByLabel('Start date').fill(invalidStartDate);
|
||||||
|
await expect(page.getByLabel('Start date')).not.toHaveAttribute('title', 'Invalid Date');
|
||||||
|
await page.getByLabel('Start date').press('Tab');
|
||||||
|
await expect(page.getByLabel('Start date')).toHaveAttribute('title', 'Invalid Date');
|
||||||
|
await page.getByLabel('Start date').fill(initialStartDate);
|
||||||
|
await expect(page.getByLabel('Start date')).not.toHaveAttribute('title', 'Invalid Date');
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('invalid start time is reported on change event, not on input event', async () => {
|
||||||
|
const initialStartTime = await page.getByLabel('Start time').inputValue();
|
||||||
|
const invalidStartTime = `${initialStartTime.substring(0, 5)}${initialStartTime.substring(6)}`;
|
||||||
|
|
||||||
|
await page.getByLabel('Start time').fill(invalidStartTime);
|
||||||
|
await expect(page.getByLabel('Start time')).not.toHaveAttribute('title', 'Invalid Time');
|
||||||
|
await page.getByLabel('Start time').press('Tab');
|
||||||
|
await expect(page.getByLabel('Start time')).toHaveAttribute('title', 'Invalid Time');
|
||||||
|
await page.getByLabel('Start time').fill(initialStartTime);
|
||||||
|
await expect(page.getByLabel('Start time')).not.toHaveAttribute('title', 'Invalid Time');
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('invalid end date is reported on change event, not on input event', async () => {
|
||||||
|
const initialEndDate = await page.getByLabel('End date').inputValue();
|
||||||
|
const invalidEndDate = `${initialEndDate.substring(0, 5)}${initialEndDate.substring(6)}`;
|
||||||
|
|
||||||
|
await page.getByLabel('End date').fill(invalidEndDate);
|
||||||
|
await expect(page.getByLabel('End date')).not.toHaveAttribute('title', 'Invalid Date');
|
||||||
|
await page.getByLabel('End date').press('Tab');
|
||||||
|
await expect(page.getByLabel('End date')).toHaveAttribute('title', 'Invalid Date');
|
||||||
|
await page.getByLabel('End date').fill(initialEndDate);
|
||||||
|
await expect(page.getByLabel('End date')).not.toHaveAttribute('title', 'Invalid Date');
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('invalid end time is reported on change event, not on input event', async () => {
|
||||||
|
const initialEndTime = await page.getByLabel('End time').inputValue();
|
||||||
|
const invalidEndTime = `${initialEndTime.substring(0, 5)}${initialEndTime.substring(6)}`;
|
||||||
|
|
||||||
// Test invalid end time
|
|
||||||
const invalidEndTime = '43:00:00';
|
|
||||||
await page.getByLabel('End time').fill(invalidEndTime);
|
await page.getByLabel('End time').fill(invalidEndTime);
|
||||||
await expect(page.getByLabel('Submit time bounds')).toBeDisabled();
|
await expect(page.getByLabel('End time')).not.toHaveAttribute('title', 'Invalid Time');
|
||||||
await page.getByLabel('End time').fill(endTime);
|
await page.getByLabel('End time').press('Tab');
|
||||||
await expect(page.getByLabel('Submit time bounds')).toBeEnabled();
|
await expect(page.getByLabel('End time')).toHaveAttribute('title', 'Invalid Time');
|
||||||
|
await page.getByLabel('End time').fill(initialEndTime);
|
||||||
|
await expect(page.getByLabel('End time')).not.toHaveAttribute('title', 'Invalid Time');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Submit valid time bounds
|
test('validate start time does not exceed end time on submit', async ({ page }) => {
|
||||||
|
// Open the time conductor popup
|
||||||
|
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
|
||||||
|
|
||||||
|
// FIXME: https://github.com/nasa/openmct/pull/7818
|
||||||
|
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
await page.getByLabel('Start date').fill(DAY);
|
||||||
|
await page.getByLabel('Start time').fill(TWO_O_CLOCK);
|
||||||
|
await page.getByLabel('End date').fill(DAY);
|
||||||
|
await page.getByLabel('End time').fill(ONE_O_CLOCK);
|
||||||
await page.getByLabel('Submit time bounds').click();
|
await page.getByLabel('Submit time bounds').click();
|
||||||
|
|
||||||
// Verify the submitted time bounds
|
await expect(page.getByLabel('Start date')).toHaveAttribute(
|
||||||
await expect(page.getByLabel('Start bounds')).toHaveText(
|
'title',
|
||||||
new RegExp(`${startDate} ${startTime}.000Z`)
|
'Specified start date exceeds end bound'
|
||||||
);
|
);
|
||||||
await expect(page.getByLabel('End bounds')).toHaveText(
|
await expect(page.getByLabel('Start bounds')).not.toHaveText(`${DAY} ${TWO_O_CLOCK}.000Z`);
|
||||||
new RegExp(`${endDate} ${endTime}.000Z`)
|
await expect(page.getByLabel('End bounds')).not.toHaveText(`${DAY} ${ONE_O_CLOCK}.000Z`);
|
||||||
|
|
||||||
|
await page.getByLabel('Start date').fill(DAY);
|
||||||
|
await page.getByLabel('Start time').fill(ONE_O_CLOCK);
|
||||||
|
await page.getByLabel('End date').fill(DAY);
|
||||||
|
await page.getByLabel('End time').fill(TWO_O_CLOCK);
|
||||||
|
await page.getByLabel('Submit time bounds').click();
|
||||||
|
|
||||||
|
await expect(page.getByLabel('Start bounds')).toHaveText(`${DAY} ${ONE_O_CLOCK}.000Z`);
|
||||||
|
await expect(page.getByLabel('End bounds')).toHaveText(`${DAY} ${TWO_O_CLOCK}.000Z`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('validate start datetime does not exceed end datetime on submit', async ({ page }) => {
|
||||||
|
// Open the time conductor popup
|
||||||
|
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
|
||||||
|
|
||||||
|
// FIXME: https://github.com/nasa/openmct/pull/7818
|
||||||
|
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
await page.getByLabel('Start date').fill(DAY_AFTER);
|
||||||
|
await page.getByLabel('Start time').fill(ONE_O_CLOCK);
|
||||||
|
await page.getByLabel('End date').fill(DAY);
|
||||||
|
await page.getByLabel('End time').fill(ONE_O_CLOCK);
|
||||||
|
await page.getByLabel('Submit time bounds').click();
|
||||||
|
|
||||||
|
await expect(page.getByLabel('Start date')).toHaveAttribute(
|
||||||
|
'title',
|
||||||
|
'Specified start date exceeds end bound'
|
||||||
);
|
);
|
||||||
|
await expect(page.getByLabel('Start bounds')).not.toHaveText(
|
||||||
|
`${DAY_AFTER} ${ONE_O_CLOCK}.000Z`
|
||||||
|
);
|
||||||
|
await expect(page.getByLabel('End bounds')).not.toHaveText(`${DAY} ${ONE_O_CLOCK}.000Z`);
|
||||||
|
|
||||||
|
await page.getByLabel('Start date').fill(DAY);
|
||||||
|
await page.getByLabel('Start time').fill(ONE_O_CLOCK);
|
||||||
|
await page.getByLabel('End date').fill(DAY_AFTER);
|
||||||
|
await page.getByLabel('End time').fill(ONE_O_CLOCK);
|
||||||
|
await page.getByLabel('Submit time bounds').click();
|
||||||
|
|
||||||
|
await expect(page.getByLabel('Start bounds')).toHaveText(`${DAY} ${ONE_O_CLOCK}.000Z`);
|
||||||
|
await expect(page.getByLabel('End bounds')).toHaveText(`${DAY_AFTER} ${ONE_O_CLOCK}.000Z`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cancelling form does not set bounds', async ({ page }) => {
|
||||||
|
test.info().annotations.push({
|
||||||
|
type: 'issue',
|
||||||
|
description: 'https://github.com/nasa/openmct/issues/7791'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Open the time conductor popup
|
||||||
|
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
|
||||||
|
|
||||||
|
await page.getByLabel('Start date').fill(DAY);
|
||||||
|
await page.getByLabel('Start time').fill(ONE_O_CLOCK);
|
||||||
|
await page.getByLabel('End date').fill(DAY_AFTER);
|
||||||
|
await page.getByLabel('End time').fill(ONE_O_CLOCK);
|
||||||
|
await page.getByLabel('Discard changes and close time popup').click();
|
||||||
|
|
||||||
|
await expect(page.getByLabel('Start bounds')).not.toHaveText(`${DAY} ${ONE_O_CLOCK}.000Z`);
|
||||||
|
await expect(page.getByLabel('End bounds')).not.toHaveText(`${DAY_AFTER} ${ONE_O_CLOCK}.000Z`);
|
||||||
|
|
||||||
|
// Open the time conductor popup
|
||||||
|
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
|
||||||
|
|
||||||
|
await page.getByLabel('Start date').fill(DAY);
|
||||||
|
await page.getByLabel('Start time').fill(ONE_O_CLOCK);
|
||||||
|
await page.getByLabel('End date').fill(DAY_AFTER);
|
||||||
|
await page.getByLabel('End time').fill(ONE_O_CLOCK);
|
||||||
|
await page.getByLabel('Submit time bounds').click();
|
||||||
|
|
||||||
|
await expect(page.getByLabel('Start bounds')).toHaveText(`${DAY} ${ONE_O_CLOCK}.000Z`);
|
||||||
|
await expect(page.getByLabel('End bounds')).toHaveText(`${DAY_AFTER} ${ONE_O_CLOCK}.000Z`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -131,77 +276,6 @@ test.describe('Global Time Conductor', () => {
|
|||||||
await expect(page.getByLabel('End offset: 01:30:31')).toBeVisible();
|
await expect(page.getByLabel('End offset: 01:30:31')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Input field validation: fixed time mode', async ({ page }) => {
|
|
||||||
test.info().annotations.push({
|
|
||||||
type: 'issue',
|
|
||||||
description: 'https://github.com/nasa/openmct/issues/7791'
|
|
||||||
});
|
|
||||||
// Switch to fixed time mode
|
|
||||||
await setFixedTimeMode(page);
|
|
||||||
|
|
||||||
// Define valid time bounds for testing
|
|
||||||
const validBounds = {
|
|
||||||
startDate: '2024-04-20',
|
|
||||||
startTime: '00:04:20',
|
|
||||||
endDate: '2024-04-20',
|
|
||||||
endTime: '16:04:20'
|
|
||||||
};
|
|
||||||
// Set valid time conductor bounds ✌️
|
|
||||||
await setTimeConductorBounds(page, validBounds);
|
|
||||||
|
|
||||||
// Verify that the time bounds are set correctly
|
|
||||||
await expect(page.getByLabel(`Start bounds: 2024-04-20 00:04:20.000Z`)).toBeVisible();
|
|
||||||
await expect(page.getByLabel(`End bounds: 2024-04-20 16:04:20.000Z`)).toBeVisible();
|
|
||||||
|
|
||||||
// Open the Time Conductor Mode popup
|
|
||||||
await page.getByLabel('Time Conductor Mode').click();
|
|
||||||
|
|
||||||
// Test invalid start date
|
|
||||||
const invalidStartDate = '2024-04-21';
|
|
||||||
await page.getByLabel('Start date').fill(invalidStartDate);
|
|
||||||
await expect(page.getByLabel('Submit time bounds')).toBeDisabled();
|
|
||||||
await page.getByLabel('Start date').fill(validBounds.startDate);
|
|
||||||
await expect(page.getByLabel('Submit time bounds')).toBeEnabled();
|
|
||||||
|
|
||||||
// Test invalid end date
|
|
||||||
const invalidEndDate = '2024-04-19';
|
|
||||||
await page.getByLabel('End date').fill(invalidEndDate);
|
|
||||||
await expect(page.getByLabel('Submit time bounds')).toBeDisabled();
|
|
||||||
await page.getByLabel('End date').fill(validBounds.endDate);
|
|
||||||
await expect(page.getByLabel('Submit time bounds')).toBeEnabled();
|
|
||||||
|
|
||||||
// Test invalid start time
|
|
||||||
const invalidStartTime = '16:04:21';
|
|
||||||
await page.getByLabel('Start time').fill(invalidStartTime);
|
|
||||||
await expect(page.getByLabel('Submit time bounds')).toBeDisabled();
|
|
||||||
await page.getByLabel('Start time').fill(validBounds.startTime);
|
|
||||||
await expect(page.getByLabel('Submit time bounds')).toBeEnabled();
|
|
||||||
|
|
||||||
// Test invalid end time
|
|
||||||
const invalidEndTime = '00:04:19';
|
|
||||||
await page.getByLabel('End time').fill(invalidEndTime);
|
|
||||||
await expect(page.getByLabel('Submit time bounds')).toBeDisabled();
|
|
||||||
await page.getByLabel('End time').fill(validBounds.endTime);
|
|
||||||
await expect(page.getByLabel('Submit time bounds')).toBeEnabled();
|
|
||||||
|
|
||||||
// Verify that the time bounds remain unchanged after invalid inputs
|
|
||||||
await expect(page.getByLabel(`Start bounds: 2024-04-20 00:04:20.000Z`)).toBeVisible();
|
|
||||||
await expect(page.getByLabel(`End bounds: 2024-04-20 16:04:20.000Z`)).toBeVisible();
|
|
||||||
|
|
||||||
// Discard changes and verify that bounds remain unchanged
|
|
||||||
await setTimeConductorBounds(page, {
|
|
||||||
startDate: validBounds.startDate,
|
|
||||||
startTime: '04:20:00',
|
|
||||||
endDate: validBounds.endDate,
|
|
||||||
endTime: '04:20:20',
|
|
||||||
submitChanges: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// Verify that the original time bounds are still displayed after discarding changes
|
|
||||||
await expect(page.getByLabel(`Start bounds: 2024-04-20 00:04:20.000Z`)).toBeVisible();
|
|
||||||
await expect(page.getByLabel(`End bounds: 2024-04-20 16:04:20.000Z`)).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verify that offsets and url params are preserved when switching
|
* Verify that offsets and url params are preserved when switching
|
||||||
* between fixed timespan and real-time mode.
|
* between fixed timespan and real-time mode.
|
||||||
|
@ -26,14 +26,25 @@ import fs from 'fs';
|
|||||||
import { createDomainObjectWithDefaults, createPlanFromJSON } from '../../appActions.js';
|
import { createDomainObjectWithDefaults, createPlanFromJSON } from '../../appActions.js';
|
||||||
import { scanForA11yViolations, test } from '../../avpFixtures.js';
|
import { scanForA11yViolations, test } from '../../avpFixtures.js';
|
||||||
import { VISUAL_FIXED_URL } from '../../constants.js';
|
import { VISUAL_FIXED_URL } from '../../constants.js';
|
||||||
import { setBoundsToSpanAllActivities, setDraftStatusForPlan } from '../../helper/planningUtils.js';
|
import {
|
||||||
|
getFirstActivity,
|
||||||
|
setBoundsToSpanAllActivities,
|
||||||
|
setDraftStatusForPlan
|
||||||
|
} from '../../helper/planningUtils.js';
|
||||||
|
|
||||||
const examplePlanSmall2 = JSON.parse(
|
const examplePlanSmall2 = JSON.parse(
|
||||||
fs.readFileSync(new URL('../../test-data/examplePlans/ExamplePlan_Small2.json', import.meta.url))
|
fs.readFileSync(new URL('../../test-data/examplePlans/ExamplePlan_Small2.json', import.meta.url))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const FIRST_ACTIVITY_SMALL_2 = getFirstActivity(examplePlanSmall2);
|
||||||
|
|
||||||
test.describe('Visual - Gantt Chart @a11y', () => {
|
test.describe('Visual - Gantt Chart @a11y', () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
|
// Set the clock to the end of the first activity in the plan
|
||||||
|
// This is so we can see the "now" line in the plan view
|
||||||
|
await page.clock.install({ time: FIRST_ACTIVITY_SMALL_2.end + 10000 });
|
||||||
|
await page.clock.resume();
|
||||||
|
|
||||||
await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });
|
await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });
|
||||||
});
|
});
|
||||||
test('Gantt Chart View', async ({ page, theme }) => {
|
test('Gantt Chart View', async ({ page, theme }) => {
|
||||||
|
@ -27,14 +27,21 @@ import { createDomainObjectWithDefaults, createPlanFromJSON } from '../../appAct
|
|||||||
import { scanForA11yViolations, test } from '../../avpFixtures.js';
|
import { scanForA11yViolations, test } from '../../avpFixtures.js';
|
||||||
import { waitForAnimations } from '../../baseFixtures.js';
|
import { waitForAnimations } from '../../baseFixtures.js';
|
||||||
import { VISUAL_FIXED_URL } from '../../constants.js';
|
import { VISUAL_FIXED_URL } from '../../constants.js';
|
||||||
import { setBoundsToSpanAllActivities } from '../../helper/planningUtils.js';
|
import { getFirstActivity, setBoundsToSpanAllActivities } from '../../helper/planningUtils.js';
|
||||||
|
|
||||||
const examplePlanSmall2 = JSON.parse(
|
const examplePlanSmall2 = JSON.parse(
|
||||||
fs.readFileSync(new URL('../../test-data/examplePlans/ExamplePlan_Small2.json', import.meta.url))
|
fs.readFileSync(new URL('../../test-data/examplePlans/ExamplePlan_Small2.json', import.meta.url))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const FIRST_ACTIVITY_SMALL_2 = getFirstActivity(examplePlanSmall2);
|
||||||
|
|
||||||
test.describe('Visual - Time Strip @a11y', () => {
|
test.describe('Visual - Time Strip @a11y', () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
|
// Set the clock to the end of the first activity in the plan
|
||||||
|
// This is so we can see the "now" line in the plan view
|
||||||
|
await page.clock.install({ time: FIRST_ACTIVITY_SMALL_2.end + 10000 });
|
||||||
|
await page.clock.resume();
|
||||||
|
|
||||||
await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });
|
await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });
|
||||||
});
|
});
|
||||||
test('Time Strip View', async ({ page, theme }) => {
|
test('Time Strip View', async ({ page, theme }) => {
|
||||||
|
@ -42,6 +42,7 @@ const examplePlanSmall2 = JSON.parse(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const FIRST_ACTIVITY_SMALL_1 = getFirstActivity(examplePlanSmall1);
|
const FIRST_ACTIVITY_SMALL_1 = getFirstActivity(examplePlanSmall1);
|
||||||
|
const FIRST_ACTIVITY_SMALL_2 = getFirstActivity(examplePlanSmall2);
|
||||||
|
|
||||||
test.describe('Visual - Timelist progress bar @clock @a11y', () => {
|
test.describe('Visual - Timelist progress bar @clock @a11y', () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
@ -59,6 +60,11 @@ test.describe('Visual - Timelist progress bar @clock @a11y', () => {
|
|||||||
|
|
||||||
test.describe('Visual - Plan View @a11y', () => {
|
test.describe('Visual - Plan View @a11y', () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
|
// Set the clock to the end of the first activity in the plan
|
||||||
|
// This is so we can see the "now" line in the plan view
|
||||||
|
await page.clock.install({ time: FIRST_ACTIVITY_SMALL_2.end + 10000 });
|
||||||
|
await page.clock.resume();
|
||||||
|
|
||||||
await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });
|
await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
26
package-lock.json
generated
26
package-lock.json
generated
@ -94,7 +94,7 @@
|
|||||||
"webpack-merge": "5.10.0"
|
"webpack-merge": "5.10.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.14.2 <22"
|
"node": ">=18.14.2 <23"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"e2e": {
|
"e2e": {
|
||||||
@ -105,7 +105,7 @@
|
|||||||
"@axe-core/playwright": "4.8.5",
|
"@axe-core/playwright": "4.8.5",
|
||||||
"@percy/cli": "1.27.4",
|
"@percy/cli": "1.27.4",
|
||||||
"@percy/playwright": "1.0.4",
|
"@percy/playwright": "1.0.4",
|
||||||
"@playwright/test": "1.47.2"
|
"@playwright/test": "1.48.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"e2e/node_modules/@percy/cli": {
|
"e2e/node_modules/@percy/cli": {
|
||||||
@ -1561,13 +1561,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@playwright/test": {
|
"node_modules/@playwright/test": {
|
||||||
"version": "1.47.2",
|
"version": "1.48.1",
|
||||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.47.2.tgz",
|
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.1.tgz",
|
||||||
"integrity": "sha512-jTXRsoSPONAs8Za9QEQdyjFn+0ZQFjCiIztAIF6bi1HqhBzG9Ma7g1WotyiGqFSBRZjIEqMdT8RUlbk1QVhzCQ==",
|
"integrity": "sha512-s9RtWoxkOLmRJdw3oFvhFbs9OJS0BzrLUc8Hf6l2UdCNd1rqeEyD4BhCJkvzeEoD1FsK4mirsWwGerhVmYKtZg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright": "1.47.2"
|
"playwright": "1.48.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
@ -8830,13 +8830,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/playwright": {
|
"node_modules/playwright": {
|
||||||
"version": "1.47.2",
|
"version": "1.48.1",
|
||||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.47.2.tgz",
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.1.tgz",
|
||||||
"integrity": "sha512-nx1cLMmQWqmA3UsnjaaokyoUpdVaaDhJhMoxX2qj3McpjnsqFHs516QAKYhqHAgOP+oCFTEOCOAaD1RgD/RQfA==",
|
"integrity": "sha512-j8CiHW/V6HxmbntOfyB4+T/uk08tBy6ph0MpBXwuoofkSnLmlfdYNNkFTYD6ofzzlSqLA1fwH4vwvVFvJgLN0w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.47.2"
|
"playwright-core": "1.48.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
@ -8849,9 +8849,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/playwright-core": {
|
"node_modules/playwright-core": {
|
||||||
"version": "1.47.2",
|
"version": "1.48.1",
|
||||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.47.2.tgz",
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.1.tgz",
|
||||||
"integrity": "sha512-3JvMfF+9LJfe16l7AbSmU555PaTl2tPyQsVInqm3id16pdDfvZ8TTZ/pyzmkbDrZTQefyzU7AIHlZqQnxpqHVQ==",
|
"integrity": "sha512-Yw/t4VAFX/bBr1OzwCuOMZkY1Cnb4z/doAFSwf4huqAGWmf9eMNjmK7NiOljCdLmxeRYcGPPmcDgU0zOlzP0YA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
@ -140,7 +140,7 @@
|
|||||||
"url": "git+https://github.com/nasa/openmct.git"
|
"url": "git+https://github.com/nasa/openmct.git"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.14.2 <22"
|
"node": ">=18.14.2 <23"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"Firefox ESR",
|
"Firefox ESR",
|
||||||
|
@ -35,7 +35,7 @@ export const DEFAULT_SHELVE_DURATIONS = [
|
|||||||
value: 900000
|
value: 900000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Indefinite',
|
name: 'Unlimited',
|
||||||
value: null
|
value: null
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@ -136,17 +136,21 @@ export default class FaultManagementAPI {
|
|||||||
/**
|
/**
|
||||||
* Retrieves the available shelve durations from the provider, or the default durations if the
|
* Retrieves the available shelve durations from the provider, or the default durations if the
|
||||||
* provider does not provide any.
|
* provider does not provide any.
|
||||||
* @returns {ShelveDuration[]}
|
* @returns {ShelveDuration[] | undefined}
|
||||||
*/
|
*/
|
||||||
getShelveDurations() {
|
getShelveDurations() {
|
||||||
return this.provider?.getShelveDurations() ?? DEFAULT_SHELVE_DURATIONS;
|
if (!this.provider) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.provider.getShelveDurations?.() ?? DEFAULT_SHELVE_DURATIONS;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} ShelveDuration
|
* @typedef {Object} ShelveDuration
|
||||||
* @property {string} name - The name of the shelve duration
|
* @property {string} name - The name of the shelve duration
|
||||||
* @property {number|null} value - The value of the shelve duration in milliseconds, or null for indefinite
|
* @property {number|null} value - The value of the shelve duration in milliseconds, or null for unlimited
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -332,7 +332,11 @@ export default {
|
|||||||
this.domainObject.configuration.axes.xKey === undefined ||
|
this.domainObject.configuration.axes.xKey === undefined ||
|
||||||
this.domainObject.configuration.axes.yKey === undefined
|
this.domainObject.configuration.axes.yKey === undefined
|
||||||
) {
|
) {
|
||||||
return;
|
const { xKey, yKey } = this.identifyAxesKeys(axisMetadata);
|
||||||
|
this.openmct.objects.mutate(this.domainObject, 'configuration.axes', {
|
||||||
|
xKey,
|
||||||
|
yKey
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let xValues = [];
|
let xValues = [];
|
||||||
@ -431,6 +435,30 @@ export default {
|
|||||||
subscribeToAll() {
|
subscribeToAll() {
|
||||||
const telemetryObjects = Object.values(this.telemetryObjects);
|
const telemetryObjects = Object.values(this.telemetryObjects);
|
||||||
telemetryObjects.forEach(this.subscribeToObject);
|
telemetryObjects.forEach(this.subscribeToObject);
|
||||||
|
},
|
||||||
|
identifyAxesKeys(metadata) {
|
||||||
|
const { xAxisMetadata, yAxisMetadata } = metadata;
|
||||||
|
|
||||||
|
let xKey;
|
||||||
|
let yKey;
|
||||||
|
|
||||||
|
// If xAxisMetadata contains array values, use the first one for xKey
|
||||||
|
const arrayValues = xAxisMetadata.filter((metaDatum) => metaDatum.isArrayValue);
|
||||||
|
const nonArrayValues = xAxisMetadata.filter((metaDatum) => !metaDatum.isArrayValue);
|
||||||
|
|
||||||
|
if (arrayValues.length > 0) {
|
||||||
|
xKey = arrayValues[0].key;
|
||||||
|
yKey = arrayValues.length > 1 ? arrayValues[1].key : yAxisMetadata.key;
|
||||||
|
} else if (nonArrayValues.length > 0) {
|
||||||
|
xKey = nonArrayValues[0].key;
|
||||||
|
yKey = 'none';
|
||||||
|
} else {
|
||||||
|
// Fallback if no valid xKey or yKey is found
|
||||||
|
xKey = 'none';
|
||||||
|
yKey = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
return { xKey, yKey };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -28,11 +28,7 @@
|
|||||||
{ 'is-style-invisible': styleItem.style && styleItem.style.isStyleInvisible },
|
{ 'is-style-invisible': styleItem.style && styleItem.style.isStyleInvisible },
|
||||||
{ 'c-style-thumb--mixed': mixedStyles.indexOf('backgroundColor') > -1 }
|
{ 'c-style-thumb--mixed': mixedStyles.indexOf('backgroundColor') > -1 }
|
||||||
]"
|
]"
|
||||||
:style="[
|
:style="[encodedImageUrl ? { backgroundImage: 'url(' + encodedImageUrl + ')' } : itemStyle]"
|
||||||
styleItem.style.imageUrl
|
|
||||||
? { backgroundImage: 'url(' + styleItem.style.imageUrl + ')' }
|
|
||||||
: itemStyle
|
|
||||||
]"
|
|
||||||
class="c-style-thumb"
|
class="c-style-thumb"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@ -62,7 +58,7 @@
|
|||||||
@change="updateStyleValue"
|
@change="updateStyleValue"
|
||||||
/>
|
/>
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
v-if="hasProperty(styleItem.style.imageUrl)"
|
v-if="hasProperty(encodedImageUrl)"
|
||||||
class="c-style__toolbar-button--image-url"
|
class="c-style__toolbar-button--image-url"
|
||||||
:options="imageUrlOption"
|
:options="imageUrlOption"
|
||||||
@change="updateStyleValue"
|
@change="updateStyleValue"
|
||||||
@ -93,6 +89,8 @@ import ToolbarButton from '@/ui/toolbar/components/ToolbarButton.vue';
|
|||||||
import ToolbarColorPicker from '@/ui/toolbar/components/ToolbarColorPicker.vue';
|
import ToolbarColorPicker from '@/ui/toolbar/components/ToolbarColorPicker.vue';
|
||||||
import ToolbarToggleButton from '@/ui/toolbar/components/ToolbarToggleButton.vue';
|
import ToolbarToggleButton from '@/ui/toolbar/components/ToolbarToggleButton.vue';
|
||||||
|
|
||||||
|
import { encode_url } from '../../../../utils/encoding';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'StyleEditor',
|
name: 'StyleEditor',
|
||||||
components: {
|
components: {
|
||||||
@ -183,11 +181,14 @@ export default {
|
|||||||
},
|
},
|
||||||
property: 'imageUrl',
|
property: 'imageUrl',
|
||||||
formKeys: ['url'],
|
formKeys: ['url'],
|
||||||
value: { url: this.styleItem.style.imageUrl },
|
value: { url: this.encodedImageUrl },
|
||||||
isEditing: this.isEditing,
|
isEditing: this.isEditing,
|
||||||
nonSpecific: this.mixedStyles.indexOf('imageUrl') > -1
|
nonSpecific: this.mixedStyles.indexOf('imageUrl') > -1
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
encodedImageUrl() {
|
||||||
|
return encode_url(this.styleItem.style.imageUrl);
|
||||||
|
},
|
||||||
isStyleInvisibleOption() {
|
isStyleInvisibleOption() {
|
||||||
return {
|
return {
|
||||||
value: this.styleItem.style.isStyleInvisible,
|
value: this.styleItem.style.isStyleInvisible,
|
||||||
|
@ -35,6 +35,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { encode_url } from '../../../utils/encoding';
|
||||||
import conditionalStylesMixin from '../mixins/objectStyles-mixin.js';
|
import conditionalStylesMixin from '../mixins/objectStyles-mixin.js';
|
||||||
import LayoutFrame from './LayoutFrame.vue';
|
import LayoutFrame from './LayoutFrame.vue';
|
||||||
|
|
||||||
@ -80,12 +81,12 @@ export default {
|
|||||||
return this.isEditing || !this.itemStyle?.isStyleInvisible;
|
return this.isEditing || !this.itemStyle?.isStyleInvisible;
|
||||||
},
|
},
|
||||||
style() {
|
style() {
|
||||||
let backgroundImage = 'url(' + this.item.url + ')';
|
let backgroundImage = `url('${encode_url(this.item.url)}')`;
|
||||||
let border = '1px solid ' + this.item.stroke;
|
let border = '1px solid ' + this.item.stroke;
|
||||||
|
|
||||||
if (this.itemStyle) {
|
if (this.itemStyle) {
|
||||||
if (this.itemStyle.imageUrl !== undefined) {
|
if (this.itemStyle.imageUrl !== undefined) {
|
||||||
backgroundImage = 'url(' + this.itemStyle.imageUrl + ')';
|
backgroundImage = `url('${encode_url(this.itemStyle.imageUrl)}')`;
|
||||||
}
|
}
|
||||||
|
|
||||||
border = this.itemStyle.border;
|
border = this.itemStyle.border;
|
||||||
|
@ -330,7 +330,8 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
shelveData.comment = data.comment || '';
|
shelveData.comment = data.comment || '';
|
||||||
shelveData.shelveDuration = data.shelveDuration ?? this.shelveDurations[0].value;
|
shelveData.shelveDuration =
|
||||||
|
data.shelveDuration === undefined ? this.shelveDurations[0].value : data.shelveDuration;
|
||||||
} else {
|
} else {
|
||||||
shelveData = {
|
shelveData = {
|
||||||
shelved: false
|
shelved: false
|
||||||
|
@ -42,24 +42,6 @@ export const FAULT_MANAGEMENT_TYPE = 'faultManagement';
|
|||||||
export const FAULT_MANAGEMENT_INSPECTOR = 'faultManagementInspector';
|
export const FAULT_MANAGEMENT_INSPECTOR = 'faultManagementInspector';
|
||||||
export const FAULT_MANAGEMENT_ALARMS = 'alarms';
|
export const FAULT_MANAGEMENT_ALARMS = 'alarms';
|
||||||
export const FAULT_MANAGEMENT_GLOBAL_ALARMS = 'global-alarm-status';
|
export const FAULT_MANAGEMENT_GLOBAL_ALARMS = 'global-alarm-status';
|
||||||
export const FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS = [
|
|
||||||
{
|
|
||||||
name: '5 Minutes',
|
|
||||||
value: 300000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '10 Minutes',
|
|
||||||
value: 600000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '15 Minutes',
|
|
||||||
value: 900000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Indefinite',
|
|
||||||
value: 0
|
|
||||||
}
|
|
||||||
];
|
|
||||||
export const FAULT_MANAGEMENT_VIEW = 'faultManagement.view';
|
export const FAULT_MANAGEMENT_VIEW = 'faultManagement.view';
|
||||||
export const FAULT_MANAGEMENT_NAMESPACE = 'faults.taxonomy';
|
export const FAULT_MANAGEMENT_NAMESPACE = 'faults.taxonomy';
|
||||||
export const FILTER_ITEMS = ['Standard View', 'Acknowledged', 'Unacknowledged', 'Shelved'];
|
export const FILTER_ITEMS = ['Standard View', 'Acknowledged', 'Unacknowledged', 'Shelved'];
|
||||||
|
@ -38,7 +38,7 @@
|
|||||||
<img
|
<img
|
||||||
ref="img"
|
ref="img"
|
||||||
class="c-thumb__image"
|
class="c-thumb__image"
|
||||||
:src="`${image.thumbnailUrl || image.url}`"
|
:src="imageSrc"
|
||||||
fetchpriority="low"
|
fetchpriority="low"
|
||||||
@load="imageLoadCompleted"
|
@load="imageLoadCompleted"
|
||||||
/>
|
/>
|
||||||
@ -54,6 +54,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { encode_url } from '../../../utils/encoding';
|
||||||
|
|
||||||
const THUMB_PADDING = 4;
|
const THUMB_PADDING = 4;
|
||||||
const BORDER_WIDTH = 2;
|
const BORDER_WIDTH = 2;
|
||||||
|
|
||||||
@ -96,6 +98,9 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
imageSrc() {
|
||||||
|
return `${encode_url(this.image.thumbnailUrl) || encode_url(this.image.url)}`;
|
||||||
|
},
|
||||||
ariaLabel() {
|
ariaLabel() {
|
||||||
return `Image thumbnail from ${this.image.formattedTime}${this.showAnnotationIndicator ? ', has annotations' : ''}`;
|
return `Image thumbnail from ${this.image.formattedTime}${this.showAnnotationIndicator ? ', has annotations' : ''}`;
|
||||||
},
|
},
|
||||||
|
@ -222,6 +222,7 @@ import { TIME_CONTEXT_EVENTS } from '@/api/time/constants.js';
|
|||||||
import imageryData from '@/plugins/imagery/mixins/imageryData.js';
|
import imageryData from '@/plugins/imagery/mixins/imageryData.js';
|
||||||
import { VIEW_LARGE_ACTION_KEY } from '@/plugins/viewLargeAction/viewLargeAction.js';
|
import { VIEW_LARGE_ACTION_KEY } from '@/plugins/viewLargeAction/viewLargeAction.js';
|
||||||
|
|
||||||
|
import { encode_url } from '../../../utils/encoding';
|
||||||
import eventHelpers from '../lib/eventHelpers.js';
|
import eventHelpers from '../lib/eventHelpers.js';
|
||||||
import AnnotationsCanvas from './AnnotationsCanvas.vue';
|
import AnnotationsCanvas from './AnnotationsCanvas.vue';
|
||||||
import Compass from './Compass/CompassComponent.vue';
|
import Compass from './Compass/CompassComponent.vue';
|
||||||
@ -364,7 +365,7 @@ export default {
|
|||||||
filter: `brightness(${this.filters.brightness}%) contrast(${this.filters.contrast}%)`,
|
filter: `brightness(${this.filters.brightness}%) contrast(${this.filters.contrast}%)`,
|
||||||
backgroundImage: `${
|
backgroundImage: `${
|
||||||
this.imageUrl
|
this.imageUrl
|
||||||
? `url(${this.imageUrl}),
|
? `url(${encode_url(this.imageUrl)}),
|
||||||
repeating-linear-gradient(
|
repeating-linear-gradient(
|
||||||
45deg,
|
45deg,
|
||||||
transparent,
|
transparent,
|
||||||
@ -789,7 +790,7 @@ export default {
|
|||||||
},
|
},
|
||||||
getVisibleLayerStyles(layer) {
|
getVisibleLayerStyles(layer) {
|
||||||
return {
|
return {
|
||||||
backgroundImage: `url(${layer.source})`,
|
backgroundImage: `url(${encode_url(layer.source)})`,
|
||||||
transform: `scale(${this.zoomFactor}) translate(${this.imageTranslateX / 2}px, ${
|
transform: `scale(${this.zoomFactor}) translate(${this.imageTranslateX / 2}px, ${
|
||||||
this.imageTranslateY / 2
|
this.imageTranslateY / 2
|
||||||
}px)`,
|
}px)`,
|
||||||
|
@ -243,11 +243,12 @@ export default {
|
|||||||
if (this.planObject) {
|
if (this.planObject) {
|
||||||
this.showReplacePlanDialog(domainObject);
|
this.showReplacePlanDialog(domainObject);
|
||||||
} else {
|
} else {
|
||||||
this.swimlaneVisibility = this.configuration.swimlaneVisibility;
|
|
||||||
this.setupPlan(domainObject);
|
this.setupPlan(domainObject);
|
||||||
|
this.swimlaneVisibility = this.configuration.swimlaneVisibility;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handleConfigurationChange(newConfiguration) {
|
handleConfigurationChange(newConfiguration) {
|
||||||
|
this.configuration = this.planViewConfiguration.getConfiguration();
|
||||||
Object.keys(newConfiguration).forEach((key) => {
|
Object.keys(newConfiguration).forEach((key) => {
|
||||||
this[key] = newConfiguration[key];
|
this[key] = newConfiguration[key];
|
||||||
});
|
});
|
||||||
@ -423,7 +424,10 @@ export default {
|
|||||||
return currentRow || SWIMLANE_PADDING;
|
return currentRow || SWIMLANE_PADDING;
|
||||||
},
|
},
|
||||||
generateActivities() {
|
generateActivities() {
|
||||||
const groupNames = getValidatedGroups(this.domainObject, this.planData);
|
if (!this.planObject) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const groupNames = getValidatedGroups(this.planObject, this.planData);
|
||||||
|
|
||||||
if (!groupNames.length) {
|
if (!groupNames.length) {
|
||||||
return;
|
return;
|
||||||
|
@ -11,18 +11,19 @@
|
|||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
ref="startDate"
|
ref="startDate"
|
||||||
v-model="formattedBounds.start"
|
v-model="formattedBounds.startDate"
|
||||||
class="c-input--datetime"
|
class="c-input--datetime"
|
||||||
type="text"
|
type="text"
|
||||||
autocorrect="off"
|
autocorrect="off"
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
aria-label="Start date"
|
aria-label="Start date"
|
||||||
@input="validateAllBounds('startDate')"
|
@input="validateInput('startDate')"
|
||||||
|
@change="reportValidity('startDate')"
|
||||||
/>
|
/>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
v-if="isUTCBased"
|
v-if="isUTCBased"
|
||||||
class="c-ctrl-wrapper--menus-right"
|
class="c-ctrl-wrapper--menus-right"
|
||||||
:default-date-time="formattedBounds.start"
|
:default-date-time="formattedBounds.startDate"
|
||||||
:formatter="timeFormatter"
|
:formatter="timeFormatter"
|
||||||
@date-selected="startDateSelected"
|
@date-selected="startDateSelected"
|
||||||
/>
|
/>
|
||||||
@ -37,7 +38,8 @@
|
|||||||
autocorrect="off"
|
autocorrect="off"
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
aria-label="Start time"
|
aria-label="Start time"
|
||||||
@input="validateAllBounds('startDate')"
|
@input="validateInput('startTime')"
|
||||||
|
@change="reportValidity('startTime')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -48,18 +50,19 @@
|
|||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
ref="endDate"
|
ref="endDate"
|
||||||
v-model="formattedBounds.end"
|
v-model="formattedBounds.endDate"
|
||||||
class="c-input--datetime"
|
class="c-input--datetime"
|
||||||
type="text"
|
type="text"
|
||||||
autocorrect="off"
|
autocorrect="off"
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
aria-label="End date"
|
aria-label="End date"
|
||||||
@input="validateAllBounds('endDate')"
|
@input="validateInput('endDate')"
|
||||||
|
@change="reportValidity('endDate')"
|
||||||
/>
|
/>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
v-if="isUTCBased"
|
v-if="isUTCBased"
|
||||||
class="c-ctrl-wrapper--menus-left"
|
class="c-ctrl-wrapper--menus-left"
|
||||||
:default-date-time="formattedBounds.end"
|
:default-date-time="formattedBounds.endDate"
|
||||||
:formatter="timeFormatter"
|
:formatter="timeFormatter"
|
||||||
@date-selected="endDateSelected"
|
@date-selected="endDateSelected"
|
||||||
/>
|
/>
|
||||||
@ -74,14 +77,15 @@
|
|||||||
autocorrect="off"
|
autocorrect="off"
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
aria-label="End time"
|
aria-label="End time"
|
||||||
@input="validateAllBounds('endDate')"
|
@input="validateInput('endTime')"
|
||||||
|
@change="reportValidity('endTime')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pr-time-input pr-time-input--buttons">
|
<div class="pr-time-input pr-time-input--buttons">
|
||||||
<button
|
<button
|
||||||
class="c-button c-button--major icon-check"
|
class="c-button c-button--major icon-check"
|
||||||
:disabled="isDisabled"
|
:disabled="hasInputValidityError"
|
||||||
aria-label="Submit time bounds"
|
aria-label="Submit time bounds"
|
||||||
@click.prevent="handleFormSubmission(true)"
|
@click.prevent="handleFormSubmission(true)"
|
||||||
></button>
|
></button>
|
||||||
@ -125,6 +129,7 @@ export default {
|
|||||||
return {
|
return {
|
||||||
timeFormatter: this.getFormatter(timeSystem.timeFormat),
|
timeFormatter: this.getFormatter(timeSystem.timeFormat),
|
||||||
durationFormatter: this.getFormatter(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER),
|
durationFormatter: this.getFormatter(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER),
|
||||||
|
timeSystemKey: timeSystem.key,
|
||||||
bounds: {
|
bounds: {
|
||||||
start: bounds.start,
|
start: bounds.start,
|
||||||
end: bounds.end
|
end: bounds.end
|
||||||
@ -136,9 +141,29 @@ export default {
|
|||||||
endTime: ''
|
endTime: ''
|
||||||
},
|
},
|
||||||
isUTCBased: timeSystem.isUTCBased,
|
isUTCBased: timeSystem.isUTCBased,
|
||||||
isDisabled: false
|
inputValidityMap: {
|
||||||
|
startDate: { valid: true },
|
||||||
|
startTime: { valid: true },
|
||||||
|
endDate: { valid: true },
|
||||||
|
endTime: { valid: true }
|
||||||
|
},
|
||||||
|
logicalValidityMap: {
|
||||||
|
limit: { valid: true },
|
||||||
|
bounds: { valid: true }
|
||||||
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
hasInputValidityError() {
|
||||||
|
return Object.values(this.inputValidityMap).some((isValid) => !isValid.valid);
|
||||||
|
},
|
||||||
|
hasLogicalValidationErrors() {
|
||||||
|
return Object.values(this.logicalValidityMap).some((isValid) => !isValid.valid);
|
||||||
|
},
|
||||||
|
isValid() {
|
||||||
|
return !this.hasInputValidityError && !this.hasLogicalValidationErrors;
|
||||||
|
}
|
||||||
|
},
|
||||||
watch: {
|
watch: {
|
||||||
inputBounds: {
|
inputBounds: {
|
||||||
handler(newBounds) {
|
handler(newBounds) {
|
||||||
@ -168,25 +193,17 @@ export default {
|
|||||||
this.setBounds(bounds);
|
this.setBounds(bounds);
|
||||||
this.setViewFromBounds(bounds);
|
this.setViewFromBounds(bounds);
|
||||||
},
|
},
|
||||||
clearAllValidation() {
|
|
||||||
[this.$refs.startDate, this.$refs.endDate].forEach(this.clearValidationForInput);
|
|
||||||
},
|
|
||||||
clearValidationForInput(input) {
|
|
||||||
if (input) {
|
|
||||||
input.setCustomValidity('');
|
|
||||||
input.title = '';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setBounds(bounds) {
|
setBounds(bounds) {
|
||||||
this.bounds = bounds;
|
this.bounds = bounds;
|
||||||
},
|
},
|
||||||
setViewFromBounds(bounds) {
|
setViewFromBounds(bounds) {
|
||||||
this.formattedBounds.start = this.timeFormatter.format(bounds.start).split(' ')[0];
|
this.formattedBounds.startDate = this.timeFormatter.format(bounds.start).split(' ')[0];
|
||||||
this.formattedBounds.end = this.timeFormatter.format(bounds.end).split(' ')[0];
|
this.formattedBounds.endDate = this.timeFormatter.format(bounds.end).split(' ')[0];
|
||||||
this.formattedBounds.startTime = this.durationFormatter.format(Math.abs(bounds.start));
|
this.formattedBounds.startTime = this.durationFormatter.format(Math.abs(bounds.start));
|
||||||
this.formattedBounds.endTime = this.durationFormatter.format(Math.abs(bounds.end));
|
this.formattedBounds.endTime = this.durationFormatter.format(Math.abs(bounds.end));
|
||||||
},
|
},
|
||||||
setTimeSystem(timeSystem) {
|
setTimeSystem(timeSystem) {
|
||||||
|
this.timeSystemKey = timeSystem.key;
|
||||||
this.timeFormatter = this.getFormatter(timeSystem.timeFormat);
|
this.timeFormatter = this.getFormatter(timeSystem.timeFormat);
|
||||||
this.durationFormatter = this.getFormatter(
|
this.durationFormatter = this.getFormatter(
|
||||||
timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER
|
timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER
|
||||||
@ -201,10 +218,10 @@ export default {
|
|||||||
setBoundsFromView(dismiss) {
|
setBoundsFromView(dismiss) {
|
||||||
if (this.$refs.fixedDeltaInput.checkValidity()) {
|
if (this.$refs.fixedDeltaInput.checkValidity()) {
|
||||||
let start = this.timeFormatter.parse(
|
let start = this.timeFormatter.parse(
|
||||||
`${this.formattedBounds.start} ${this.formattedBounds.startTime}`
|
`${this.formattedBounds.startDate} ${this.formattedBounds.startTime}`
|
||||||
);
|
);
|
||||||
let end = this.timeFormatter.parse(
|
let end = this.timeFormatter.parse(
|
||||||
`${this.formattedBounds.end} ${this.formattedBounds.endTime}`
|
`${this.formattedBounds.endDate} ${this.formattedBounds.endTime}`
|
||||||
);
|
);
|
||||||
|
|
||||||
this.$emit('update', { start, end });
|
this.$emit('update', { start, end });
|
||||||
@ -215,96 +232,93 @@ export default {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handleFormSubmission(shouldDismiss) {
|
clearAllValidation() {
|
||||||
this.validateAllBounds('startDate');
|
Object.keys(this.inputValidityMap).forEach(this.clearValidation);
|
||||||
this.validateAllBounds('endDate');
|
},
|
||||||
|
clearValidation(refName) {
|
||||||
|
const input = this.getInput(refName);
|
||||||
|
|
||||||
if (!this.isDisabled) {
|
input.setCustomValidity('');
|
||||||
|
input.title = '';
|
||||||
|
},
|
||||||
|
handleFormSubmission(shouldDismiss) {
|
||||||
|
this.validateLimit();
|
||||||
|
this.reportValidity('limit');
|
||||||
|
this.validateBounds();
|
||||||
|
this.reportValidity('bounds');
|
||||||
|
|
||||||
|
if (this.isValid) {
|
||||||
this.setBoundsFromView(shouldDismiss);
|
this.setBoundsFromView(shouldDismiss);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
validateAllBounds(ref) {
|
validateInput(refName) {
|
||||||
this.isDisabled = false;
|
this.clearAllValidation();
|
||||||
|
|
||||||
if (!this.areBoundsFormatsValid()) {
|
const inputType = refName.includes('Date') ? 'Date' : 'Time';
|
||||||
this.isDisabled = true;
|
const formatter = inputType === 'Date' ? this.timeFormatter : this.durationFormatter;
|
||||||
return false;
|
const validationResult = formatter.validate(this.formattedBounds[refName])
|
||||||
}
|
? { valid: true }
|
||||||
|
: { valid: false, message: `Invalid ${inputType}` };
|
||||||
|
|
||||||
let validationResult = { valid: true };
|
this.inputValidityMap[refName] = validationResult;
|
||||||
const currentInput = this.$refs[ref];
|
},
|
||||||
|
validateBounds() {
|
||||||
return [this.$refs.startDate, this.$refs.endDate].every((input) => {
|
const bounds = {
|
||||||
let boundsValues = {
|
|
||||||
start: this.timeFormatter.parse(
|
start: this.timeFormatter.parse(
|
||||||
`${this.formattedBounds.start} ${this.formattedBounds.startTime}`
|
`${this.formattedBounds.startDate} ${this.formattedBounds.startTime}`
|
||||||
),
|
),
|
||||||
end: this.timeFormatter.parse(
|
end: this.timeFormatter.parse(
|
||||||
`${this.formattedBounds.end} ${this.formattedBounds.endTime}`
|
`${this.formattedBounds.endDate} ${this.formattedBounds.endTime}`
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
//TODO: Do we need limits here? We have conductor limits disabled right now
|
|
||||||
// const limit = this.getBoundsLimit();
|
|
||||||
const limit = false;
|
|
||||||
|
|
||||||
if (this.isUTCBased && limit && boundsValues.end - boundsValues.start > limit) {
|
this.logicalValidityMap.bounds = this.openmct.time.validateBounds(bounds);
|
||||||
if (input === currentInput) {
|
},
|
||||||
validationResult = {
|
validateLimit(bounds) {
|
||||||
|
const limit = this.configuration?.menuOptions
|
||||||
|
?.filter((option) => option.timeSystem === this.timeSystemKey)
|
||||||
|
?.find((option) => option.limit)?.limit;
|
||||||
|
|
||||||
|
if (this.isUTCBased && limit && bounds.end - bounds.start > limit) {
|
||||||
|
this.logicalValidityMap.limit = {
|
||||||
valid: false,
|
valid: false,
|
||||||
message: 'Start and end difference exceeds allowable limit'
|
message: 'Start and end difference exceeds allowable limit'
|
||||||
};
|
};
|
||||||
|
} else {
|
||||||
|
this.logicalValidityMap.limit = { valid: true };
|
||||||
}
|
}
|
||||||
} else if (input === currentInput) {
|
|
||||||
validationResult = this.openmct.time.validateBounds(boundsValues);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.handleValidationResults(input, validationResult);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
areBoundsFormatsValid() {
|
reportValidity(refName) {
|
||||||
return [this.$refs.startDate, this.$refs.endDate].every((input) => {
|
const input = this.getInput(refName);
|
||||||
const formattedDate =
|
const validationResult = this.inputValidityMap[refName] ?? this.logicalValidityMap[refName];
|
||||||
input === this.$refs.startDate
|
|
||||||
? `${this.formattedBounds.start} ${this.formattedBounds.startTime}`
|
|
||||||
: `${this.formattedBounds.end} ${this.formattedBounds.endTime}`;
|
|
||||||
|
|
||||||
const validationResult = this.timeFormatter.validate(formattedDate)
|
|
||||||
? { valid: true }
|
|
||||||
: { valid: false, message: 'Invalid date' };
|
|
||||||
|
|
||||||
return this.handleValidationResults(input, validationResult);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
getBoundsLimit() {
|
|
||||||
const configuration = this.configuration.menuOptions
|
|
||||||
.filter((option) => option.timeSystem === this.timeSystem.key)
|
|
||||||
.find((option) => option.limit);
|
|
||||||
|
|
||||||
const limit = configuration ? configuration.limit : undefined;
|
|
||||||
|
|
||||||
return limit;
|
|
||||||
},
|
|
||||||
handleValidationResults(input, validationResult) {
|
|
||||||
if (validationResult.valid !== true) {
|
if (validationResult.valid !== true) {
|
||||||
input.setCustomValidity(validationResult.message);
|
input.setCustomValidity(validationResult.message);
|
||||||
input.title = validationResult.message;
|
input.title = validationResult.message;
|
||||||
this.isDisabled = true;
|
this.hasLogicalValidationErrors = true;
|
||||||
} else {
|
} else {
|
||||||
input.setCustomValidity('');
|
input.setCustomValidity('');
|
||||||
input.title = '';
|
input.title = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$refs.fixedDeltaInput.reportValidity();
|
this.$refs.fixedDeltaInput.reportValidity();
|
||||||
|
},
|
||||||
|
getInput(refName) {
|
||||||
|
if (Object.keys(this.inputValidityMap).includes(refName)) {
|
||||||
|
return this.$refs[refName];
|
||||||
|
}
|
||||||
|
|
||||||
return validationResult.valid;
|
return this.$refs.startDate;
|
||||||
},
|
},
|
||||||
startDateSelected(date) {
|
startDateSelected(date) {
|
||||||
this.formattedBounds.start = this.timeFormatter.format(date).split(' ')[0];
|
this.formattedBounds.startDate = this.timeFormatter.format(date).split(' ')[0];
|
||||||
this.validateAllBounds('startDate');
|
this.validateInput('startDate');
|
||||||
|
this.reportValidity('startDate');
|
||||||
},
|
},
|
||||||
endDateSelected(date) {
|
endDateSelected(date) {
|
||||||
this.formattedBounds.end = this.timeFormatter.format(date).split(' ')[0];
|
this.formattedBounds.endDate = this.timeFormatter.format(date).split(' ')[0];
|
||||||
this.validateAllBounds('endDate');
|
this.validateInput('endDate');
|
||||||
|
this.reportValidity('endDate');
|
||||||
},
|
},
|
||||||
hide($event) {
|
hide($event) {
|
||||||
if ($event.target.className.indexOf('c-button icon-x') > -1) {
|
if ($event.target.className.indexOf('c-button icon-x') > -1) {
|
||||||
|
@ -79,6 +79,7 @@ export default {
|
|||||||
const svgWidth = ref(0);
|
const svgWidth = ref(0);
|
||||||
const svgHeight = ref(0);
|
const svgHeight = ref(0);
|
||||||
const axisTransform = ref('translate(0,20)');
|
const axisTransform = ref('translate(0,20)');
|
||||||
|
const alignmentOffset = ref(0);
|
||||||
const nowMarkerStyle = reactive({
|
const nowMarkerStyle = reactive({
|
||||||
height: '0px',
|
height: '0px',
|
||||||
left: '0px'
|
left: '0px'
|
||||||
@ -100,6 +101,7 @@ export default {
|
|||||||
svgWidth,
|
svgWidth,
|
||||||
svgHeight,
|
svgHeight,
|
||||||
axisTransform,
|
axisTransform,
|
||||||
|
alignmentOffset,
|
||||||
nowMarkerStyle,
|
nowMarkerStyle,
|
||||||
openmct
|
openmct
|
||||||
};
|
};
|
||||||
|
@ -32,6 +32,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import mount from 'utils/mount';
|
import mount from 'utils/mount';
|
||||||
|
|
||||||
|
import { encode_url } from '../../utils/encoding';
|
||||||
import AboutDialog from './AboutDialog.vue';
|
import AboutDialog from './AboutDialog.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -39,7 +40,7 @@ export default {
|
|||||||
mounted() {
|
mounted() {
|
||||||
const branding = this.openmct.branding();
|
const branding = this.openmct.branding();
|
||||||
if (branding.smallLogoImage) {
|
if (branding.smallLogoImage) {
|
||||||
this.$refs.aboutLogo.style.backgroundImage = `url('${branding.smallLogoImage}')`;
|
this.$refs.aboutLogo.style.backgroundImage = `url('${encode_url(branding.smallLogoImage)}')`;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
25
src/utils/encoding.js
Normal file
25
src/utils/encoding.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
export function encode_url(url) {
|
||||||
|
return url ? encodeURI(url) : url;
|
||||||
|
}
|
Reference in New Issue
Block a user