From 25e7a16c771c91934f1d8f2dfde81f75a6288c37 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 28 Oct 2023 14:57:47 +0000 Subject: [PATCH 001/126] chore(deps-dev): bump cspell from 7.3.6 to 7.3.8 (#7162) Bumps [cspell](https://github.com/streetsidesoftware/cspell) from 7.3.6 to 7.3.8. - [Release notes](https://github.com/streetsidesoftware/cspell/releases) - [Changelog](https://github.com/streetsidesoftware/cspell/blob/main/CHANGELOG.md) - [Commits](https://github.com/streetsidesoftware/cspell/compare/v7.3.6...v7.3.8) --- updated-dependencies: - dependency-name: cspell dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a0339fb912..3ca354d19b 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "codecov": "3.8.3", "comma-separated-values": "3.6.4", "copy-webpack-plugin": "11.0.0", - "cspell": "7.3.6", + "cspell": "7.3.8", "css-loader": "6.8.1", "d3-axis": "3.0.0", "d3-scale": "3.3.0", From 7a8a6d3649c1ca738e8ea6ad0c4e1933b5d47ce0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 29 Oct 2023 06:55:09 -0700 Subject: [PATCH 002/126] chore(deps-dev): bump eslint-plugin-unicorn from 44.0.2 to 48.0.1 (#7163) Bumps [eslint-plugin-unicorn](https://github.com/sindresorhus/eslint-plugin-unicorn) from 44.0.2 to 48.0.1. - [Release notes](https://github.com/sindresorhus/eslint-plugin-unicorn/releases) - [Commits](https://github.com/sindresorhus/eslint-plugin-unicorn/compare/v44.0.2...v48.0.1) --- updated-dependencies: - dependency-name: eslint-plugin-unicorn dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3ca354d19b..08f9698836 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "eslint-plugin-playwright": "0.12.0", "eslint-plugin-prettier": "4.2.1", "eslint-plugin-simple-import-sort": "10.0.0", - "eslint-plugin-unicorn": "44.0.2", + "eslint-plugin-unicorn": "48.0.1", "eslint-plugin-vue": "9.17.0", "eslint-plugin-you-dont-need-lodash-underscore": "6.12.0", "eventemitter3": "1.2.0", From ebe5323f82bad5dabb65cbbe1b4e580964332d63 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 29 Oct 2023 15:55:57 +0000 Subject: [PATCH 003/126] chore(deps-dev): bump painterro from 1.2.78 to 1.2.87 (#7165) Bumps [painterro](https://github.com/devforth/painterro) from 1.2.78 to 1.2.87. - [Release notes](https://github.com/devforth/painterro/releases) - [Changelog](https://github.com/devforth/painterro/blob/master/Release.md) - [Commits](https://github.com/devforth/painterro/compare/v1.2.78...v1.2.87) --- updated-dependencies: - dependency-name: painterro dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 08f9698836..8280d3cdcf 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "moment-timezone": "0.5.41", "npm-run-all2": "6.0.6", "nyc": "15.1.0", - "painterro": "1.2.78", + "painterro": "1.2.87", "plotly.js-basic-dist": "2.20.0", "plotly.js-gl2d-dist": "2.20.0", "prettier": "2.8.7", From 16ca994cfac99d0805b6d79d381c56daf61c6c9e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 29 Oct 2023 09:13:09 -0700 Subject: [PATCH 004/126] chore(deps-dev): bump sinon from 15.1.0 to 17.0.0 (#7155) Bumps [sinon](https://github.com/sinonjs/sinon) from 15.1.0 to 17.0.0. - [Release notes](https://github.com/sinonjs/sinon/releases) - [Changelog](https://github.com/sinonjs/sinon/blob/main/docs/changelog.md) - [Commits](https://github.com/sinonjs/sinon/compare/v15.1.0...v17.0.0) --- updated-dependencies: - dependency-name: sinon dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8280d3cdcf..9215eb0788 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "sanitize-html": "2.11.0", "sass": "1.68.0", "sass-loader": "13.3.2", - "sinon": "15.1.0", + "sinon": "17.0.0", "style-loader": "3.3.3", "tiny-emitter": "2.1.0", "typescript": "5.2.2", From c7fd584b58e164241f0cde40670ff99257c81da3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Oct 2023 23:05:33 +0000 Subject: [PATCH 005/126] chore(deps-dev): bump @braintree/sanitize-url from 6.0.2 to 6.0.4 (#7190) Bumps [@braintree/sanitize-url](https://github.com/braintree/sanitize-url) from 6.0.2 to 6.0.4. - [Changelog](https://github.com/braintree/sanitize-url/blob/main/CHANGELOG.md) - [Commits](https://github.com/braintree/sanitize-url/compare/v6.0.2...v6.0.4) --- updated-dependencies: - dependency-name: "@braintree/sanitize-url" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9215eb0788..c9075c8c94 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "The Open MCT core platform", "devDependencies": { "@babel/eslint-parser": "7.22.5", - "@braintree/sanitize-url": "6.0.2", + "@braintree/sanitize-url": "6.0.4", "@deploysentinel/playwright": "0.3.4", "@percy/cli": "1.26.0", "@percy/playwright": "1.0.4", From a0fd1f01712fdcd5d82b455d90ed49604f76f1f3 Mon Sep 17 00:00:00 2001 From: Michael Rogers Date: Tue, 31 Oct 2023 09:28:20 -0500 Subject: [PATCH 006/126] Removed errant brace in ObjectAPI Error (#7192) Removed errant brace --- src/api/objects/ObjectAPI.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/objects/ObjectAPI.js b/src/api/objects/ObjectAPI.js index ef21c567a1..6013a88361 100644 --- a/src/api/objects/ObjectAPI.js +++ b/src/api/objects/ObjectAPI.js @@ -221,7 +221,7 @@ export default class ObjectAPI { const provider = this.getProvider(identifier); if (!provider) { - throw new Error(`No Provider Matched for keyString "${this.makeKeyString(identifier)}}"`); + throw new Error(`No Provider Matched for keyString "${this.makeKeyString(identifier)}"`); } if (!provider.get) { From ae229205769af13fcc8ce39f1c58326b2afc4832 Mon Sep 17 00:00:00 2001 From: Shefali Joshi Date: Wed, 1 Nov 2023 08:47:43 -0700 Subject: [PATCH 007/126] Refine display options and add Independent Time Conductor option for Time List view (#7161) * Apply sort settings immediately - even when in edit mode. * Adds test for sort order * Enable independent time conductor for time list view * Remove time frame duration options. * Remove immediate sorting in edit mode. * Closes #7113 - Color of current events changed to bring more in-line with color conventions. - Changed Time List rgba colors to solids. - Removed bolding on current events text. * Fix tests to include new changes --------- Co-authored-by: Charles Hacskaylo --- src/plugins/timelist/TimelistComponent.vue | 125 +++++------------- .../timelist/inspector/EventProperties.vue | 28 +--- .../inspector/TimelistPropertiesView.vue | 10 +- src/plugins/timelist/pluginSpec.js | 54 +++++--- src/plugins/timelist/timelist.scss | 1 - src/styles/_constants-espresso.scss | 6 +- src/styles/_constants-maelstrom.scss | 8 +- src/styles/_constants-snow.scss | 8 +- src/ui/components/ObjectFrame.vue | 9 +- src/ui/layout/BrowseBar.vue | 8 +- src/utils/constants.js | 8 ++ 11 files changed, 99 insertions(+), 166 deletions(-) create mode 100644 src/utils/constants.js diff --git a/src/plugins/timelist/TimelistComponent.vue b/src/plugins/timelist/TimelistComponent.vue index 423e511dab..66a22be871 100644 --- a/src/plugins/timelist/TimelistComponent.vue +++ b/src/plugins/timelist/TimelistComponent.vue @@ -116,8 +116,10 @@ export default { }, mounted() { this.isEditing = this.openmct.editor.isEditing(); - this.timestamp = this.openmct.time.now(); - this.openmct.time.on(TIME_CONTEXT_EVENTS.modeChanged, this.setFixedTime); + this.updateTimestamp = _.throttle(this.updateTimestamp, 1000); + + this.setTimeContext(); + this.timestamp = this.timeContext.now(); this.getPlanDataAndSetConfig(this.domainObject); @@ -137,8 +139,6 @@ export default { ); this.status = this.openmct.status.get(this.domainObject.identifier); - this.updateTimestamp = _.throttle(this.updateTimestamp, 1000); - this.openmct.time.on('tick', this.updateTimestamp); this.openmct.editor.on('isEditing', this.setEditState); this.deferAutoScroll = _.debounce(this.deferAutoScroll, 500); @@ -150,7 +150,7 @@ export default { this.composition.load(); } - this.setFixedTime(this.openmct.time.getMode()); + this.setFixedTime(this.timeContext.getMode()); }, beforeUnmount() { if (this.unlisten) { @@ -166,8 +166,7 @@ export default { } this.openmct.editor.off('isEditing', this.setEditState); - this.openmct.time.off('tick', this.updateTimestamp); - this.openmct.time.off(TIME_CONTEXT_EVENTS.modeChanged, this.setFixedTime); + this.stopFollowingTimeContext(); this.$el.parentElement?.removeEventListener('scroll', this.deferAutoScroll, true); if (this.clearAutoScrollDisabledTimer) { @@ -180,6 +179,21 @@ export default { } }, methods: { + setTimeContext() { + this.stopFollowingTimeContext(); + this.timeContext = this.openmct.time.getContextForView(this.path); + this.followTimeContext(); + }, + followTimeContext() { + this.timeContext.on(TIME_CONTEXT_EVENTS.modeChanged, this.setFixedTime); + this.timeContext.on(TIME_CONTEXT_EVENTS.tick, this.updateTimestamp); + }, + stopFollowingTimeContext() { + if (this.timeContext) { + this.timeContext.off(TIME_CONTEXT_EVENTS.modeChanged, this.setFixedTime); + this.timeContext.off(TIME_CONTEXT_EVENTS.tick, this.updateTimestamp); + } + }, planFileUpdated(selectFile) { this.getPlanData({ selectFile, @@ -198,7 +212,6 @@ export default { } else { this.filterValue = configuration.filter; this.setSort(); - this.setViewBounds(); this.listActivities(); } }, @@ -208,7 +221,7 @@ export default { }, setFixedTime() { this.filterValue = this.domainObject.configuration.filter; - this.isFixedTime = !this.openmct.time.isRealTime(); + this.isFixedTime = !this.timeContext.isRealTime(); if (this.isFixedTime) { this.hideAll = false; } @@ -269,71 +282,6 @@ export default { getPlanData(domainObject) { this.planData = getValidatedData(domainObject); }, - setViewBounds() { - const pastEventsIndex = this.domainObject.configuration.pastEventsIndex; - const currentEventsIndex = this.domainObject.configuration.currentEventsIndex; - const futureEventsIndex = this.domainObject.configuration.futureEventsIndex; - const pastEventsDuration = this.domainObject.configuration.pastEventsDuration; - const pastEventsDurationIndex = this.domainObject.configuration.pastEventsDurationIndex; - const futureEventsDuration = this.domainObject.configuration.futureEventsDuration; - const futureEventsDurationIndex = this.domainObject.configuration.futureEventsDurationIndex; - - if (pastEventsIndex === 0 && futureEventsIndex === 0 && currentEventsIndex === 0) { - this.viewBounds = undefined; - this.hideAll = true; - - return; - } - - this.hideAll = false; - - if (pastEventsIndex === 1 && futureEventsIndex === 1 && currentEventsIndex === 1) { - this.viewBounds = undefined; - - return; - } - - this.viewBounds = {}; - - if (pastEventsIndex !== 1) { - const pastDurationInMS = this.getDurationInMilliSeconds( - pastEventsDuration, - pastEventsDurationIndex - ); - this.viewBounds.pastEnd = (timestamp) => { - if (pastEventsIndex === 2) { - return timestamp - pastDurationInMS; - } else if (pastEventsIndex === 0) { - return timestamp + 1; - } - }; - } - - if (futureEventsIndex !== 1) { - const futureDurationInMS = this.getDurationInMilliSeconds( - futureEventsDuration, - futureEventsDurationIndex - ); - this.viewBounds.futureStart = (timestamp) => { - if (futureEventsIndex === 2) { - return timestamp + futureDurationInMS; - } else if (futureEventsIndex === 0) { - return 0; - } - }; - } - }, - getDurationInMilliSeconds(duration, durationIndex) { - if (duration > 0) { - if (durationIndex === 0) { - return duration * 1000; - } else if (durationIndex === 1) { - return duration * 60 * 1000; - } else if (durationIndex === 2) { - return duration * 60 * 60 * 1000; - } - } - }, listActivities() { let groups = Object.keys(this.planData); let activities = []; @@ -356,18 +304,18 @@ export default { }, isActivityInBounds(activity) { const startInBounds = - activity.start >= this.openmct.time.bounds()?.start && - activity.start <= this.openmct.time.bounds()?.end; + activity.start >= this.timeContext.bounds()?.start && + activity.start <= this.timeContext.bounds()?.end; const endInBounds = - activity.end >= this.openmct.time.bounds()?.start && - activity.end <= this.openmct.time.bounds()?.end; + activity.end >= this.timeContext.bounds()?.start && + activity.end <= this.timeContext.bounds()?.end; const middleInBounds = - activity.start <= this.openmct.time.bounds()?.start && - activity.end >= this.openmct.time.bounds()?.end; + activity.start <= this.timeContext.bounds()?.start && + activity.end >= this.timeContext.bounds()?.end; return startInBounds || endInBounds || middleInBounds; }, - filterActivities(activity, index) { + filterActivities(activity) { if (this.isEditing) { return true; } @@ -381,15 +329,12 @@ export default { return false; } //current event or future start event or past end event - const isCurrent = this.timestamp >= activity.start && this.timestamp <= activity.end; - const isPast = - this.timestamp > activity.end && - (this.viewBounds?.pastEnd === undefined || - activity.end >= this.viewBounds?.pastEnd(this.timestamp)); - const isFuture = - this.timestamp < activity.start && - (this.viewBounds?.futureStart === undefined || - activity.start <= this.viewBounds?.futureStart(this.timestamp)); + const showCurrentEvents = this.domainObject.configuration.currentEventsIndex > 0; + + const isCurrent = + showCurrentEvents && this.timestamp >= activity.start && this.timestamp <= activity.end; + const isPast = this.timestamp > activity.end; + const isFuture = this.timestamp < activity.start; return isCurrent || isPast || isFuture; }, diff --git a/src/plugins/timelist/inspector/EventProperties.vue b/src/plugins/timelist/inspector/EventProperties.vue index 2d17073a17..09878566ed 100644 --- a/src/plugins/timelist/inspector/EventProperties.vue +++ b/src/plugins/timelist/inspector/EventProperties.vue @@ -32,34 +32,15 @@ {{ activityOption }} - -
{{ activitiesOptions[index] }} - {{ duration }} {{ durationOptions[durationIndex] }}
- From deacd91078beb14a40a214312b21daebe93112fc Mon Sep 17 00:00:00 2001 From: Scott Bell Date: Mon, 13 Nov 2023 19:27:50 +0100 Subject: [PATCH 031/126] Defer rendering for inactive tabs in open mct tabbed view (#7149) * simple prototype * add a few examples * revert to original * only check first element * only print when we're firing * need to return status * ignore polling logic if not visible * convert to es6 classes * add private variables * remove debug code * revert on this branch webgl changes * fix draw loader import * do not use v-model for search component * remove flakey unit tests and add e2e tests for same behavior * remove fdescribe * add test word * add simple functional test for tabs * add performance test for tabs * make tab selection more explict * better describe expects * lint * switch back to fixed time * fix perf test for webpacked version * lint * relax condition * relax condition * resolve PR comments * address PR review comments * typo on role vs locator --- .cspell.json | 1 + .../functional/plugins/tabs/tabs.e2e.spec.js | 74 +++++++++++++ .../telemetryTable/telemetryTable.e2e.spec.js | 81 ++++++++++++++ e2e/tests/performance/tabs.e2e.spec.js | 100 ++++++++++++++++++ src/api/nice/NicelyCalled.js | 88 +++++++++++++++ src/plugins/LADTable/components/LadRow.vue | 7 +- .../components/TelemetryView.vue | 31 +++--- .../gauge/components/GaugeComponent.vue | 14 ++- src/plugins/plot/chart/MctChart.vue | 9 +- src/plugins/tabs/components/TabsComponent.vue | 3 + .../components/TableComponent.vue | 44 ++++---- src/plugins/telemetryTable/pluginSpec.js | 27 ----- 12 files changed, 413 insertions(+), 66 deletions(-) create mode 100644 e2e/tests/functional/plugins/tabs/tabs.e2e.spec.js create mode 100644 e2e/tests/performance/tabs.e2e.spec.js create mode 100644 src/api/nice/NicelyCalled.js diff --git a/.cspell.json b/.cspell.json index 684868251d..f29d189016 100644 --- a/.cspell.json +++ b/.cspell.json @@ -488,6 +488,7 @@ "blockquotes", "Blockquote", "Blockquotes", + "oger", "lcovonly", "gcov" ], diff --git a/e2e/tests/functional/plugins/tabs/tabs.e2e.spec.js b/e2e/tests/functional/plugins/tabs/tabs.e2e.spec.js new file mode 100644 index 0000000000..79ff77c3c2 --- /dev/null +++ b/e2e/tests/functional/plugins/tabs/tabs.e2e.spec.js @@ -0,0 +1,74 @@ +/***************************************************************************** + * 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. + *****************************************************************************/ + +const { createDomainObjectWithDefaults } = require('../../../../appActions'); +const { test, expect } = require('../../../../pluginFixtures'); + +test.describe('Tabs View', () => { + test('Renders tabbed elements', async ({ page }) => { + await page.goto('./', { waitUntil: 'domcontentloaded' }); + + const tabsView = await createDomainObjectWithDefaults(page, { + type: 'Tabs View' + }); + const table = await createDomainObjectWithDefaults(page, { + type: 'Telemetry Table', + parent: tabsView.uuid + }); + await createDomainObjectWithDefaults(page, { + type: 'Event Message Generator', + parent: table.uuid + }); + const notebook = await createDomainObjectWithDefaults(page, { + type: 'Notebook', + parent: tabsView.uuid + }); + const sineWaveGenerator = await createDomainObjectWithDefaults(page, { + type: 'Sine Wave Generator', + parent: tabsView.uuid + }); + + page.goto(tabsView.url); + + // select first tab + await page.getByLabel(`${table.name} tab`).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(); + + // ensure notebook visible + await expect(page.locator('.c-notebook__drag-area')).toBeVisible(); + + // select third tab + await page.getByLabel(`${sineWaveGenerator.name} tab`).click(); + + // expect sine wave generator visible + expect(await page.locator('.c-plot').isVisible()).toBe(true); + + // now try to select the first tab again + await page.getByLabel(`${table.name} tab`).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 295e63dacd..fd3b25a593 100644 --- a/e2e/tests/functional/plugins/telemetryTable/telemetryTable.e2e.spec.js +++ b/e2e/tests/functional/plugins/telemetryTable/telemetryTable.e2e.spec.js @@ -78,4 +78,85 @@ test.describe('Telemetry Table', () => { const endBoundMilliseconds = Date.parse(endDate); expect(latestMilliseconds).toBeLessThanOrEqual(endBoundMilliseconds); }); + + test('Supports filtering telemetry by regular text search', async ({ page }) => { + await page.goto('./', { waitUntil: 'domcontentloaded' }); + + const table = await createDomainObjectWithDefaults(page, { type: 'Telemetry Table' }); + await createDomainObjectWithDefaults(page, { + type: 'Event Message Generator', + parent: table.uuid + }); + + // focus the Telemetry Table + await page.goto(table.url); + + 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(); + // ensure we've got more than one cell + expect(cells.length).toBeGreaterThan(1); + // ensure the text content of each cell contains the search term + for (const cell of cells) { + const text = await cell.textContent(); + expect(text).toContain('Roger'); + } + + 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(); + // ensure we've got more than one cell + expect(cells.length).toBe(0); + // ensure the text content of each cell contains the search term + for (const cell of cells) { + const text = await cell.textContent(); + expect(text).not.toContain('Dodger'); + } + + // Click pause button + await page.click('button[title="Pause"]'); + }); + + test('Supports filtering using Regex', async ({ page }) => { + await page.goto('./', { waitUntil: 'domcontentloaded' }); + + const table = await createDomainObjectWithDefaults(page, { type: 'Telemetry Table' }); + await createDomainObjectWithDefaults(page, { + type: 'Event Message Generator', + parent: table.uuid + }); + + // focus the Telemetry Table + page.goto(table.url); + await page.getByRole('searchbox', { name: 'message filter input' }).hover(); + await page.getByLabel('Message filter header').getByRole('button', { name: '/R/' }).click(); + 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(); + // ensure we've got more than one cell + expect(cells.length).toBeGreaterThan(1); + // ensure the text content of each cell contains the search term + for (const cell of cells) { + const text = await cell.textContent(); + expect(text).toContain('Roger'); + } + + 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(); + // ensure we've got more than one cell + expect(cells.length).toBe(0); + // ensure the text content of each cell contains the search term + for (const cell of cells) { + const text = await cell.textContent(); + expect(text).not.toContain('Dodger'); + } + + // Click pause button + await page.click('button[title="Pause"]'); + }); }); diff --git a/e2e/tests/performance/tabs.e2e.spec.js b/e2e/tests/performance/tabs.e2e.spec.js new file mode 100644 index 0000000000..833f2e3669 --- /dev/null +++ b/e2e/tests/performance/tabs.e2e.spec.js @@ -0,0 +1,100 @@ +/***************************************************************************** + * 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. + *****************************************************************************/ + +const { createDomainObjectWithDefaults, waitForPlotsToRender } = require('../../appActions'); +const { test, expect } = require('../../pluginFixtures'); + +test.describe('Tabs View', () => { + test('Renders tabbed elements nicely', async ({ page }) => { + // Code to hook into the requestAnimationFrame function and log each call + let animationCalls = []; + await page.exposeFunction('logCall', (callCount) => { + animationCalls.push(callCount); + }); + await page.addInitScript(() => { + const oldRequestAnimationFrame = window.requestAnimationFrame; + let callCount = 0; + window.requestAnimationFrame = function (callback) { + // eslint-disable-next-line no-undef + logCall(callCount++); + return oldRequestAnimationFrame(callback); + }; + }); + await page.goto('./', { waitUntil: 'domcontentloaded' }); + + const tabsView = await createDomainObjectWithDefaults(page, { + type: 'Tabs View' + }); + const table = await createDomainObjectWithDefaults(page, { + type: 'Telemetry Table', + parent: tabsView.uuid + }); + await createDomainObjectWithDefaults(page, { + type: 'Event Message Generator', + parent: table.uuid + }); + const notebook = await createDomainObjectWithDefaults(page, { + type: 'Notebook', + parent: tabsView.uuid + }); + const sineWaveGenerator = await createDomainObjectWithDefaults(page, { + type: 'Sine Wave Generator', + parent: tabsView.uuid + }); + + page.goto(tabsView.url); + + // select first tab + await page.getByLabel(`${table.name} tab`).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(); + + // expect notebook visible + await expect(page.locator('.c-notebook__drag-area')).toBeVisible(); + + // select third tab + await page.getByLabel(`${sineWaveGenerator.name} tab`).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(); + animationCalls = []; + // expect notebook visible + await expect(page.locator('.c-notebook__drag-area')).toBeVisible(); + const notebookAnimationCalls = animationCalls.length; + + // select sine wave generator and clear animation calls + animationCalls = []; + await page.getByLabel(`${sineWaveGenerator.name} tab`).click(); + + // ensure sine wave generator visible + await waitForPlotsToRender(page); + // we should be calling animation frames + const sineWaveAnimationCalls = animationCalls.length; + expect(sineWaveAnimationCalls).toBeGreaterThanOrEqual(notebookAnimationCalls); + }); +}); diff --git a/src/api/nice/NicelyCalled.js b/src/api/nice/NicelyCalled.js new file mode 100644 index 0000000000..d8e77363f0 --- /dev/null +++ b/src/api/nice/NicelyCalled.js @@ -0,0 +1,88 @@ +/***************************************************************************** + * 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. + *****************************************************************************/ + +/** + * Optimizes `requestAnimationFrame` calls to only execute when the element is visible in the viewport. + */ +export default class NicelyCalled { + #element; + #isIntersecting; + #observer; + #lastUnfiredFunc; + + /** + * Constructs a NicelyCalled instance to manage visibility-based requestAnimationFrame calls. + * + * @param {HTMLElement} element - The DOM element to observe for visibility changes. + * @throws {Error} If element is not provided. + */ + constructor(element) { + if (!element) { + throw new Error(`Nice visibility must be created with an element`); + } + this.#element = element; + this.#isIntersecting = true; + + this.#observer = new IntersectionObserver(this.#observerCallback); + this.#observer.observe(this.#element); + this.#lastUnfiredFunc = null; + } + + #observerCallback = ([entry]) => { + if (entry.target === this.#element) { + this.#isIntersecting = entry.isIntersecting; + if (this.#isIntersecting && this.#lastUnfiredFunc) { + window.requestAnimationFrame(this.#lastUnfiredFunc); + this.#lastUnfiredFunc = null; + } + } + }; + + /** + * Executes a function within requestAnimationFrame if the observed element is visible. + * If the element is not visible, the function is stored and called when the element becomes visible. + * Note that if called multiple times while not visible, only the last execution is stored and executed. + * + * @param {Function} func - The function to execute. + * @returns {boolean} True if the function was executed immediately, false otherwise. + */ + execute(func) { + if (this.#isIntersecting) { + window.requestAnimationFrame(func); + return true; + } else { + this.#lastUnfiredFunc = func; + return false; + } + } + + /** + * Stops observing the element for visibility changes and cleans up resources to prevent memory leaks. + */ + destroy() { + this.#observer.unobserve(this.#element); + this.#element = null; + this.#isIntersecting = null; + this.#observer = null; + this.#lastUnfiredFunc = null; + } +} diff --git a/src/plugins/LADTable/components/LadRow.vue b/src/plugins/LADTable/components/LadRow.vue index 6f9e57b7a0..ffc2eb2284 100644 --- a/src/plugins/LADTable/components/LadRow.vue +++ b/src/plugins/LADTable/components/LadRow.vue @@ -22,6 +22,7 @@