From 47f0b66c7e52b29f559c3479591aa2be539d72cd Mon Sep 17 00:00:00 2001 From: John Hill Date: Fri, 4 Oct 2024 18:47:25 -0400 Subject: [PATCH] [CI] Fix flake with clocks and state generators (#7867) * add two appActions * replace with appAction * replace with determinsitic appAction * fix lint * speed --- .circleci/config.yml | 16 +++--- e2e/appActions.js | 51 +++++++++++++++++++ e2e/tests/framework/appActions.e2e.spec.js | 21 ++++++++ .../styling/conditionSetStyling.e2e.spec.js | 26 +++------- .../visual-a11y/displayLayout.visual.spec.js | 23 +++++---- 5 files changed, 98 insertions(+), 39 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 5da66eafc6..3998a1c918 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -8,8 +8,8 @@ executors: - 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: @@ -323,7 +323,7 @@ workflows: - e2e-couchdb triggers: - schedule: - cron: "0 0 * * *" + cron: '0 0 * * *' filters: branches: only: 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/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/styling/conditionSetStyling.e2e.spec.js b/e2e/tests/functional/plugins/styling/conditionSetStyling.e2e.spec.js index f059fe9ee7..bfb6f4ba63 100644 --- a/e2e/tests/functional/plugins/styling/conditionSetStyling.e2e.spec.js +++ b/e2e/tests/functional/plugins/styling/conditionSetStyling.e2e.spec.js @@ -23,7 +23,11 @@ This test suite is dedicated to tests which verify the basic operations surrounding conditionSets and styling */ -import { createDomainObjectWithDefaults, setRealTimeMode } from '../../../../appActions.js'; +import { + createDomainObjectWithDefaults, + linkParameterToObject, + setRealTimeMode +} from '../../../../appActions.js'; import { MISSION_TIME } from '../../../../constants.js'; import { expect, test } from '../../../../pluginFixtures.js'; @@ -92,7 +96,7 @@ test.describe('Conditionally Styling, using a Condition Set', () => { await page.getByLabel('Save').click(); await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); - await searchAndLinkParameterToObject(page, stateGenerator.name, displayLayout.name); + await linkParameterToObject(page, stateGenerator.name, displayLayout.name); //Add a box to the display layout await page.goto(displayLayout.url, { waitUntil: 'domcontentloaded' }); @@ -157,21 +161,3 @@ async function waitForStyleChange(element, expectedStyle, timeout = 0) { expect(style).toBe(expectedStyle); }).toPass({ timeout: 1000 }); // timeout allows for the style to be applied } - -/** - * 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 searchAndLinkParameterToObject(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(); -} 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();