diff --git a/.circleci/config.yml b/.circleci/config.yml index 5cd72f893a..3998a1c918 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -5,11 +5,11 @@ orbs: executors: pw-focal-development: docker: - - image: mcr.microsoft.com/playwright:v1.45.2-focal + - image: mcr.microsoft.com/playwright:v1.47.2-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) PERCY_PARALLEL_TOTAL: 2 ubuntu: machine: @@ -17,7 +17,7 @@ executors: docker_layer_caching: true commands: build_and_install: - description: "All steps used to build and install." + description: 'All steps used to build and install.' parameters: node-version: type: string @@ -27,7 +27,7 @@ commands: node-version: << parameters.node-version >> - node/install-packages 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) @@ -61,7 +61,7 @@ commands: [[ $EUID -ne 0 ]] && sudo chmod +x codecov || chmod +x codecov ./codecov --help 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 @@ -135,13 +135,13 @@ jobs: suite: #ci or full type: string executor: pw-focal-development - parallelism: 7 + parallelism: 8 steps: - build_and_install: 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: @@ -198,7 +198,7 @@ jobs: steps: - build_and_install: node-version: lts/hydrogen - - run: npx playwright@1.45.2 install #Necessary for bare ubuntu machine + - run: npx playwright@1.47.2 install #Necessary for bare ubuntu machine - run: | export $(cat src/plugins/persistence/couch/.env.ci | xargs) docker compose -f src/plugins/persistence/couch/couchdb-compose.yaml up --detach @@ -323,7 +323,7 @@ workflows: - e2e-couchdb triggers: - schedule: - cron: "0 0 * * *" + cron: '0 0 * * *' filters: branches: only: diff --git a/.github/workflows/e2e-couchdb.yml b/.github/workflows/e2e-couchdb.yml index 1a8800324c..2003662947 100644 --- a/.github/workflows/e2e-couchdb.yml +++ b/.github/workflows/e2e-couchdb.yml @@ -37,7 +37,7 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - run: npx playwright@1.45.2 install + - run: npx playwright@1.47.2 install - name: Start CouchDB Docker Container and Init with Setup Scripts run: | diff --git a/.github/workflows/e2e-flakefinder.yml b/.github/workflows/e2e-flakefinder.yml index c7eccf222d..c06edea653 100644 --- a/.github/workflows/e2e-flakefinder.yml +++ b/.github/workflows/e2e-flakefinder.yml @@ -30,7 +30,7 @@ jobs: restore-keys: | ${{ runner.os }}-node- - - run: npx playwright@1.45.2 install + - run: npx playwright@1.47.2 install - run: npm ci --no-audit --progress=false - name: Run E2E Tests (Repeated 10 Times) diff --git a/.github/workflows/e2e-perf.yml b/.github/workflows/e2e-perf.yml index 27bfad23cb..2553336f11 100644 --- a/.github/workflows/e2e-perf.yml +++ b/.github/workflows/e2e-perf.yml @@ -28,7 +28,7 @@ jobs: restore-keys: | ${{ runner.os }}-node- - - run: npx playwright@1.45.2 install + - run: npx playwright@1.47.2 install - run: npm ci --no-audit --progress=false - run: npm run test:perf:localhost - run: npm run test:perf:contract diff --git a/.github/workflows/e2e-pr.yml b/.github/workflows/e2e-pr.yml index 9c0724f645..869fd2dfbf 100644 --- a/.github/workflows/e2e-pr.yml +++ b/.github/workflows/e2e-pr.yml @@ -33,7 +33,7 @@ jobs: restore-keys: | ${{ runner.os }}-node- - - run: npx playwright@1.45.2 install + - run: npx playwright@1.47.2 install - run: npx playwright install chrome-beta - run: npm ci --no-audit --progress=false - run: npm run test:e2e:full -- --max-failures=40 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..045dfedb18 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,116 @@ +# 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 }} \ No newline at end of file diff --git a/e2e/appActions.js b/e2e/appActions.js index f1602bc3cd..6cd901fb4d 100644 --- a/e2e/appActions.js +++ b/e2e/appActions.js @@ -227,6 +227,37 @@ async function createExampleTelemetryObject(page, parent = 'mine') { }; } +/** + * Create a Stable State Telemetry Object (State Generator) for use in visual tests + * and tests against plotting telemetry (e.g. logPlot tests). This will change state every 2 seconds. + * @param {import('@playwright/test').Page} page + * @param {string | import('../src/api/objects/ObjectAPI').Identifier} [parent] the uuid or identifier of the parent object. Defaults to 'mine' + * @returns {Promise} An object containing information about the telemetry object. + */ +async function createStableStateTelemetry(page, parent = 'mine') { + const parentUrl = await getHashUrlToDomainObject(page, parent); + + await page.goto(`${parentUrl}`); + const createdObject = await createDomainObjectWithDefaults(page, { + type: 'State Generator', + name: 'Stable State Generator' + }); + // edit the state generator to have a 1 second update rate + await page.getByLabel('More actions').click(); + await page.getByRole('menuitem', { name: 'Edit Properties...' }).click(); + await page.getByLabel('State Duration (seconds)', { exact: true }).fill('2'); + await page.getByLabel('Save').click(); + // Wait until the URL is updated + const uuid = await getFocusedObjectUuid(page); + const url = await getHashUrlToDomainObject(page, uuid); + + return { + name: createdObject.name, + uuid, + url + }; +} + /** * Navigates directly to a given object url, in fixed time mode, with the given start and end bounds. Note: does not set * default view type. @@ -629,13 +660,33 @@ async function getCanvasPixels(page, canvasSelector) { ); } +/** + * Search for telemetry and link it to an object. objectName should come from the domainObject.name function. + * @param {import('@playwright/test').Page} page + * @param {string} parameterName + * @param {string} objectName + */ +async function linkParameterToObject(page, parameterName, objectName) { + await page.getByRole('searchbox', { name: 'Search Input' }).click(); + await page.getByRole('searchbox', { name: 'Search Input' }).fill(parameterName); + await page.getByLabel('Object Results').getByText(parameterName).click(); + await page.getByLabel('More actions').click(); + await page.getByLabel('Create Link').click(); + await page.getByLabel('Modal Overlay').getByLabel('Search Input').click(); + await page.getByLabel('Modal Overlay').getByLabel('Search Input').fill(objectName); + await page.getByLabel('Modal Overlay').getByLabel(`Navigate to ${objectName}`).click(); + await page.getByLabel('Save').click(); +} + export { createDomainObjectWithDefaults, createExampleTelemetryObject, createNotification, createPlanFromJSON, + createStableStateTelemetry, expandEntireTree, getCanvasPixels, + linkParameterToObject, navigateToObjectWithFixedTimeBounds, navigateToObjectWithRealTime, setEndOffset, diff --git a/e2e/package.json b/e2e/package.json index 1e55aaa7e5..61000a86d0 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -16,7 +16,7 @@ "devDependencies": { "@percy/cli": "1.27.4", "@percy/playwright": "1.0.4", - "@playwright/test": "1.45.2", + "@playwright/test": "1.47.2", "@axe-core/playwright": "4.8.5" }, "author": { @@ -24,4 +24,4 @@ "url": "https://www.nasa.gov" }, "license": "Apache-2.0" -} +} \ No newline at end of file diff --git a/e2e/tests/framework/appActions.e2e.spec.js b/e2e/tests/framework/appActions.e2e.spec.js index 280ef33f17..9e044d3d07 100644 --- a/e2e/tests/framework/appActions.e2e.spec.js +++ b/e2e/tests/framework/appActions.e2e.spec.js @@ -26,8 +26,10 @@ import { createExampleTelemetryObject, createNotification, createPlanFromJSON, + createStableStateTelemetry, expandEntireTree, getCanvasPixels, + linkParameterToObject, navigateToObjectWithFixedTimeBounds, navigateToObjectWithRealTime, setEndOffset, @@ -339,4 +341,23 @@ test.describe('AppActions @framework', () => { // Expect this step to fail await waitForPlotsToRender(page, { timeout: 1000 }); }); + test('createStableStateTelemetry', async ({ page }) => { + const stableStateTelemetry = await createStableStateTelemetry(page); + expect(stableStateTelemetry.name).toBe('Stable State Generator'); + expect(stableStateTelemetry.url).toBe(`./#/browse/mine/${stableStateTelemetry.uuid}`); + expect(stableStateTelemetry.uuid).toBeDefined(); + }); + test('linkParameterToObject', async ({ page }) => { + const displayLayout = await createDomainObjectWithDefaults(page, { + type: 'Display Layout', + name: 'Test Display Layout' + }); + const exampleTelemetry = await createExampleTelemetryObject(page); + + await linkParameterToObject(page, exampleTelemetry.name, displayLayout.name); + await page.goto(displayLayout.url); + await expect(page.getByRole('main').getByText('Test Display Layout')).toBeVisible(); + await expandEntireTree(page); + await expect(page.getByLabel('Navigate to VIPER Rover').first()).toBeVisible(); + }); }); diff --git a/e2e/tests/functional/plugins/performanceIndicator/performanceIndicator.e2e.spec.js b/e2e/tests/functional/plugins/performanceIndicator/performanceIndicator.e2e.spec.js index ef1709fb99..102dbd81de 100644 --- a/e2e/tests/functional/plugins/performanceIndicator/performanceIndicator.e2e.spec.js +++ b/e2e/tests/functional/plugins/performanceIndicator/performanceIndicator.e2e.spec.js @@ -1,5 +1,5 @@ /***************************************************************************** - * Open MCT, Copyright (c) 2014-2023, United States Government + * Open MCT, Copyright (c) 2014-2024, United States Government * as represented by the Administrator of the National Aeronautics and Space * Administration. All rights reserved. * diff --git a/e2e/tests/functional/plugins/reloadAction/reloadAction.e2e.spec.js b/e2e/tests/functional/plugins/reloadAction/reloadAction.e2e.spec.js index 499454bb2a..345283f4d6 100644 --- a/e2e/tests/functional/plugins/reloadAction/reloadAction.e2e.spec.js +++ b/e2e/tests/functional/plugins/reloadAction/reloadAction.e2e.spec.js @@ -1,5 +1,5 @@ /***************************************************************************** - * Open MCT, Copyright (c) 2014-2023, United States Government + * Open MCT, Copyright (c) 2014-2024, United States Government * as represented by the Administrator of the National Aeronautics and Space * Administration. All rights reserved. * diff --git a/e2e/tests/functional/plugins/styling/conditionSetStyling.e2e.spec.js b/e2e/tests/functional/plugins/styling/conditionSetStyling.e2e.spec.js new file mode 100644 index 0000000000..bfb6f4ba63 --- /dev/null +++ b/e2e/tests/functional/plugins/styling/conditionSetStyling.e2e.spec.js @@ -0,0 +1,163 @@ +/***************************************************************************** + * 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 is dedicated to tests which verify the basic operations surrounding conditionSets and styling +*/ + +import { + createDomainObjectWithDefaults, + linkParameterToObject, + setRealTimeMode +} from '../../../../appActions.js'; +import { MISSION_TIME } from '../../../../constants.js'; +import { expect, test } from '../../../../pluginFixtures.js'; + +test.describe('Conditionally Styling, using a Condition Set', () => { + let stateGenerator; + let conditionSet; + let displayLayout; + const STATE_CHANGE_INTERVAL = '1'; + + test.beforeEach(async ({ page }) => { + // Install the clock and set the time to the mission time such that the state generator will be controllable + await page.clock.install({ time: MISSION_TIME }); + await page.clock.resume(); + await page.goto('./', { waitUntil: 'domcontentloaded' }); + // Create Condition Set, State Generator, and Display Layout + conditionSet = await createDomainObjectWithDefaults(page, { + type: 'Condition Set', + name: 'Test Condition Set' + }); + stateGenerator = await createDomainObjectWithDefaults(page, { + type: 'State Generator', + name: 'One Second State Generator' + }); + // edit the state generator to have a 1 second update rate + await page.getByTitle('More actions').click(); + await page.getByRole('menuitem', { name: 'Edit Properties...' }).click(); + await page.getByLabel('State Duration (seconds)', { exact: true }).fill(STATE_CHANGE_INTERVAL); + await page.getByLabel('Save').click(); + + displayLayout = await createDomainObjectWithDefaults(page, { + type: 'Display Layout', + name: 'Test Display Layout' + }); + }); + + test('Conditional styling, using a Condition Set, will style correctly based on the output @clock', async ({ + page + }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/nasa/openmct/issues/7840' + }); + + // set up the condition set to use the state generator + await page.goto(conditionSet.url, { waitUntil: 'domcontentloaded' }); + + // Add the State Generator to the Condition Set by dragging from the main tree + await page.getByLabel('Show selected item in tree').click(); + await page + .getByRole('tree', { + name: 'Main Tree' + }) + .getByRole('treeitem', { + name: stateGenerator.name + }) + .dragTo(page.locator('#conditionCollection')); + + // Add the state generator to the first criterion such that there is a condition named 'OFF' when the state generator is off + await page.getByLabel('Add Condition').click(); + await page + .getByLabel('Criterion Telemetry Selection') + .selectOption({ label: stateGenerator.name }); + await page.getByLabel('Criterion Metadata Selection').selectOption({ label: 'State' }); + await page.getByLabel('Criterion Comparison Selection').selectOption({ label: 'is' }); + await page.getByLabel('Condition Name Input').first().fill('OFF'); + await page.getByLabel('Save').click(); + await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); + + await linkParameterToObject(page, stateGenerator.name, displayLayout.name); + + //Add a box to the display layout + await page.goto(displayLayout.url, { waitUntil: 'domcontentloaded' }); + await page.getByLabel('Edit Object').click(); + + //Add a box to the display layout and move it to the right + //TEMP: Click the layout such that the state generator is deselected + await page.getByLabel('Test Display Layout Layout Grid').locator('div').nth(1).click(); + await page.getByLabel('Add Drawing Object').click(); + await page.getByText('Box').click(); + await page.getByLabel('X:').click(); + await page.getByLabel('X:').fill('10'); + await page.getByLabel('X:').press('Enter'); + + // set up conditional styling such that the box is red when the state generator condition is 'OFF' + await page.getByRole('tab', { name: 'Styles' }).click(); + await page.getByRole('button', { name: 'Use Conditional Styling...' }).click(); + await page.getByLabel('Modal Overlay').getByLabel('Expand My Items folder').click(); + await page.getByLabel('Modal Overlay').getByLabel(`Preview ${conditionSet.name}`).click(); + await page.getByText('Ok').click(); + await page.getByLabel('Set background color').first().click(); + await page.getByLabel('#ff0000').click(); + await page.getByLabel('Save', { exact: true }).click(); + await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); + + await setRealTimeMode(page); + + //Pause at a time when the state generator is 'OFF' which is 20 minutes in the future + await page.clock.pauseAt(new Date(MISSION_TIME + 1200000)); + + const redBG = 'background-color: rgb(255, 0, 0);'; + const defaultBG = 'background-color: rgb(102, 102, 102);'; + const textElement = page.getByLabel('Alpha-numeric telemetry value').locator('div:first-child'); + const styledElement = page.getByLabel('Box', { exact: true }); + + await page.clock.resume(); + + // Check if the style is red when text is 'OFF' + await expect(textElement).toHaveText('OFF'); + await waitForStyleChange(styledElement, redBG); + + // Fast forward to the next state change + await page.clock.fastForward(STATE_CHANGE_INTERVAL * 1000); + + // Check if the style is not red when text is 'ON' + await expect(textElement).toHaveText('ON'); + await waitForStyleChange(styledElement, defaultBG); + }); +}); + +/** + * Wait for the style of an element to change to the expected style. + * @param {import('@playwright/test').Locator} element - The element to check. + * @param {string} expectedStyle - The expected style to wait for. + * @param {number} timeout - The timeout in milliseconds. + */ +async function waitForStyleChange(element, expectedStyle, timeout = 0) { + await expect(async () => { + const style = await element.getAttribute('style'); + + // eslint-disable-next-line playwright/prefer-web-first-assertions + expect(style).toBe(expectedStyle); + }).toPass({ timeout: 1000 }); // timeout allows for the style to be applied +} diff --git a/e2e/tests/functional/staleness.e2e.spec.js b/e2e/tests/functional/staleness.e2e.spec.js index d247674cae..d1a9bfd201 100644 --- a/e2e/tests/functional/staleness.e2e.spec.js +++ b/e2e/tests/functional/staleness.e2e.spec.js @@ -1,5 +1,5 @@ /***************************************************************************** - * Open MCT, Copyright (c) 2014-2023, United States Government + * Open MCT, Copyright (c) 2014-2024, United States Government * as represented by the Administrator of the National Aeronautics and Space * Administration. All rights reserved. * diff --git a/e2e/tests/functional/userRoles.e2e.spec.js b/e2e/tests/functional/userRoles.e2e.spec.js index 455b096aab..40fdca548b 100644 --- a/e2e/tests/functional/userRoles.e2e.spec.js +++ b/e2e/tests/functional/userRoles.e2e.spec.js @@ -1,5 +1,5 @@ /***************************************************************************** - * Open MCT, Copyright (c) 2014-2023, United States Government + * Open MCT, Copyright (c) 2014-2024, United States Government * as represented by the Administrator of the National Aeronautics and Space * Administration. All rights reserved. * diff --git a/e2e/tests/visual-a11y/displayLayout.visual.spec.js b/e2e/tests/visual-a11y/displayLayout.visual.spec.js index 401c5b243c..462b778fad 100644 --- a/e2e/tests/visual-a11y/displayLayout.visual.spec.js +++ b/e2e/tests/visual-a11y/displayLayout.visual.spec.js @@ -22,7 +22,11 @@ import percySnapshot from '@percy/playwright'; -import { createDomainObjectWithDefaults } from '../../appActions.js'; +import { + createDomainObjectWithDefaults, + createStableStateTelemetry, + linkParameterToObject +} from '../../appActions.js'; import { MISSION_TIME, VISUAL_FIXED_URL } from '../../constants.js'; import { test } from '../../pluginFixtures.js'; @@ -47,16 +51,13 @@ test.describe('Visual - Display Layout @clock', () => { name: 'Child Right Layout', parent: parentLayout.uuid }); - await createDomainObjectWithDefaults(page, { - type: 'Sine Wave Generator', - name: 'SWG 1', - parent: child1Layout.uuid - }); - await createDomainObjectWithDefaults(page, { - type: 'Sine Wave Generator', - name: 'SWG 2', - parent: child2Layout.uuid - }); + + const stableStateTelemetry = await createStableStateTelemetry(page); + await linkParameterToObject(page, stableStateTelemetry.name, child1Layout.name); + await linkParameterToObject(page, stableStateTelemetry.name, child2Layout.name); + + // Pause the clock at a time where the telemetry is stable 20 minutes in the future + await page.clock.pauseAt(new Date(MISSION_TIME + 1200000)); await page.goto(parentLayout.url, { waitUntil: 'domcontentloaded' }); await page.getByRole('button', { name: 'Edit Object' }).click(); diff --git a/karma.conf.cjs b/karma.conf.cjs index 1fc2256bfe..e96836acbb 100644 --- a/karma.conf.cjs +++ b/karma.conf.cjs @@ -1,5 +1,5 @@ /***************************************************************************** - * Open MCT, Copyright (c) 2014-2023, United States Government + * Open MCT, Copyright (c) 2014-2024, United States Government * as represented by the Administrator of the National Aeronautics and Space * Administration. All rights reserved. * diff --git a/package-lock.json b/package-lock.json index 8511238236..b97206f69d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -104,7 +104,7 @@ "@axe-core/playwright": "4.8.5", "@percy/cli": "1.27.4", "@percy/playwright": "1.0.4", - "@playwright/test": "1.45.2" + "@playwright/test": "1.47.2" } }, "e2e/node_modules/@percy/cli": { @@ -1560,12 +1560,13 @@ } }, "node_modules/@playwright/test": { - "version": "1.45.2", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.45.2.tgz", - "integrity": "sha512-JxG9eq92ET75EbVi3s+4sYbcG7q72ECeZNbdBlaMkGcNbiDQ4cAi8U2QP5oKkOx+1gpaiL1LDStmzCaEM1Z6fQ==", + "version": "1.47.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.47.2.tgz", + "integrity": "sha512-jTXRsoSPONAs8Za9QEQdyjFn+0ZQFjCiIztAIF6bi1HqhBzG9Ma7g1WotyiGqFSBRZjIEqMdT8RUlbk1QVhzCQ==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "playwright": "1.45.2" + "playwright": "1.47.2" }, "bin": { "playwright": "cli.js" @@ -8741,12 +8742,13 @@ } }, "node_modules/playwright": { - "version": "1.45.2", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.45.2.tgz", - "integrity": "sha512-ReywF2t/0teRvNBpfIgh5e4wnrI/8Su8ssdo5XsQKpjxJj+jspm00jSoz9BTg91TT0c9HRjXO7LBNVrgYj9X0g==", + "version": "1.47.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.47.2.tgz", + "integrity": "sha512-nx1cLMmQWqmA3UsnjaaokyoUpdVaaDhJhMoxX2qj3McpjnsqFHs516QAKYhqHAgOP+oCFTEOCOAaD1RgD/RQfA==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.45.2" + "playwright-core": "1.47.2" }, "bin": { "playwright": "cli.js" @@ -8759,10 +8761,11 @@ } }, "node_modules/playwright-core": { - "version": "1.45.2", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.45.2.tgz", - "integrity": "sha512-ha175tAWb0dTK0X4orvBIqi3jGEt701SMxMhyujxNrgd8K0Uy5wMSwwcQHtyB4om7INUkfndx02XnQ2p6dvLDw==", + "version": "1.47.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.47.2.tgz", + "integrity": "sha512-3JvMfF+9LJfe16l7AbSmU555PaTl2tPyQsVInqm3id16pdDfvZ8TTZ/pyzmkbDrZTQefyzU7AIHlZqQnxpqHVQ==", "dev": true, + "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" }, @@ -8776,6 +8779,7 @@ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "dev": true, "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" diff --git a/package.json b/package.json index 32740dc960..e5280188c4 100644 --- a/package.json +++ b/package.json @@ -128,7 +128,7 @@ "test:perf:contract": "npm test --workspace e2e -- --config=playwright-performance-dev.config.js", "test:perf:localhost": "npm test --workspace e2e -- --config=playwright-performance-prod.config.js --project=chrome", "test:perf:memory": "npm test --workspace e2e -- --config=playwright-performance-prod.config.js --project=chrome-memory", - "update-about-dialog-copyright": "perl -pi -e 's/20\\d\\d\\-202\\d/2014\\-2023/gm' ./src/ui/layout/AboutDialog.vue", + "update-about-dialog-copyright": "perl -pi -e 's/20\\d\\d\\-202\\d/2014\\-2024/gm' ./src/ui/layout/AboutDialog.vue", "update-copyright-date": "npm run update-about-dialog-copyright && grep -lr --null --include=*.{js,scss,vue,ts,sh,html,md,frag} 'Copyright (c) 20' . | xargs -r0 perl -pi -e 's/Copyright\\s\\(c\\)\\s20\\d\\d\\-20\\d\\d/Copyright \\(c\\)\\ 2014\\-2024/gm'", "cov:e2e:report": "nyc report --reporter=lcovonly --report-dir=./coverage/e2e", "prepare": "npm run build:prod && npx tsc" diff --git a/src/api/telemetry/TelemetryAPI.js b/src/api/telemetry/TelemetryAPI.js index 5e963e0f58..086337d048 100644 --- a/src/api/telemetry/TelemetryAPI.js +++ b/src/api/telemetry/TelemetryAPI.js @@ -231,26 +231,20 @@ export default class TelemetryAPI { * @returns {TelemetryRequestOptions} the options, with defaults filled in */ standardizeRequestOptions(options = {}) { - if (!Object.hasOwn(options, 'start')) { - const bounds = options.timeContext?.getBounds(); - if (bounds?.start) { - options.start = options.timeContext.getBounds().start; - } else { - options.start = this.openmct.time.getBounds().start; - } - } - - if (!Object.hasOwn(options, 'end')) { - const bounds = options.timeContext?.getBounds(); - if (bounds?.end) { - options.end = options.timeContext.getBounds().end; - } else { - options.end = this.openmct.time.getBounds().end; - } + if (!Object.hasOwn(options, 'timeContext')) { + options.timeContext = this.openmct.time; } if (!Object.hasOwn(options, 'domain')) { - options.domain = this.openmct.time.getTimeSystem().key; + options.domain = options.timeContext.getTimeSystem().key; + } + + if (!Object.hasOwn(options, 'start')) { + options.start = options.timeContext.getBounds().start; + } + + if (!Object.hasOwn(options, 'end')) { + options.end = options.timeContext.getBounds().end; } return options; diff --git a/src/api/telemetry/TelemetryAPISpec.js b/src/api/telemetry/TelemetryAPISpec.js index 03ea417501..648b1172ca 100644 --- a/src/api/telemetry/TelemetryAPISpec.js +++ b/src/api/telemetry/TelemetryAPISpec.js @@ -269,36 +269,40 @@ describe('Telemetry API', () => { await telemetryAPI.request(domainObject); const { signal } = new AbortController(); - expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(jasmine.any(Object), { + expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(domainObject, { signal, start: 0, end: 1, - domain: 'system' + domain: 'system', + timeContext: openmct.time }); - expect(telemetryProvider.request).toHaveBeenCalledWith(jasmine.any(Object), { + expect(telemetryProvider.request).toHaveBeenCalledWith(domainObject, { signal, start: 0, end: 1, - domain: 'system' + domain: 'system', + timeContext: openmct.time }); telemetryProvider.supportsRequest.calls.reset(); telemetryProvider.request.calls.reset(); await telemetryAPI.request(domainObject, {}); - expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(jasmine.any(Object), { + expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(domainObject, { signal, start: 0, end: 1, - domain: 'system' + domain: 'system', + timeContext: openmct.time }); - expect(telemetryProvider.request).toHaveBeenCalledWith(jasmine.any(Object), { + expect(telemetryProvider.request).toHaveBeenCalledWith(domainObject, { signal, start: 0, end: 1, - domain: 'system' + domain: 'system', + timeContext: openmct.time }); }); @@ -313,18 +317,20 @@ describe('Telemetry API', () => { domain: 'someDomain' }); const { signal } = new AbortController(); - expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(jasmine.any(Object), { + expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(domainObject, { start: 20, end: 30, domain: 'someDomain', - signal + signal, + timeContext: openmct.time }); - expect(telemetryProvider.request).toHaveBeenCalledWith(jasmine.any(Object), { + expect(telemetryProvider.request).toHaveBeenCalledWith(domainObject, { start: 20, end: 30, domain: 'someDomain', - signal + signal, + timeContext: openmct.time }); }); describe('telemetry batching support', () => { diff --git a/src/api/telemetry/TelemetryCollection.js b/src/api/telemetry/TelemetryCollection.js index d3999753c8..ed1463933e 100644 --- a/src/api/telemetry/TelemetryCollection.js +++ b/src/api/telemetry/TelemetryCollection.js @@ -62,9 +62,6 @@ export default class TelemetryCollection extends EventEmitter { this.futureBuffer = []; this.parseTime = undefined; this.metadata = this.openmct.telemetry.getMetadata(domainObject); - if (!Object.hasOwn(options, 'timeContext')) { - options.timeContext = this.openmct.time; - } this.options = options; this.unsubscribe = undefined; this.pageState = undefined; @@ -84,6 +81,9 @@ export default class TelemetryCollection extends EventEmitter { this._error(LOADED_ERROR); } + if (!Object.hasOwn(this.options, 'timeContext')) { + this.options.timeContext = this.openmct.time; + } this._setTimeSystem(this.options.timeContext.getTimeSystem()); this.lastBounds = this.options.timeContext.getBounds(); // prioritize passed options over time bounds @@ -137,7 +137,7 @@ export default class TelemetryCollection extends EventEmitter { * @private */ async _requestHistoricalTelemetry() { - let options = this.openmct.telemetry.standardizeRequestOptions({ ...this.options }); + const options = this.openmct.telemetry.standardizeRequestOptions({ ...this.options }); const historicalProvider = this.openmct.telemetry.findRequestProvider( this.domainObject, options diff --git a/src/plugins/condition/ConditionManager.js b/src/plugins/condition/ConditionManager.js index 838730883d..4a18140cf7 100644 --- a/src/plugins/condition/ConditionManager.js +++ b/src/plugins/condition/ConditionManager.js @@ -39,7 +39,7 @@ export default class ConditionManager extends EventEmitter { this.shouldEvaluateNewTelemetry = this.shouldEvaluateNewTelemetry.bind(this); this.compositionLoad = this.composition.load(); - this.subscriptions = {}; + this.telemetryCollections = {}; this.telemetryObjects = {}; this.testData = { conditionTestInputs: this.conditionSetDomainObject.configuration.conditionTestData, @@ -48,55 +48,46 @@ export default class ConditionManager extends EventEmitter { this.initialize(); } - async requestLatestValue(endpoint) { - const options = { + subscribeToTelemetry(telemetryObject) { + const keyString = this.openmct.objects.makeKeyString(telemetryObject.identifier); + + if (this.telemetryCollections[keyString]) { + return; + } + + const requestOptions = { size: 1, strategy: 'latest' }; - const latestData = await this.openmct.telemetry.request(endpoint, options); - if (!latestData) { - throw new Error('Telemetry request failed by returning a falsy response'); - } - if (latestData.length === 0) { - return; - } - this.telemetryReceived(endpoint, latestData[0]); - } - - subscribeToTelemetry(endpoint) { - const telemetryKeyString = this.openmct.objects.makeKeyString(endpoint.identifier); - if (this.subscriptions[telemetryKeyString]) { - return; - } - - const metadata = this.openmct.telemetry.getMetadata(endpoint); - - this.telemetryObjects[telemetryKeyString] = Object.assign({}, endpoint, { - telemetryMetaData: metadata ? metadata.valueMetadatas : [] - }); - - // get latest telemetry value (in case subscription is cached and no new data is coming in) - this.requestLatestValue(endpoint); - - this.subscriptions[telemetryKeyString] = this.openmct.telemetry.subscribe( - endpoint, - this.telemetryReceived.bind(this, endpoint) + this.telemetryCollections[keyString] = this.openmct.telemetry.requestCollection( + telemetryObject, + requestOptions ); + + const metadata = this.openmct.telemetry.getMetadata(telemetryObject); + const telemetryMetaData = metadata ? metadata.valueMetadatas : []; + + this.telemetryObjects[keyString] = { ...telemetryObject, telemetryMetaData }; + + this.telemetryCollections[keyString].on( + 'add', + this.telemetryReceived.bind(this, telemetryObject) + ); + this.telemetryCollections[keyString].load(); + this.updateConditionTelemetryObjects(); } unsubscribeFromTelemetry(endpointIdentifier) { - const id = this.openmct.objects.makeKeyString(endpointIdentifier); - if (!this.subscriptions[id]) { - console.log('no subscription to remove'); - + const keyString = this.openmct.objects.makeKeyString(endpointIdentifier); + if (!this.telemetryCollections[keyString]) { return; } - this.subscriptions[id](); - delete this.subscriptions[id]; - delete this.telemetryObjects[id]; + this.telemetryCollections[keyString].destroy(); + this.telemetryCollections[keyString] = null; + this.telemetryObjects[keyString] = null; this.removeConditionTelemetryObjects(); //force re-computation of condition set result as we might be in a state where @@ -107,7 +98,7 @@ export default class ConditionManager extends EventEmitter { this.timeSystems, this.openmct.time.getTimeSystem() ); - this.updateConditionResults({ id: id }); + this.updateConditionResults({ id: keyString }); this.updateCurrentCondition(latestTimestamp); if (Object.keys(this.telemetryObjects).length === 0) { @@ -410,11 +401,13 @@ export default class ConditionManager extends EventEmitter { return this.openmct.time.getBounds().end >= currentTimestamp; } - telemetryReceived(endpoint, datum) { + telemetryReceived(endpoint, data) { if (!this.isTelemetryUsed(endpoint)) { return; } + const datum = data[0]; + const normalizedDatum = this.createNormalizedDatum(datum, endpoint); const timeSystemKey = this.openmct.time.getTimeSystem().key; let timestamp = {}; @@ -507,8 +500,9 @@ export default class ConditionManager extends EventEmitter { destroy() { this.composition.off('add', this.subscribeToTelemetry, this); this.composition.off('remove', this.unsubscribeFromTelemetry, this); - Object.values(this.subscriptions).forEach((unsubscribe) => unsubscribe()); - delete this.subscriptions; + Object.values(this.telemetryCollections).forEach((telemetryCollection) => + telemetryCollection.destroy() + ); this.conditions.forEach((condition) => { condition.destroy(); diff --git a/src/plugins/condition/pluginSpec.js b/src/plugins/condition/pluginSpec.js index d995d235da..233ee9ad06 100644 --- a/src/plugins/condition/pluginSpec.js +++ b/src/plugins/condition/pluginSpec.js @@ -720,50 +720,69 @@ describe('the plugin', function () { }; }); - it('should evaluate as old when telemetry is not received in the allotted time', (done) => { + it('should evaluate as old when telemetry is not received in the allotted time', async () => { + let onAddResolve; + const onAddCalledPromise = new Promise((resolve) => { + onAddResolve = resolve; + }); + const mockTelemetryCollection = { + load: jasmine.createSpy('load'), + on: jasmine.createSpy('on').and.callFake((event, callback) => { + if (event === 'add') { + onAddResolve(); + } + }) + }; + openmct.telemetry = jasmine.createSpyObj('telemetry', [ - 'subscribe', 'getMetadata', 'request', 'getValueFormatter', - 'abortAllRequests' + 'abortAllRequests', + 'requestCollection' ]); + openmct.telemetry.request.and.returnValue(Promise.resolve([])); openmct.telemetry.getMetadata.and.returnValue({ ...testTelemetryObject.telemetry, - valueMetadatas: [] + valueMetadatas: testTelemetryObject.telemetry.values, + valuesForHints: jasmine + .createSpy('valuesForHints') + .and.returnValue(testTelemetryObject.telemetry.values), + value: jasmine.createSpy('value').and.callFake((key) => { + return testTelemetryObject.telemetry.values.find((value) => value.key === key); + }) }); - openmct.telemetry.request.and.returnValue(Promise.resolve([])); + openmct.telemetry.requestCollection.and.returnValue(mockTelemetryCollection); openmct.telemetry.getValueFormatter.and.returnValue({ parse: function (value) { return value; } }); + let conditionMgr = new ConditionManager(conditionSetDomainObject, openmct); conditionMgr.on('conditionSetResultUpdated', mockListener); conditionMgr.telemetryObjects = { 'test-object': testTelemetryObject }; conditionMgr.updateConditionTelemetryObjects(); - setTimeout(() => { - expect(mockListener).toHaveBeenCalledWith({ - output: 'Any old telemetry', - id: { - namespace: '', - key: 'cf4456a9-296a-4e6b-b182-62ed29cd15b9' - }, - conditionId: '39584410-cbf9-499e-96dc-76f27e69885d', - utc: undefined - }); - done(); - }, 400); + // Wait for the 'on' callback to be called + await onAddCalledPromise; + + // Simulate the passage of time and no data received + await new Promise((resolve) => setTimeout(resolve, 400)); + + expect(mockListener).toHaveBeenCalledWith({ + output: 'Any old telemetry', + id: { + namespace: '', + key: 'cf4456a9-296a-4e6b-b182-62ed29cd15b9' + }, + conditionId: '39584410-cbf9-499e-96dc-76f27e69885d', + utc: undefined + }); }); - it('should not evaluate as old when telemetry is received in the allotted time', (done) => { - openmct.telemetry.getMetadata = jasmine.createSpy('getMetadata'); - openmct.telemetry.getMetadata.and.returnValue({ - ...testTelemetryObject.telemetry, - valueMetadatas: testTelemetryObject.telemetry.values - }); + it('should not evaluate as old when telemetry is received in the allotted time', async () => { const testDatum = { 'some-key2': '', utc: 1, @@ -771,8 +790,49 @@ describe('the plugin', function () { 'some-key': null, id: 'test-object' }; - openmct.telemetry.request = jasmine.createSpy('request'); + + let onAddResolve; + let onAddCallback; + const onAddCalledPromise = new Promise((resolve) => { + onAddResolve = resolve; + }); + + const mockTelemetryCollection = { + load: jasmine.createSpy('load'), + on: jasmine.createSpy('on').and.callFake((event, callback) => { + if (event === 'add') { + onAddCallback = callback; + onAddResolve(); + } + }) + }; + + openmct.telemetry = jasmine.createSpyObj('telemetry', [ + 'getMetadata', + 'getValueFormatter', + 'request', + 'subscribe', + 'requestCollection' + ]); + openmct.telemetry.subscribe.and.returnValue(function () {}); openmct.telemetry.request.and.returnValue(Promise.resolve([testDatum])); + openmct.telemetry.getMetadata.and.returnValue({ + ...testTelemetryObject.telemetry, + valueMetadatas: testTelemetryObject.telemetry.values, + valuesForHints: jasmine + .createSpy('valuesForHints') + .and.returnValue(testTelemetryObject.telemetry.values), + value: jasmine.createSpy('value').and.callFake((key) => { + return testTelemetryObject.telemetry.values.find((value) => value.key === key); + }) + }); + openmct.telemetry.requestCollection.and.returnValue(mockTelemetryCollection); + openmct.telemetry.getValueFormatter.and.returnValue({ + parse: function (value) { + return value; + } + }); + const date = 1; conditionSetDomainObject.configuration.conditionCollection[0].configuration.criteria[0].input = ['0.4']; @@ -782,19 +842,25 @@ describe('the plugin', function () { 'test-object': testTelemetryObject }; conditionMgr.updateConditionTelemetryObjects(); - conditionMgr.telemetryReceived(testTelemetryObject, testDatum); - setTimeout(() => { - expect(mockListener).toHaveBeenCalledWith({ - output: 'Default', - id: { - namespace: '', - key: 'cf4456a9-296a-4e6b-b182-62ed29cd15b9' - }, - conditionId: '2532d90a-e0d6-4935-b546-3123522da2de', - utc: date - }); - done(); - }, 300); + + // Wait for the 'on' callback to be called + await onAddCalledPromise; + + // Simulate receiving telemetry data + onAddCallback([testDatum]); + + // Wait a bit for the condition manager to process the data + await new Promise((resolve) => setTimeout(resolve, 100)); + + expect(mockListener).toHaveBeenCalledWith({ + output: 'Default', + id: { + namespace: '', + key: 'cf4456a9-296a-4e6b-b182-62ed29cd15b9' + }, + conditionId: '2532d90a-e0d6-4935-b546-3123522da2de', + utc: date + }); }); }); @@ -902,17 +968,25 @@ describe('the plugin', function () { openmct.telemetry.getMetadata = jasmine.createSpy('getMetadata'); openmct.telemetry.getMetadata.and.returnValue({ ...testTelemetryObject.telemetry, - valueMetadatas: [] + valueMetadatas: testTelemetryObject.telemetry.values, + valuesForHints: jasmine + .createSpy('valuesForHints') + .and.returnValue(testTelemetryObject.telemetry.values), + value: jasmine.createSpy('value').and.callFake((key) => { + return testTelemetryObject.telemetry.values.find((value) => value.key === key); + }) }); conditionMgr.on('conditionSetResultUpdated', mockListener); conditionMgr.telemetryObjects = { 'test-object': testTelemetryObject }; conditionMgr.updateConditionTelemetryObjects(); - conditionMgr.telemetryReceived(testTelemetryObject, { - 'some-key': 2, - utc: date - }); + conditionMgr.telemetryReceived(testTelemetryObject, [ + { + 'some-key': 2, + utc: date + } + ]); let result = conditionMgr.conditions.map((condition) => condition.result); expect(result[2]).toBeUndefined(); }); @@ -1002,26 +1076,37 @@ describe('the plugin', function () { } }; openmct.$injector = jasmine.createSpyObj('$injector', ['get']); - // const mockTransactionService = jasmine.createSpyObj( - // 'transactionService', - // ['commit'] - // ); openmct.telemetry = jasmine.createSpyObj('telemetry', [ 'isTelemetryObject', + 'request', 'subscribe', 'getMetadata', 'getValueFormatter', - 'request' + 'requestCollection' ]); - openmct.telemetry.isTelemetryObject.and.returnValue(true); openmct.telemetry.subscribe.and.returnValue(function () {}); + openmct.telemetry.request.and.returnValue(Promise.resolve([])); + openmct.telemetry.isTelemetryObject.and.returnValue(true); + openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry); + openmct.telemetry.getMetadata.and.returnValue({ + ...testTelemetryObject.telemetry, + valueMetadatas: testTelemetryObject.telemetry.values, + valuesForHints: jasmine + .createSpy('valuesForHints') + .and.returnValue(testTelemetryObject.telemetry.values), + value: jasmine.createSpy('value').and.callFake((key) => { + return testTelemetryObject.telemetry.values.find((value) => value.key === key); + }) + }); openmct.telemetry.getValueFormatter.and.returnValue({ parse: function (value) { return value; } }); - openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry); - openmct.telemetry.request.and.returnValue(Promise.resolve([])); + openmct.telemetry.requestCollection.and.returnValue({ + load: jasmine.createSpy('load'), + on: jasmine.createSpy('on') + }); const styleRuleManger = new StyleRuleManager(stylesObject, openmct, null, true); spyOn(styleRuleManger, 'subscribeToConditionSet'); diff --git a/src/plugins/plan/components/PlanView.vue b/src/plugins/plan/components/PlanView.vue index e3d6728268..86a9f3cdf1 100644 --- a/src/plugins/plan/components/PlanView.vue +++ b/src/plugins/plan/components/PlanView.vue @@ -69,7 +69,6 @@ const INNER_TEXT_PADDING = 15; const TEXT_LEFT_PADDING = 5; const ROW_PADDING = 5; const SWIMLANE_PADDING = 3; -const RESIZE_POLL_INTERVAL = 200; const ROW_HEIGHT = 22; const MAX_TEXT_WIDTH = 300; const MIN_ACTIVITY_WIDTH = 2; @@ -143,13 +142,15 @@ export default { this.canvasContext = canvas.getContext('2d'); this.setDimensions(); this.setTimeContext(); - this.resizeTimer = setInterval(this.resize, RESIZE_POLL_INTERVAL); this.handleConfigurationChange(this.configuration); this.planViewConfiguration.on('change', this.handleConfigurationChange); this.loadComposition(); + + this.resizeObserver = new ResizeObserver(this.resize); + this.resizeObserver.observe(this.$refs.plan); }, beforeUnmount() { - clearInterval(this.resizeTimer); + this.resizeObserver.disconnect(); this.stopFollowingTimeContext(); if (this.unlisten) { this.unlisten(); diff --git a/src/plugins/plan/inspector/components/PlanActivityPropertiesView.vue b/src/plugins/plan/inspector/components/PlanActivityPropertiesView.vue index 5d4b771589..b6b7a20ded 100644 --- a/src/plugins/plan/inspector/components/PlanActivityPropertiesView.vue +++ b/src/plugins/plan/inspector/components/PlanActivityPropertiesView.vue @@ -1,5 +1,5 @@