From 0340fe18fa576578ac9b1f7fa7faf72a5b9996d7 Mon Sep 17 00:00:00 2001 From: Charles Hacskaylo Date: Wed, 24 Jan 2024 21:18:29 -0800 Subject: [PATCH 01/17] Change Imagery positional freshness label from 'POS' to 'ROV' (#7409) Closes #7404 - Change 'POS' to 'ROV'. --- src/plugins/imagery/components/ImageryView.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/imagery/components/ImageryView.vue b/src/plugins/imagery/components/ImageryView.vue index 4615b95ad3..543ef0875e 100644 --- a/src/plugins/imagery/components/ImageryView.vue +++ b/src/plugins/imagery/components/ImageryView.vue @@ -145,7 +145,7 @@ v-if="relatedTelemetry.hasRelatedTelemetry && isSpacecraftPositionFresh" class="c-imagery__age icon-check c-imagery--new no-animation" > - POS + ROV From 450cab428fc9c30184fc49da7dba6f601b276851 Mon Sep 17 00:00:00 2001 From: John Hill Date: Wed, 24 Jan 2024 21:26:48 -0800 Subject: [PATCH 02/17] move performance tests to GHA (#7412) * move performance tests to GHA * no need for chrome beta * Add baseline imagery test * skip flaky app * lint --- .circleci/config.yml | 22 ++-- .github/workflows/e2e-perf.yml | 58 +++++++++ .../plugins/plot/overlayPlot.e2e.spec.js | 110 +++++++++--------- e2e/tests/visual-a11y/imagery.visual.spec.js | 93 +++++++++++++++ 4 files changed, 217 insertions(+), 66 deletions(-) create mode 100644 .github/workflows/e2e-perf.yml create mode 100644 e2e/tests/visual-a11y/imagery.visual.spec.js diff --git a/.circleci/config.yml b/.circleci/config.yml index 1388bf5352..c0d6981dbb 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -5,20 +5,20 @@ executors: - image: mcr.microsoft.com/playwright:v1.39.0-focal environment: 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_LOGLEVEL: 'debug' # Enable DEBUG level logging for Percy (Issue: https://github.com/nasa/openmct/issues/5742) + PERCY_POSTINSTALL_BROWSER: "true" # Needed to store the percy browser in cache deps + PERCY_LOGLEVEL: "debug" # Enable DEBUG level logging for Percy (Issue: https://github.com/nasa/openmct/issues/5742) ubuntu: machine: image: ubuntu-2204:current docker_layer_caching: true parameters: BUST_CACHE: - description: 'Set this with the CircleCI UI Trigger Workflow button (boolean = true) to bust the cache!' + description: "Set this with the CircleCI UI Trigger Workflow button (boolean = true) to bust the cache!" default: false type: boolean commands: build_and_install: - description: 'All steps used to build and install. Will use cache if found' + description: "All steps used to build and install. Will use cache if found" parameters: node-version: type: string @@ -30,7 +30,7 @@ commands: node-version: << parameters.node-version >> - run: npm install --no-audit --progress=false restore_cache_cmd: - description: 'Custom command for restoring cache with the ability to bust cache. When BUST_CACHE is set to true, jobs will not restore cache' + description: "Custom command for restoring cache with the ability to bust cache. When BUST_CACHE is set to true, jobs will not restore cache" parameters: node-version: type: string @@ -42,7 +42,7 @@ commands: - restore_cache: key: deps--{{ arch }}--{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}-{{ checksum ".circleci/config.yml" }} save_cache_cmd: - description: 'Custom command for saving cache.' + description: "Custom command for saving cache." parameters: node-version: type: string @@ -53,7 +53,7 @@ commands: - ~/.npm - node_modules generate_and_store_version_and_filesystem_artifacts: - description: 'Track important packages and files' + description: "Track important packages and files" steps: - run: | [[ $EUID -ne 0 ]] && (sudo mkdir -p /tmp/artifacts && sudo chmod 777 /tmp/artifacts) || (mkdir -p /tmp/artifacts && chmod 777 /tmp/artifacts) @@ -64,7 +64,7 @@ commands: - store_artifacts: path: /tmp/artifacts/ generate_e2e_code_cov_report: - description: 'Generate e2e code coverage artifacts and publish to codecov.io. Needed to that we can ignore the exit code status of the npm run test' + description: "Generate e2e code coverage artifacts and publish to codecov.io. Needed to that we can ignore the exit code status of the npm run test" parameters: suite: type: string @@ -129,7 +129,7 @@ jobs: node-version: lts/hydrogen - when: #Only install chrome-beta when running the 'full' suite to save $$$ condition: - equal: ['full', <>] + equal: ["full", <>] steps: - run: npx playwright install chrome-beta - run: SHARD="$((${CIRCLE_NODE_INDEX}+1))"; npm run test:e2e:<> -- --shard=${SHARD}/${CIRCLE_NODE_TOTAL} @@ -251,8 +251,6 @@ workflows: - e2e-test: name: e2e-stable suite: stable - - mem-test - - perf-test - visual-a11y-tests: name: visual-test-ci suite: ci @@ -278,7 +276,7 @@ workflows: - e2e-couchdb triggers: - schedule: - cron: '0 0 * * *' + cron: "0 0 * * *" filters: branches: only: diff --git a/.github/workflows/e2e-perf.yml b/.github/workflows/e2e-perf.yml new file mode 100644 index 0000000000..2255bde61b --- /dev/null +++ b/.github/workflows/e2e-perf.yml @@ -0,0 +1,58 @@ +name: 'e2e-perf' +on: + push: + branches: master + workflow_dispatch: + pull_request: + types: + - labeled + - opened + schedule: + - cron: '0 0 * * *' +jobs: + e2e-full: + if: contains(github.event.pull_request.labels.*.name, 'pr:e2e:perf') || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 'lts/hydrogen' + + - name: Cache NPM dependencies + uses: actions/cache@v3 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package.json') }} + restore-keys: | + ${{ runner.os }}-node- + + - run: npx playwright@1.39.0 install + - run: npm install --cache ~/.npm --no-audit --progress=false + - run: npm run test:perf:localhost + - run: npm run test:perf:contract + - run: npm run test:perf:memory + - name: Archive test results + if: success() || failure() + uses: actions/upload-artifact@v3 + with: + path: test-results + + - name: Remove pr:e2e:perf label (if present) + if: always() + uses: actions/github-script@v6 + with: + script: | + const { owner, repo, number } = context.issue; + const labelToRemove = 'pr:e2e:perf'; + try { + await github.rest.issues.removeLabel({ + owner, + repo, + issue_number: number, + name: labelToRemove + }); + } catch (error) { + core.warning(`Failed to remove ' + labelToRemove + ' label: ${error.message}`); + } diff --git a/e2e/tests/functional/plugins/plot/overlayPlot.e2e.spec.js b/e2e/tests/functional/plugins/plot/overlayPlot.e2e.spec.js index a80f15be0a..52c6f0fb3f 100644 --- a/e2e/tests/functional/plugins/plot/overlayPlot.e2e.spec.js +++ b/e2e/tests/functional/plugins/plot/overlayPlot.e2e.spec.js @@ -63,78 +63,80 @@ test.describe('Overlay Plot', () => { await expect(seriesColorSwatch).toHaveCSS('background-color', 'rgb(255, 166, 61)'); }); - test('Limit lines persist when series is moved to another Y Axis and on refresh', async ({ - page - }) => { - test.info().annotations.push({ - type: 'issue', - description: 'https://github.com/nasa/openmct/issues/6338' - }); - // Create an Overlay Plot with a default SWG - const overlayPlot = await createDomainObjectWithDefaults(page, { - type: 'Overlay Plot' - }); + //skipping due to https://github.com/nasa/openmct/issues/7405 + test.fixme( + 'Limit lines persist when series is moved to another Y Axis and on refresh', + async ({ page }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/nasa/openmct/issues/6338' + }); + // Create an Overlay Plot with a default SWG + const overlayPlot = await createDomainObjectWithDefaults(page, { + type: 'Overlay Plot' + }); - const swgA = await createDomainObjectWithDefaults(page, { - type: 'Sine Wave Generator', - parent: overlayPlot.uuid - }); + const swgA = await createDomainObjectWithDefaults(page, { + type: 'Sine Wave Generator', + parent: overlayPlot.uuid + }); - await page.goto(overlayPlot.url); + 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); + // 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(); + // 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' }) - .locator('[title="Display limit lines"]~div input') - .check(); + // 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' }) + .locator('[title="Display limit lines"]~div input') + .check(); - await assertLimitLinesExistAndAreVisible(page); + await assertLimitLinesExistAndAreVisible(page); - // Save (exit edit mode) - await page.locator('button[title="Save"]').click(); - await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); + // Save (exit edit mode) + await page.locator('button[title="Save"]').click(); + await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); - await assertLimitLinesExistAndAreVisible(page); + await assertLimitLinesExistAndAreVisible(page); - await page.reload(); + await page.reload(); - await assertLimitLinesExistAndAreVisible(page); + await assertLimitLinesExistAndAreVisible(page); - // Enter edit mode - await page.getByLabel('Edit Object').click(); + // Enter edit mode + await page.getByLabel('Edit Object').click(); - await page.getByRole('tab', { name: 'Elements' }).click(); + await page.getByRole('tab', { name: 'Elements' }).click(); - // Drag Sine Wave Generator series from Y Axis 1 into Y Axis 2 - await page - .locator(`#inspector-elements-tree >> text=${swgA.name}`) - .dragTo(page.locator('[aria-label="Element Item Group Y Axis 2"]')); + // Drag Sine Wave Generator series from Y Axis 1 into Y Axis 2 + await page + .locator(`#inspector-elements-tree >> text=${swgA.name}`) + .dragTo(page.locator('[aria-label="Element Item Group Y Axis 2"]')); - await assertLimitLinesExistAndAreVisible(page); + await assertLimitLinesExistAndAreVisible(page); - // Save (exit edit mode) - await page.locator('button[title="Save"]').click(); - await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); + // Save (exit edit mode) + await page.locator('button[title="Save"]').click(); + await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); - await assertLimitLinesExistAndAreVisible(page); + await assertLimitLinesExistAndAreVisible(page); - await page.reload(); + await page.reload(); - await assertLimitLinesExistAndAreVisible(page); - }); + await assertLimitLinesExistAndAreVisible(page); + } + ); test('The elements pool supports dragging series into multiple y-axis buckets', async ({ page diff --git a/e2e/tests/visual-a11y/imagery.visual.spec.js b/e2e/tests/visual-a11y/imagery.visual.spec.js new file mode 100644 index 0000000000..78de781dd5 --- /dev/null +++ b/e2e/tests/visual-a11y/imagery.visual.spec.js @@ -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. + *****************************************************************************/ + +import percySnapshot from '@percy/playwright'; + +import { createDomainObjectWithDefaults, setRealTimeMode } from '../../appActions.js'; +import { VISUAL_URL } from '../../constants.js'; +import { expect, test } from '../../pluginFixtures.js'; + +test.describe('Visual - Example Imagery', () => { + let exampleImagery; + let parentLayout; + + test.beforeEach(async ({ page }) => { + await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' }); + + parentLayout = await createDomainObjectWithDefaults(page, { + type: 'Display Layout', + name: 'Parent Layout' + }); + + exampleImagery = await createDomainObjectWithDefaults(page, { + type: 'Example Imagery', + name: 'Example Imagery Test', + parent: parentLayout.uuid + }); + + // Modify Example Imagery to create a really stable Example Imagery + await page.goto(exampleImagery.url, { waitUntil: 'domcontentloaded' }); + await page.getByRole('button', { name: 'More actions' }).click(); + await page.getByRole('menuitem', { name: 'Edit Properties...' }).click(); + await page + .locator('#imageLocation-textarea') + .fill( + 'https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg' + ); + await page.getByRole('button', { name: 'Save' }).click(); + await page.reload({ waitUntil: 'domcontentloaded' }); + await page.getByTitle('Collapse Browse Pane').click(); + await page.getByTitle('Collapse Inspect Pane').click(); + }); + + test('Example Imagery in Fixed Time', async ({ page, theme }) => { + await page.goto(exampleImagery.url, { waitUntil: 'domcontentloaded' }); + + await expect(page.getByLabel('Image Wrapper')).toBeVisible(); + + await percySnapshot(page, `Example Imagery in Fixed Time (theme: ${theme})`); + + await page.getByLabel('Image Wrapper').hover(); + + await percySnapshot(page, `Example Imagery Hover in Fixed Time (theme: ${theme})`); + }); + + test('Example Imagery in Real Time', async ({ page, theme }) => { + await page.goto(exampleImagery.url, { waitUntil: 'domcontentloaded' }); + + await setRealTimeMode(page, true); + //Temporary to close the dialog + await page.getByLabel('Submit time offsets').click(); + + await expect(page.getByLabel('Image Wrapper')).toBeVisible(); + + await percySnapshot(page, `Example Imagery in Real Time (theme: ${theme})`); + }); + + test('Example Imagery in Display Layout', async ({ page, theme }) => { + await page.goto(parentLayout.url, { waitUntil: 'domcontentloaded' }); + + await expect(page.getByLabel('Image Wrapper')).toBeVisible(); + + await percySnapshot(page, `Example Imagery in Display Layout (theme: ${theme})`); + }); +}); From 2b2c74da9cdda4a1caebd7d7b7cefe8bc45c813c Mon Sep 17 00:00:00 2001 From: Scott Bell Date: Thu, 25 Jan 2024 06:36:44 +0100 Subject: [PATCH 03/17] New action to reload an individual view and all of its children (#7362) * add reload action plugin * checking for domain object before reloading * check if objects are equal before refreshing * add test * lint * change to label * ensure object styles are initialized * resubscribe to staleness too * add better labels for tabels * ensure tab uses exact for label now due to table aria changes * fix table tests * make tabs exact * update conflicts --------- Co-authored-by: Jesse Mazzella --- .../reloadAction/reloadAction.e2e.spec.js | 125 ++++++++++++++++++ .../functional/plugins/tabs/tabs.e2e.spec.js | 8 +- .../telemetryTable/telemetryTable.e2e.spec.js | 19 ++- e2e/tests/performance/tabs.e2e.spec.js | 12 +- src/MCT.js | 1 + src/plugins/plugins.js | 2 + src/plugins/reloadAction/ReloadAction.js | 37 ++++++ src/plugins/reloadAction/plugin.js | 28 ++++ .../telemetryTable/components/TableCell.vue | 2 +- .../components/TableComponent.vue | 1 + src/ui/components/ObjectView.vue | 10 ++ 11 files changed, 227 insertions(+), 18 deletions(-) create mode 100644 e2e/tests/functional/plugins/reloadAction/reloadAction.e2e.spec.js create mode 100644 src/plugins/reloadAction/ReloadAction.js create mode 100644 src/plugins/reloadAction/plugin.js diff --git a/e2e/tests/functional/plugins/reloadAction/reloadAction.e2e.spec.js b/e2e/tests/functional/plugins/reloadAction/reloadAction.e2e.spec.js new file mode 100644 index 0000000000..c64e19a007 --- /dev/null +++ b/e2e/tests/functional/plugins/reloadAction/reloadAction.e2e.spec.js @@ -0,0 +1,125 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2023, 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. + *****************************************************************************/ +import { createDomainObjectWithDefaults, expandEntireTree } from '../../../../appActions.js'; +import { expect, test } from '../../../../pluginFixtures.js'; + +test.describe('Reload action', () => { + test.beforeEach(async ({ page }) => { + await page.goto('./', { waitUntil: 'domcontentloaded' }); + + const displayLayout = await createDomainObjectWithDefaults(page, { + type: 'Display Layout' + }); + + const alphaTable = await createDomainObjectWithDefaults(page, { + type: 'Telemetry Table', + name: 'Alpha Table' + }); + + const betaTable = await createDomainObjectWithDefaults(page, { + type: 'Telemetry Table', + name: 'Beta Table' + }); + + await createDomainObjectWithDefaults(page, { + type: 'Sine Wave Generator', + parent: alphaTable.uuid, + customParameters: { + '[aria-label="Data Rate (hz)"]': '0.001' + } + }); + + await createDomainObjectWithDefaults(page, { + type: 'Sine Wave Generator', + parent: betaTable.uuid, + customParameters: { + '[aria-label="Data Rate (hz)"]': '0.001' + } + }); + + await page.goto(displayLayout.url); + + // Expand all folders + await expandEntireTree(page); + + await page.getByLabel('Edit Object', { exact: true }).click(); + + await page.dragAndDrop(`text='Alpha Table'`, '.l-layout__grid-holder', { + targetPosition: { x: 0, y: 0 } + }); + + await page.dragAndDrop(`text='Beta Table'`, '.l-layout__grid-holder', { + targetPosition: { x: 0, y: 250 } + }); + + await page.locator('button[title="Save"]').click(); + await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); + }); + + test('can reload display layout and its children', async ({ page }) => { + const beforeReloadAlphaTelemetryValue = await page + .getByLabel('Alpha Table table content') + .getByLabel('wavelengths table cell') + .first() + .getAttribute('title'); + const beforeReloadBetaTelemetryValue = await page + .getByLabel('Beta Table table content') + .getByLabel('wavelengths table cell') + .first() + .getAttribute('title'); + // reload alpha + await page.getByTitle('View menu items').first().click(); + await page.getByRole('menuitem', { name: /Reload/ }).click(); + + const afterReloadAlphaTelemetryValue = await page + .getByLabel('Alpha Table table content') + .getByLabel('wavelengths table cell') + .first() + .getAttribute('title'); + const afterReloadBetaTelemetryValue = await page + .getByLabel('Beta Table table content') + .getByLabel('wavelengths table cell') + .first() + .getAttribute('title'); + + expect(beforeReloadAlphaTelemetryValue).not.toEqual(afterReloadAlphaTelemetryValue); + expect(beforeReloadBetaTelemetryValue).toEqual(afterReloadBetaTelemetryValue); + + // now reload parent + await page.getByTitle('More actions').click(); + await page.getByRole('menuitem', { name: /Reload/ }).click(); + + const fullReloadAlphaTelemetryValue = await page + .getByLabel('Alpha Table table content') + .getByLabel('wavelengths table cell') + .first() + .getAttribute('title'); + const fullReloadBetaTelemetryValue = await page + .getByLabel('Beta Table table content') + .getByLabel('wavelengths table cell') + .first() + .getAttribute('title'); + + expect(fullReloadAlphaTelemetryValue).not.toEqual(afterReloadAlphaTelemetryValue); + expect(fullReloadBetaTelemetryValue).not.toEqual(afterReloadBetaTelemetryValue); + }); +}); diff --git a/e2e/tests/functional/plugins/tabs/tabs.e2e.spec.js b/e2e/tests/functional/plugins/tabs/tabs.e2e.spec.js index 7d5df80fb8..9f8c9eb8c7 100644 --- a/e2e/tests/functional/plugins/tabs/tabs.e2e.spec.js +++ b/e2e/tests/functional/plugins/tabs/tabs.e2e.spec.js @@ -50,7 +50,7 @@ test.describe('Tabs View', () => { page.goto(tabsView.url); // select first tab - await page.getByLabel(`${table.name} tab`).click(); + await page.getByLabel(`${table.name} tab`, { exact: true }).click(); // ensure table header visible await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible(); @@ -58,7 +58,7 @@ test.describe('Tabs View', () => { await expect(page.locator('canvas[id=webglContext]')).toBeHidden(); // select second tab - await page.getByLabel(`${notebook.name} tab`).click(); + await page.getByLabel(`${notebook.name} tab`, { exact: true }).click(); // ensure notebook visible await expect(page.locator('.c-notebook__drag-area')).toBeVisible(); @@ -67,7 +67,7 @@ test.describe('Tabs View', () => { await expect(page.locator('canvas[id=webglContext]')).toBeHidden(); // select third tab - await page.getByLabel(`${sineWaveGenerator.name} tab`).click(); + await page.getByLabel(`${sineWaveGenerator.name} tab`, { exact: true }).click(); // expect sine wave generator visible await expect(page.locator('.c-plot')).toBeVisible(); @@ -78,7 +78,7 @@ test.describe('Tabs View', () => { await expect(page.locator('canvas').nth(1)).toBeVisible(); // now try to select the first tab again - await page.getByLabel(`${table.name} tab`).click(); + await page.getByLabel(`${table.name} tab`, { exact: true }).click(); // ensure table header visible await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible(); diff --git a/e2e/tests/functional/plugins/telemetryTable/telemetryTable.e2e.spec.js b/e2e/tests/functional/plugins/telemetryTable/telemetryTable.e2e.spec.js index 1bc7e062b6..4ce30f034e 100644 --- a/e2e/tests/functional/plugins/telemetryTable/telemetryTable.e2e.spec.js +++ b/e2e/tests/functional/plugins/telemetryTable/telemetryTable.e2e.spec.js @@ -64,10 +64,9 @@ test.describe('Telemetry Table', () => { // Get the most recent telemetry date const latestTelemetryDate = await page - .locator('table.c-telemetry-table__body > tbody > tr') + .getByLabel('table content') + .getByLabel('utc table cell') .last() - .locator('td') - .nth(1) .getAttribute('title'); // Verify that it is <= our new end bound @@ -91,7 +90,7 @@ test.describe('Telemetry Table', () => { await page.getByRole('searchbox', { name: 'message filter input' }).click(); await page.getByRole('searchbox', { name: 'message filter input' }).fill('Roger'); - let cells = await page.getByRole('cell', { name: /Roger/ }).all(); + let cells = await page.getByRole('cell').getByText(/Roger/).all(); // ensure we've got more than one cell expect(cells.length).toBeGreaterThan(1); // ensure the text content of each cell contains the search term @@ -103,7 +102,10 @@ test.describe('Telemetry Table', () => { await page.getByRole('searchbox', { name: 'message filter input' }).click(); await page.getByRole('searchbox', { name: 'message filter input' }).fill('Dodger'); - cells = await page.getByRole('cell', { name: /Dodger/ }).all(); + cells = await page + .getByRole('cell') + .getByText(/Dodger/) + .all(); // ensure we've got more than one cell expect(cells.length).toBe(0); // ensure the text content of each cell contains the search term @@ -135,7 +137,7 @@ test.describe('Telemetry Table', () => { await page.getByRole('searchbox', { name: 'message filter input' }).click(); await page.getByRole('searchbox', { name: 'message filter input' }).fill('/[Rr]oger/'); - let cells = await page.getByRole('cell', { name: /Roger/ }).all(); + let cells = await page.getByRole('cell').getByText(/Roger/).all(); // ensure we've got more than one cell expect(cells.length).toBeGreaterThan(1); // ensure the text content of each cell contains the search term @@ -147,7 +149,10 @@ test.describe('Telemetry Table', () => { await page.getByRole('searchbox', { name: 'message filter input' }).click(); await page.getByRole('searchbox', { name: 'message filter input' }).fill('/[Dd]oger/'); - cells = await page.getByRole('cell', { name: /Dodger/ }).all(); + cells = await page + .getByRole('cell') + .getByText(/Dodger/) + .all(); // ensure we've got more than one cell expect(cells.length).toBe(0); // ensure the text content of each cell contains the search term diff --git a/e2e/tests/performance/tabs.e2e.spec.js b/e2e/tests/performance/tabs.e2e.spec.js index 9689be8a38..3db219f1da 100644 --- a/e2e/tests/performance/tabs.e2e.spec.js +++ b/e2e/tests/performance/tabs.e2e.spec.js @@ -24,7 +24,7 @@ import { createDomainObjectWithDefaults, waitForPlotsToRender } from '../../appA import { expect, test } from '../../pluginFixtures.js'; test.describe('Tabs View', () => { - test('Renders tabbed elements nicely', async ({ page }) => { + test('Renders tabbed elements only when visible', async ({ page }) => { // Code to hook into the requestAnimationFrame function and log each call let animationCalls = []; await page.exposeFunction('logCall', (callCount) => { @@ -64,24 +64,24 @@ test.describe('Tabs View', () => { page.goto(tabsView.url); // select first tab - await page.getByLabel(`${table.name} tab`).click(); + await page.getByLabel(`${table.name} tab`, { exact: true }).click(); // ensure table header visible await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible(); // select second tab - await page.getByLabel(`${notebook.name} tab`).click(); + await page.getByLabel(`${notebook.name} tab`, { exact: true }).click(); // expect notebook visible await expect(page.locator('.c-notebook__drag-area')).toBeVisible(); // select third tab - await page.getByLabel(`${sineWaveGenerator.name} tab`).click(); + await page.getByLabel(`${sineWaveGenerator.name} tab`, { exact: true }).click(); // ensure sine wave generator visible expect(await page.locator('.c-plot').isVisible()).toBe(true); // now select notebook and clear animation calls - await page.getByLabel(`${notebook.name} tab`).click(); + await page.getByLabel(`${notebook.name} tab`, { exact: true }).click(); animationCalls = []; // expect notebook visible await expect(page.locator('.c-notebook__drag-area')).toBeVisible(); @@ -89,7 +89,7 @@ test.describe('Tabs View', () => { // select sine wave generator and clear animation calls animationCalls = []; - await page.getByLabel(`${sineWaveGenerator.name} tab`).click(); + await page.getByLabel(`${sineWaveGenerator.name} tab`, { exact: true }).click(); // ensure sine wave generator visible await waitForPlotsToRender(page); diff --git a/src/MCT.js b/src/MCT.js index 5640995739..0a011b33ab 100644 --- a/src/MCT.js +++ b/src/MCT.js @@ -251,6 +251,7 @@ export class MCT extends EventEmitter { this.install(this.plugins.FlexibleLayout()); this.install(this.plugins.GoToOriginalAction()); this.install(this.plugins.OpenInNewTabAction()); + this.install(this.plugins.ReloadAction()); this.install(this.plugins.WebPage()); this.install(this.plugins.Condition()); this.install(this.plugins.ConditionWidget()); diff --git a/src/plugins/plugins.js b/src/plugins/plugins.js index c1c2e539ad..fb7887c9f3 100644 --- a/src/plugins/plugins.js +++ b/src/plugins/plugins.js @@ -65,6 +65,7 @@ import PerformanceIndicator from './performanceIndicator/plugin.js'; import CouchDBPlugin from './persistence/couch/plugin.js'; import PlanLayout from './plan/plugin.js'; import PlotPlugin from './plot/plugin.js'; +import ReloadAction from './reloadAction/plugin.js'; import RemoteClock from './remoteClock/plugin.js'; import StaticRootPlugin from './staticRootPlugin/plugin.js'; import SummaryWidget from './summaryWidget/plugin.js'; @@ -141,6 +142,7 @@ plugins.Filters = Filters; plugins.ObjectMigration = ObjectMigration; plugins.GoToOriginalAction = GoToOriginalAction; plugins.OpenInNewTabAction = OpenInNewTabAction; +plugins.ReloadAction = ReloadAction; plugins.ClearData = ClearData; plugins.WebPage = WebPagePlugin; plugins.Espresso = Espresso; diff --git a/src/plugins/reloadAction/ReloadAction.js b/src/plugins/reloadAction/ReloadAction.js new file mode 100644 index 0000000000..ff86bff4f5 --- /dev/null +++ b/src/plugins/reloadAction/ReloadAction.js @@ -0,0 +1,37 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2023, 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 default class ReloadAction { + constructor(openmct) { + this.name = 'Reload'; + this.key = 'reload'; + this.description = 'Reload this object and its children'; + this.group = 'action'; + this.priority = 10; + this.cssClass = 'icon-refresh'; + + this.openmct = openmct; + } + invoke(objectPath, view) { + const domainObject = objectPath[0]; + this.openmct.objectViews.emit('reload', domainObject); + } +} diff --git a/src/plugins/reloadAction/plugin.js b/src/plugins/reloadAction/plugin.js new file mode 100644 index 0000000000..c9db76b89a --- /dev/null +++ b/src/plugins/reloadAction/plugin.js @@ -0,0 +1,28 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2023, 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. + *****************************************************************************/ +import ReloadAction from './ReloadAction.js'; + +export default function plugin() { + return function install(openmct) { + openmct.actions.register(new ReloadAction(openmct)); + }; +} diff --git a/src/plugins/telemetryTable/components/TableCell.vue b/src/plugins/telemetryTable/components/TableCell.vue index 4a4fa81ff8..e30b64ce09 100644 --- a/src/plugins/telemetryTable/components/TableCell.vue +++ b/src/plugins/telemetryTable/components/TableCell.vue @@ -22,8 +22,8 @@ @@ -74,8 +78,13 @@ export default { totalRows: { type: Number, default: 0 + }, + telemetryMode: { + type: String, + default: 'performance' } }, + emits: ['telemetry-mode-change'], data() { return { filterNames: [], @@ -93,6 +102,9 @@ export default { return !_.isEqual(filtersToCompare, _.omit(filters, [USE_GLOBAL])); }); }, + isUnlimitedMode() { + return this.telemetryMode === 'unlimited'; + }, label() { if (this.hasMixedFilters) { return FILTER_INDICATOR_LABEL_MIXED; @@ -100,6 +112,22 @@ export default { return FILTER_INDICATOR_LABEL; } }, + rowCount() { + return this.isUnlimitedMode ? this.totalRows : 'LATEST 50'; + }, + rowCountTitle() { + return this.isUnlimitedMode + ? this.totalRows + ' rows visible after any filtering' + : 'performance mode limited to 50 rows'; + }, + telemetryModeButtonLabel() { + return this.isUnlimitedMode ? 'SHOW LATEST 50' : 'SHOW ALL'; + }, + telemetryModeButtonTitle() { + return this.isUnlimitedMode + ? 'Change to Performance mode (latest 50 values)' + : 'Change to show all values'; + }, title() { if (this.hasMixedFilters) { return FILTER_INDICATOR_TITLE_MIXED; @@ -117,6 +145,9 @@ export default { this.table.configuration.off('change', this.handleConfigurationChanges); }, methods: { + toggleTelemetryMode() { + this.$emit('telemetry-mode-change'); + }, setFilterNames() { let names = []; let composition = this.openmct.composition.get(this.table.configuration.domainObject); diff --git a/src/plugins/telemetryTable/components/table-footer-indicator.scss b/src/plugins/telemetryTable/components/table-footer-indicator.scss index 02955af97d..115f769b3c 100644 --- a/src/plugins/telemetryTable/components/table-footer-indicator.scss +++ b/src/plugins/telemetryTable/components/table-footer-indicator.scss @@ -18,6 +18,7 @@ &__counts { //background: rgba(deeppink, 0.1); display: flex; + align-items: center; flex: 1 1 auto; justify-content: flex-end; overflow: hidden; diff --git a/src/plugins/telemetryTable/components/table.scss b/src/plugins/telemetryTable/components/table.scss index 1dda7f06b8..a92b84f210 100644 --- a/src/plugins/telemetryTable/components/table.scss +++ b/src/plugins/telemetryTable/components/table.scss @@ -169,30 +169,13 @@ } &__footer { - $pt: 2px; - border-top: 1px solid $colorInteriorBorder; margin-top: $interiorMargin; - padding: $pt 0; + margin-bottom: $interiorMarginSm; overflow: hidden; - transition: all 250ms; - &:not(.is-filtering) { - .c-frame & { - height: 0; - padding: 0; - visibility: hidden; - } - } - } - - .c-frame & { - // target .c-frame .c-telemetry-table {} - $pt: 2px; - &:hover { - .c-telemetry-table__footer:not(.is-filtering) { - height: $pt + 16px; - padding: initial; - visibility: visible; + .c-frame & { + .c-button { + padding: 2px 5px; } } } diff --git a/src/plugins/telemetryTable/plugin.js b/src/plugins/telemetryTable/plugin.js index b60096b304..c36f081752 100644 --- a/src/plugins/telemetryTable/plugin.js +++ b/src/plugins/telemetryTable/plugin.js @@ -19,16 +19,17 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ + import TableConfigurationViewProvider from './TableConfigurationViewProvider.js'; -import TelemetryTableType from './TelemetryTableType.js'; +import getTelemetryTableType from './TelemetryTableType.js'; import TelemetryTableViewProvider from './TelemetryTableViewProvider.js'; import TelemetryTableViewActions from './ViewActions.js'; -export default function plugin() { +export default function plugin(options) { return function install(openmct) { - openmct.objectViews.addProvider(new TelemetryTableViewProvider(openmct)); + openmct.objectViews.addProvider(new TelemetryTableViewProvider(openmct, options)); openmct.inspectorViews.addProvider(new TableConfigurationViewProvider(openmct)); - openmct.types.addType('table', TelemetryTableType); + openmct.types.addType('table', getTelemetryTableType(options)); openmct.composition.addPolicy((parent, child) => { if (parent.type === 'table') { return Object.prototype.hasOwnProperty.call(child, 'telemetry'); From 1fc6056c513fbbaaab43bd9ae7b47ae186181c6a Mon Sep 17 00:00:00 2001 From: Charles Hacskaylo Date: Fri, 26 Jan 2024 14:55:13 -0800 Subject: [PATCH 08/17] Set disabled items to use disabled property (#7342) * Closes #7322 - New CSS for `aria-disabled = true` property. - Changed multiple items to use aria-disabled instead of .disabled, including: - Action menu - Super menu - Notebook drag area - Tree item style modded to only italicize when is-navigated and is being edited. * Closes #7322 - New CSS for `aria-disabled = true` property. - Changed multiple items to use aria-disabled instead of .disabled, including: - Action menu - Super menu - Notebook drag area - Tree item style modded to only italicize when is-navigated and is being edited. - Create button sets itself to `disabled` when the editor is in use. * Closes #7322 - Create button now _actually_ sets itself to `aria-disabled` when the editor is in use. - CSS removes selector for `.is-editing`. * fix conflict --------- Co-authored-by: John Hill --- src/api/menu/components/MenuComponent.vue | 6 ++++-- src/api/menu/components/SuperMenu.vue | 3 ++- src/plugins/notebook/components/NotebookComponent.vue | 2 +- src/styles/_global.scss | 3 ++- src/ui/layout/CreateButton.vue | 11 +++++++++++ src/ui/layout/create-button.scss | 4 ---- src/ui/layout/mct-tree.scss | 2 -- 7 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/api/menu/components/MenuComponent.vue b/src/api/menu/components/MenuComponent.vue index 60b49b5bfc..d7d334f58b 100644 --- a/src/api/menu/components/MenuComponent.vue +++ b/src/api/menu/components/MenuComponent.vue @@ -28,7 +28,8 @@ v-for="action in actionGroups" :key="action.name" role="menuitem" - :class="[action.cssClass, action.isDisabled ? 'disabled' : '']" + :aria-disabled="action.isDisabled" + :class="action.cssClass" :aria-label="action.name" :title="action.description" @click="action.onItemClicked" @@ -51,7 +52,8 @@ v-for="action in options.actions" :key="action.name" role="menuitem" - :class="[action.cssClass, action.isDisabled ? 'disabled' : '']" + :aria-disabled="action.isDisabled" + :class="action.cssClass" :aria-label="action.name" :title="action.description" @click="action.onItemClicked" diff --git a/src/api/menu/components/SuperMenu.vue b/src/api/menu/components/SuperMenu.vue index 606fe0b3e7..1bbdaf940e 100644 --- a/src/api/menu/components/SuperMenu.vue +++ b/src/api/menu/components/SuperMenu.vue @@ -37,7 +37,8 @@ v-for="action in actionGroups" :key="action.name" role="menuitem" - :class="[action.cssClass, action.isDisabled ? 'disabled' : '']" + :aria-disabled="action.isDisabled" + :class="action.cssClass" :title="action.description" @click="action.onItemClicked" @mouseover="toggleItemDescription(action)" diff --git a/src/plugins/notebook/components/NotebookComponent.vue b/src/plugins/notebook/components/NotebookComponent.vue index ccb1276276..4c2d7a4c02 100644 --- a/src/plugins/notebook/components/NotebookComponent.vue +++ b/src/plugins/notebook/components/NotebookComponent.vue @@ -96,7 +96,7 @@
@@ -134,7 +135,7 @@ export default { this.swimlaneVisibility = this.configuration.swimlaneVisibility; this.clipActivityNames = this.configuration.clipActivityNames; if (this.domainObject.type === 'plan') { - this.planData = getValidatedData(this.domainObject); + this.setPlanData(this.domainObject); } const canvas = document.createElement('canvas'); @@ -177,6 +178,9 @@ export default { this.planViewConfiguration.destroy(); }, methods: { + setPlanData(domainObject) { + this.planData = getValidatedData(domainObject); + }, activityNameFitsRect(activityName, rectWidth) { return this.getTextWidth(activityName) + TEXT_LEFT_PADDING < rectWidth; }, @@ -215,9 +219,7 @@ export default { callback: () => { this.removeFromComposition(this.planObject); this.planObject = domainObject; - this.planData = getValidatedData(domainObject); - this.setStatus(this.openmct.status.get(domainObject.identifier)); - this.setScaleAndGenerateActivities(); + this.handleSelectFileChange(); dialog.dismiss(); } }, @@ -237,9 +239,7 @@ export default { } else { this.planObject = domainObject; this.swimlaneVisibility = this.configuration.swimlaneVisibility; - this.planData = getValidatedData(domainObject); - this.setStatus(this.openmct.status.get(domainObject.identifier)); - this.setScaleAndGenerateActivities(); + this.handleSelectFileChange(domainObject); } }, handleConfigurationChange(newConfiguration) { @@ -259,8 +259,10 @@ export default { this.setScaleAndGenerateActivities(); }, - handleSelectFileChange() { - this.planData = getValidatedData(this.domainObject); + handleSelectFileChange(domainObject) { + const planDomainObject = domainObject || this.domainObject; + this.setPlanData(planDomainObject); + this.setStatus(this.openmct.status.get(planDomainObject.identifier)); this.setScaleAndGenerateActivities(); }, removeFromComposition(domainObject) { @@ -434,7 +436,7 @@ export default { return; } - rawActivities.forEach((rawActivity) => { + rawActivities.forEach((rawActivity, index) => { if (!this.isActivityInBounds(rawActivity)) { return; } @@ -481,13 +483,10 @@ export default { const activity = { color: color, textColor: textColor, - name: rawActivity.name, exceeds: { start: this.xScale(this.viewBounds.start) > this.xScale(rawActivity.start), end: this.xScale(this.viewBounds.end) < this.xScale(rawActivity.end) }, - start: rawActivity.start, - end: rawActivity.end, row: currentRow, textLines: textLines, textStart: textStart, @@ -496,7 +495,11 @@ export default { rectStart: rectX1, rectEnd: showTextInsideRect ? rectX2 : textStart + textWidth, rectWidth: rectWidth, - clipPathId: this.getClipPathId(groupName, rawActivity, currentRow) + clipPathId: this.getClipPathId(groupName, rawActivity, currentRow), + selection: { + groupName, + index + } }; activitiesByRow[currentRow].push(activity); }); @@ -573,6 +576,31 @@ export default { const activityName = activity.name.toLowerCase().replace(/ /g, '-'); return `${groupName}-${activityName}-${activity.start}-${activity.end}-${row}`; + }, + selectActivity({ event, selection }) { + const element = event.currentTarget; + const multiSelect = event.metaKey; + const { groupName, index } = selection; + const rawActivity = this.planData[groupName][index]; + this.openmct.selection.select( + [ + { + element: element, + context: { + type: 'activity', + activity: rawActivity + } + }, + { + element: this.openmct.layout.$refs.browseObject.$el, + context: { + item: this.domainObject, + supportsMultiSelect: true + } + } + ], + multiSelect + ); } } }; diff --git a/src/plugins/plan/inspector/components/PlanActivitiesView.vue b/src/plugins/plan/inspector/components/PlanActivitiesView.vue index 9069dbb547..002023adc2 100644 --- a/src/plugins/plan/inspector/components/PlanActivitiesView.vue +++ b/src/plugins/plan/inspector/components/PlanActivitiesView.vue @@ -20,21 +20,35 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/plan/inspector/components/PlanActivityStatusView.vue b/src/plugins/plan/inspector/components/PlanActivityStatusView.vue new file mode 100644 index 0000000000..f2ccffee6c --- /dev/null +++ b/src/plugins/plan/inspector/components/PlanActivityStatusView.vue @@ -0,0 +1,127 @@ + + + + + diff --git a/src/plugins/plan/inspector/components/PlanActivityView.vue b/src/plugins/plan/inspector/components/PlanActivityTimeView.vue similarity index 68% rename from src/plugins/plan/inspector/components/PlanActivityView.vue rename to src/plugins/plan/inspector/components/PlanActivityTimeView.vue index 9ed488a500..ab1d4d5aee 100644 --- a/src/plugins/plan/inspector/components/PlanActivityView.vue +++ b/src/plugins/plan/inspector/components/PlanActivityTimeView.vue @@ -21,23 +21,23 @@ -->