[CI] Fix flake with clocks and state generators (#7867)

* add two appActions

* replace with appAction

* replace with determinsitic appAction

* fix lint

* speed
This commit is contained in:
John Hill 2024-10-04 18:47:25 -04:00 committed by GitHub
parent 55c023d1eb
commit 47f0b66c7e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 98 additions and 39 deletions

View File

@ -8,8 +8,8 @@ executors:
- image: mcr.microsoft.com/playwright:v1.47.2-focal - image: mcr.microsoft.com/playwright:v1.47.2-focal
environment: environment:
NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed
PERCY_POSTINSTALL_BROWSER: "true" # Needed to store the percy browser in cache deps PERCY_POSTINSTALL_BROWSER: 'true' # Needed to store the percy browser in cache deps
PERCY_LOGLEVEL: "debug" # Enable DEBUG level logging for Percy (Issue: https://github.com/nasa/openmct/issues/5742) PERCY_LOGLEVEL: 'debug' # Enable DEBUG level logging for Percy (Issue: https://github.com/nasa/openmct/issues/5742)
PERCY_PARALLEL_TOTAL: 2 PERCY_PARALLEL_TOTAL: 2
ubuntu: ubuntu:
machine: machine:
@ -17,7 +17,7 @@ executors:
docker_layer_caching: true docker_layer_caching: true
commands: commands:
build_and_install: build_and_install:
description: "All steps used to build and install." description: 'All steps used to build and install.'
parameters: parameters:
node-version: node-version:
type: string type: string
@ -27,7 +27,7 @@ commands:
node-version: << parameters.node-version >> node-version: << parameters.node-version >>
- node/install-packages - node/install-packages
generate_and_store_version_and_filesystem_artifacts: generate_and_store_version_and_filesystem_artifacts:
description: "Track important packages and files" description: 'Track important packages and files'
steps: steps:
- run: | - run: |
[[ $EUID -ne 0 ]] && (sudo mkdir -p /tmp/artifacts && sudo chmod 777 /tmp/artifacts) || (mkdir -p /tmp/artifacts && chmod 777 /tmp/artifacts) [[ $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 [[ $EUID -ne 0 ]] && sudo chmod +x codecov || chmod +x codecov
./codecov --help ./codecov --help
generate_e2e_code_cov_report: 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: parameters:
suite: suite:
type: string type: string
@ -135,13 +135,13 @@ jobs:
suite: #ci or full suite: #ci or full
type: string type: string
executor: pw-focal-development executor: pw-focal-development
parallelism: 7 parallelism: 8
steps: steps:
- build_and_install: - build_and_install:
node-version: lts/hydrogen node-version: lts/hydrogen
- when: #Only install chrome-beta when running the 'full' suite to save $$$ - when: #Only install chrome-beta when running the 'full' suite to save $$$
condition: condition:
equal: ["full", <<parameters.suite>>] equal: ['full', <<parameters.suite>>]
steps: steps:
- run: npx playwright install chrome-beta - run: npx playwright install chrome-beta
- run: - run:
@ -323,7 +323,7 @@ workflows:
- e2e-couchdb - e2e-couchdb
triggers: triggers:
- schedule: - schedule:
cron: "0 0 * * *" cron: '0 0 * * *'
filters: filters:
branches: branches:
only: only:

View File

@ -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<CreatedObjectInfo>} 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 * 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. * 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 { export {
createDomainObjectWithDefaults, createDomainObjectWithDefaults,
createExampleTelemetryObject, createExampleTelemetryObject,
createNotification, createNotification,
createPlanFromJSON, createPlanFromJSON,
createStableStateTelemetry,
expandEntireTree, expandEntireTree,
getCanvasPixels, getCanvasPixels,
linkParameterToObject,
navigateToObjectWithFixedTimeBounds, navigateToObjectWithFixedTimeBounds,
navigateToObjectWithRealTime, navigateToObjectWithRealTime,
setEndOffset, setEndOffset,

View File

@ -26,8 +26,10 @@ import {
createExampleTelemetryObject, createExampleTelemetryObject,
createNotification, createNotification,
createPlanFromJSON, createPlanFromJSON,
createStableStateTelemetry,
expandEntireTree, expandEntireTree,
getCanvasPixels, getCanvasPixels,
linkParameterToObject,
navigateToObjectWithFixedTimeBounds, navigateToObjectWithFixedTimeBounds,
navigateToObjectWithRealTime, navigateToObjectWithRealTime,
setEndOffset, setEndOffset,
@ -339,4 +341,23 @@ test.describe('AppActions @framework', () => {
// Expect this step to fail // Expect this step to fail
await waitForPlotsToRender(page, { timeout: 1000 }); 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();
});
}); });

View File

@ -23,7 +23,11 @@
This test suite is dedicated to tests which verify the basic operations surrounding conditionSets and styling 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 { MISSION_TIME } from '../../../../constants.js';
import { expect, test } from '../../../../pluginFixtures.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.getByLabel('Save').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).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 //Add a box to the display layout
await page.goto(displayLayout.url, { waitUntil: 'domcontentloaded' }); await page.goto(displayLayout.url, { waitUntil: 'domcontentloaded' });
@ -157,21 +161,3 @@ async function waitForStyleChange(element, expectedStyle, timeout = 0) {
expect(style).toBe(expectedStyle); expect(style).toBe(expectedStyle);
}).toPass({ timeout: 1000 }); // timeout allows for the style to be applied }).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();
}

View File

@ -22,7 +22,11 @@
import percySnapshot from '@percy/playwright'; 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 { MISSION_TIME, VISUAL_FIXED_URL } from '../../constants.js';
import { test } from '../../pluginFixtures.js'; import { test } from '../../pluginFixtures.js';
@ -47,16 +51,13 @@ test.describe('Visual - Display Layout @clock', () => {
name: 'Child Right Layout', name: 'Child Right Layout',
parent: parentLayout.uuid parent: parentLayout.uuid
}); });
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator', const stableStateTelemetry = await createStableStateTelemetry(page);
name: 'SWG 1', await linkParameterToObject(page, stableStateTelemetry.name, child1Layout.name);
parent: child1Layout.uuid await linkParameterToObject(page, stableStateTelemetry.name, child2Layout.name);
});
await createDomainObjectWithDefaults(page, { // Pause the clock at a time where the telemetry is stable 20 minutes in the future
type: 'Sine Wave Generator', await page.clock.pauseAt(new Date(MISSION_TIME + 1200000));
name: 'SWG 2',
parent: child2Layout.uuid
});
await page.goto(parentLayout.url, { waitUntil: 'domcontentloaded' }); await page.goto(parentLayout.url, { waitUntil: 'domcontentloaded' });
await page.getByRole('button', { name: 'Edit Object' }).click(); await page.getByRole('button', { name: 'Edit Object' }).click();