diff --git a/e2e/README.md b/e2e/README.md index 619884340d..82a972b97c 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -148,6 +148,7 @@ Current list of test tags: - `@localStorage` - Captures or generates session storage to manipulate browser state. Useful for excluding in tests which require a persistent backend (i.e. CouchDB). - `@snapshot` - Uses Playwright's snapshot functionality to record a copy of the DOM for direct comparison. Must be run inside of the playwright container. - `@unstable` - A new test or test which is known to be flaky. +- `@2p` - Indicates that multiple users are involved, or multiple tabs/pages are used. Useful for testing multi-user interactivity. ### Continuous Integration diff --git a/e2e/appActions.js b/e2e/appActions.js index 09a4f529cc..0d9963aab3 100644 --- a/e2e/appActions.js +++ b/e2e/appActions.js @@ -37,11 +37,16 @@ * @param {string | undefined} name */ async function createDomainObjectWithDefaults(page, type, name) { + // Navigate to focus the 'My Items' folder, and hide the object tree + // This is necessary so that subsequent objects can be created without a parent + // TODO: Ideally this would navigate to a common `e2e` folder + await page.goto('./#/browse/mine?hideTree=true'); + await page.waitForLoadState('networkidle'); //Click the Create button await page.click('button:has-text("Create")'); // Click the object specified by 'type' - await page.click(`text=${type}`); + await page.click(`li:text("${type}")`); // Modify the name input field of the domain object to accept 'name' if (name) { @@ -52,9 +57,13 @@ async function createDomainObjectWithDefaults(page, type, name) { // Click OK button and wait for Navigate event await Promise.all([ - page.waitForNavigation({waitUntil: 'networkidle'}), - page.click('text=OK') + page.waitForNavigation(), + page.click('[aria-label="Save"]'), + // Wait for Save Banner to appear + page.waitForSelector('.c-message-banner__message') ]); + + return name || `Unnamed ${type}`; } /** diff --git a/e2e/pluginFixtures.js b/e2e/pluginFixtures.js index 39ec388c34..101cd339ac 100644 --- a/e2e/pluginFixtures.js +++ b/e2e/pluginFixtures.js @@ -27,7 +27,7 @@ */ const { test, expect } = require('./baseFixtures'); -const { createDomainObjectWithDefaults } = require('./appActions'); +// const { createDomainObjectWithDefaults } = require('./appActions'); /** * @typedef {Object} ObjectCreateOptions @@ -36,12 +36,16 @@ const { createDomainObjectWithDefaults } = require('./appActions'); */ /** + * **NOTE: This feature is a work-in-progress and should not currently be used.** + * * Used to create a new domain object as a part of getOrCreateDomainObject. * @type {Map} */ -const createdObjects = new Map(); +// const createdObjects = new Map(); /** + * **NOTE: This feature is a work-in-progress and should not currently be used.** + * * This action will create a domain object for the test to reference and return the uuid. If an object * of a given name already exists, it will return the uuid of that object to the test instead of creating * a new file. The intent is to move object creation out of test suites which are not explicitly worried @@ -50,27 +54,29 @@ const createdObjects = new Map(); * @param {ObjectCreateOptions} options * @returns {Promise} uuid of the domain object */ -async function getOrCreateDomainObject(page, options) { - const { type, name } = options; - const objectName = name ? `${type}:${name}` : type; +// async function getOrCreateDomainObject(page, options) { +// const { type, name } = options; +// const objectName = name ? `${type}:${name}` : type; - if (createdObjects.has(objectName)) { - return createdObjects.get(objectName); - } +// if (createdObjects.has(objectName)) { +// return createdObjects.get(objectName); +// } - await createDomainObjectWithDefaults(page, type, name); +// await createDomainObjectWithDefaults(page, type, name); - // Once object is created, get the uuid from the url - const uuid = await page.evaluate(() => { - return window.location.href.match(/[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/)[0]; - }); +// // Once object is created, get the uuid from the url +// const uuid = await page.evaluate(() => { +// return window.location.href.match(/[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/)[0]; +// }); - createdObjects.set(objectName, uuid); +// createdObjects.set(objectName, uuid); - return uuid; -} +// return uuid; +// } /** + * **NOTE: This feature is a work-in-progress and should not currently be used.** + * * If provided, these options will be used to get or create the desired domain object before * any tests or test hooks have run. * The `uuid` of the `domainObject` will then be available to use within the scoped tests. @@ -87,7 +93,7 @@ async function getOrCreateDomainObject(page, options) { * ``` * @type {ObjectCreateOptions} */ -const objectCreateOptions = null; +// const objectCreateOptions = null; /** * The name of the "My Items" folder in the domain object tree. @@ -103,23 +109,23 @@ exports.test = test.extend({ // eslint-disable-next-line no-shadow openmctConfig: async ({ myItemsFolderName }, use) => { await use({ myItemsFolderName }); - }, - objectCreateOptions: [objectCreateOptions, {option: true}], + } + // objectCreateOptions: [objectCreateOptions, {option: true}], // eslint-disable-next-line no-shadow - domainObject: [async ({ page, objectCreateOptions }, use) => { - // FIXME: This is a false-positive caused by a bug in the eslint-plugin-playwright rule. - // eslint-disable-next-line playwright/no-conditional-in-test - if (objectCreateOptions === null) { - await use(page); + // domainObject: [async ({ page, objectCreateOptions }, use) => { + // // FIXME: This is a false-positive caused by a bug in the eslint-plugin-playwright rule. + // // eslint-disable-next-line playwright/no-conditional-in-test + // if (objectCreateOptions === null) { + // await use(page); - return; - } + // return; + // } - //Go to baseURL - await page.goto('./', { waitUntil: 'networkidle' }); + // //Go to baseURL + // await page.goto('./', { waitUntil: 'networkidle' }); - const uuid = await getOrCreateDomainObject(page, objectCreateOptions); - await use({ uuid }); - }, { auto: true }] + // const uuid = await getOrCreateDomainObject(page, objectCreateOptions); + // await use({ uuid }); + // }, { auto: true }] }); exports.expect = expect; diff --git a/e2e/tests/framework/appActions.e2e.spec.js b/e2e/tests/framework/appActions.e2e.spec.js new file mode 100644 index 0000000000..fdb228708e --- /dev/null +++ b/e2e/tests/framework/appActions.e2e.spec.js @@ -0,0 +1,41 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2022, 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 { test, expect } = require('../../baseFixtures.js'); +const { createDomainObjectWithDefaults } = require('../../appActions.js'); + +test.describe('appActions tests', () => { + test('createDomainObjectsWithDefaults can create multiple objects in a row', async ({ page }) => { + await page.goto('./', { waitUntil: 'networkidle' }); + await createDomainObjectWithDefaults(page, 'Timer', 'Timer Foo'); + await createDomainObjectWithDefaults(page, 'Timer', 'Timer Bar'); + await createDomainObjectWithDefaults(page, 'Timer', 'Timer Baz'); + + // Expand the tree + await page.click('.c-disclosure-triangle'); + + // Verify the objects were created + await expect(page.locator('a :text("Timer Foo")')).toBeVisible(); + await expect(page.locator('a :text("Timer Bar")')).toBeVisible(); + await expect(page.locator('a :text("Timer Baz")')).toBeVisible(); + }); +}); diff --git a/e2e/tests/framework/exampleTemplate.e2e.spec.js b/e2e/tests/framework/exampleTemplate.e2e.spec.js index e1d28ca779..b5d20062e8 100644 --- a/e2e/tests/framework/exampleTemplate.e2e.spec.js +++ b/e2e/tests/framework/exampleTemplate.e2e.spec.js @@ -52,7 +52,7 @@ const { createDomainObjectWithDefaults } = require('../../appActions'); // Structure: Try to keep a single describe block per logical groups of tests. If your test runtime exceeds 5 minutes or 500 lines, it's likely that it will need to be split. // Annotations: Please use the @unstable tag so that our automation can pick it up as a part of our test promotion pipeline. -test.describe('Renaming Timer Object @unstable', () => { +test.describe('Renaming Timer Object', () => { //Create a testcase name which will be obvious when it fails in CI test('Can create a new Timer object and rename it from actions Menu', async ({ page }) => { //Open a browser, navigate to the main page, and wait until all networkevents to resolve @@ -68,7 +68,6 @@ test.describe('Renaming Timer Object @unstable', () => { //Assert that the name has changed in the browser bar to the value we assigned above await expect(page.locator('.l-browse-bar__object-name')).toContainText(newObjectName); - }); test('An existing Timer object can be renamed twice', async ({ page }) => { //Open a browser, navigate to the main page, and wait until all networkevents to resolve diff --git a/e2e/tests/framework/pluginFixtures.e2e.spec.js b/e2e/tests/framework/pluginFixtures.e2e.spec.js index 1d8ffbe01b..0f58a075e0 100644 --- a/e2e/tests/framework/pluginFixtures.e2e.spec.js +++ b/e2e/tests/framework/pluginFixtures.e2e.spec.js @@ -25,21 +25,22 @@ This test suite is dedicated to testing our use of our custom fixtures to verify that they are working as expected. */ -const { test, expect } = require('../../pluginFixtures.js'); +const { test } = require('../../pluginFixtures.js'); -test.describe('pluginFixtures tests', () => { - test.use({ domainObjectName: 'Timer' }); - let timerUUID; +// eslint-disable-next-line playwright/no-skipped-test +test.describe.skip('pluginFixtures tests', () => { + // test.use({ domainObjectName: 'Timer' }); + // let timerUUID; - test('Creates a timer object @framework @unstable', ({ domainObject }) => { - const { uuid } = domainObject; - const uuidRegexp = /[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/; - expect(uuid).toMatch(uuidRegexp); - timerUUID = uuid; - }); + // test('Creates a timer object @framework @unstable', ({ domainObject }) => { + // const { uuid } = domainObject; + // const uuidRegexp = /[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/; + // expect(uuid).toMatch(uuidRegexp); + // timerUUID = uuid; + // }); - test('Provides same uuid for subsequent uses of the same object @framework', ({ domainObject }) => { - const { uuid } = domainObject; - expect(uuid).toEqual(timerUUID); - }); + // test('Provides same uuid for subsequent uses of the same object @framework', ({ domainObject }) => { + // const { uuid } = domainObject; + // expect(uuid).toEqual(timerUUID); + // }); }); diff --git a/e2e/tests/functional/branding.e2e.spec.js b/e2e/tests/functional/branding.e2e.spec.js index 473cb79e73..2f494af9d1 100644 --- a/e2e/tests/functional/branding.e2e.spec.js +++ b/e2e/tests/functional/branding.e2e.spec.js @@ -38,14 +38,14 @@ test.describe('Branding tests', () => { await expect(page.locator('.c-about__image')).toBeVisible(); // Modify the Build information in 'about' Modal - const versionInformationLocator = page.locator('ul.t-info.l-info.s-info'); + const versionInformationLocator = page.locator('ul.t-info.l-info.s-info').first(); await expect(versionInformationLocator).toBeEnabled(); await expect.soft(versionInformationLocator).toContainText(/Version: \d/); await expect.soft(versionInformationLocator).toContainText(/Build Date: ((?:Mon|Tue|Wed|Thu|Fri|Sat|Sun))/); await expect.soft(versionInformationLocator).toContainText(/Revision: \b[0-9a-f]{5,40}\b/); await expect.soft(versionInformationLocator).toContainText(/Branch: ./); }); - test('Verify Links in About Modal', async ({ page }) => { + test('Verify Links in About Modal @2p', async ({ page }) => { // Go to baseURL await page.goto('./', { waitUntil: 'networkidle' }); diff --git a/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js b/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js index 3f78d620ac..54256f9966 100644 --- a/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js +++ b/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js @@ -28,6 +28,7 @@ but only assume that example imagery is present. const { waitForAnimations } = require('../../../../baseFixtures'); const { test, expect } = require('../../../../pluginFixtures'); +const { createDomainObjectWithDefaults } = require('../../../../appActions'); const backgroundImageSelector = '.c-imagery__main-image__background-image'; const panHotkey = process.platform === 'linux' ? ['Control', 'Alt'] : ['Alt']; const expectedAltText = process.platform === 'linux' ? 'Ctrl+Alt drag to pan' : 'Alt drag to pan'; @@ -39,26 +40,17 @@ test.describe('Example Imagery Object', () => { //Go to baseURL await page.goto('./', { waitUntil: 'networkidle' }); - //Click the Create button - await page.click('button:has-text("Create")'); + // Create a default 'Example Imagery' object + createDomainObjectWithDefaults(page, 'Example Imagery'); - // Click text=Example Imagery - await page.click('text=Example Imagery'); - - // Click text=OK await Promise.all([ - page.waitForNavigation({waitUntil: 'networkidle'}), - page.click('text=OK'), - //Wait for Save Banner to appear - page.waitForSelector('.c-message-banner__message') + page.waitForNavigation(), + page.locator(backgroundImageSelector).hover({trial: true}), + // eslint-disable-next-line playwright/missing-playwright-await + expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery') ]); - // Close Banner - await page.locator('.c-message-banner__close-button').click(); - //Wait until Save Banner is gone - await page.waitForSelector('.c-message-banner__message', { state: 'detached'}); - await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery'); - await page.locator(backgroundImageSelector).hover({trial: true}); + // Verify that the created object is focused }); test('Can use Mouse Wheel to zoom in and out of latest image', async ({ page }) => { @@ -208,7 +200,7 @@ test.describe('Example Imagery Object', () => { const pausePlayButton = page.locator('.c-button.pause-play'); // open the time conductor drop down - await page.locator('button:has-text("Fixed Timespan")').click(); + await page.locator('.c-mode-button').click(); // Click local clock await page.locator('[data-testid="conductor-modeOption-realtime"]').click(); @@ -532,7 +524,7 @@ test.describe('Example Imagery in Flexible layout', () => { await page.locator('.c-mode-button').click(); // Select local clock mode - await page.locator('[data-testid=conductor-modeOption-realtime]').click(); + await page.locator('[data-testid=conductor-modeOption-realtime]').nth(0).click(); // Zoom in on next image await mouseZoomIn(page); diff --git a/e2e/tests/functional/plugins/notebook/tags.e2e.spec.js b/e2e/tests/functional/plugins/notebook/tags.e2e.spec.js index aa8bd9e8a1..2d48606729 100644 --- a/e2e/tests/functional/plugins/notebook/tags.e2e.spec.js +++ b/e2e/tests/functional/plugins/notebook/tags.e2e.spec.js @@ -25,25 +25,18 @@ This test suite is dedicated to tests which verify form functionality. */ const { test, expect } = require('../../../../pluginFixtures'); +const { createDomainObjectWithDefaults } = require('../../../../appActions'); /** * Creates a notebook object and adds an entry. * @param {import('@playwright/test').Page} - page to load * @param {number} [iterations = 1] - the number of entries to create */ -async function createNotebookAndEntry(page, myItemsFolderName, iterations = 1) { +async function createNotebookAndEntry(page, iterations = 1) { //Go to baseURL await page.goto('./', { waitUntil: 'networkidle' }); - // Click button:has-text("Create") - await page.locator('button:has-text("Create")').click(); - await page.locator('[title="Create and save timestamped notes with embedded object snapshots."]').click(); - // Click button:has-text("OK") - await Promise.all([ - page.waitForNavigation(), - page.locator(`[name="mctForm"] >> text=${myItemsFolderName}`).click(), - page.locator('button:has-text("OK")').click() - ]); + createDomainObjectWithDefaults(page, 'Notebook'); for (let iteration = 0; iteration < iterations; iteration++) { // Click text=To start a new entry, click here or drag and drop any object @@ -52,7 +45,6 @@ async function createNotebookAndEntry(page, myItemsFolderName, iterations = 1) { await page.locator(entryLocator).click(); await page.locator(entryLocator).fill(`Entry ${iteration}`); } - } /** @@ -60,8 +52,8 @@ async function createNotebookAndEntry(page, myItemsFolderName, iterations = 1) { * @param {import('@playwright/test').Page} page * @param {number} [iterations = 1] - the number of entries (and tags) to create */ -async function createNotebookEntryAndTags(page, myItemsFolderName, iterations = 1) { - await createNotebookAndEntry(page, myItemsFolderName, iterations); +async function createNotebookEntryAndTags(page, iterations = 1) { + await createNotebookAndEntry(page, iterations); for (let iteration = 0; iteration < iterations; iteration++) { // Click text=To start a new entry, click here or drag and drop any object @@ -81,11 +73,10 @@ async function createNotebookEntryAndTags(page, myItemsFolderName, iterations = } } -test.describe('Tagging in Notebooks', () => { - test('Can load tags', async ({ page, openmctConfig }) => { - const { myItemsFolderName } = openmctConfig; +test.describe('Tagging in Notebooks @addInit', () => { + test('Can load tags', async ({ page }) => { - await createNotebookAndEntry(page, myItemsFolderName); + await createNotebookAndEntry(page); // Click text=To start a new entry, click here or drag and drop any object await page.locator('button:has-text("Add Tag")').click(); @@ -96,10 +87,8 @@ test.describe('Tagging in Notebooks', () => { await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Drilling"); await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Driving"); }); - test('Can add tags', async ({ page, openmctConfig }) => { - const { myItemsFolderName } = openmctConfig; - - await createNotebookEntryAndTags(page, myItemsFolderName); + test('Can add tags', async ({ page }) => { + await createNotebookEntryAndTags(page); await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Science"); await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Driving"); @@ -113,10 +102,8 @@ test.describe('Tagging in Notebooks', () => { await expect(page.locator('[aria-label="Autocomplete Options"]')).not.toContainText("Driving"); await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Drilling"); }); - test('Can search for tags', async ({ page, openmctConfig }) => { - const { myItemsFolderName } = openmctConfig; - - await createNotebookEntryAndTags(page, myItemsFolderName); + test('Can search for tags', async ({ page }) => { + await createNotebookEntryAndTags(page); // Click [aria-label="OpenMCT Search"] input[type="search"] await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); // Fill [aria-label="OpenMCT Search"] input[type="search"] @@ -139,10 +126,8 @@ test.describe('Tagging in Notebooks', () => { await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible(); }); - test('Can delete tags', async ({ page, openmctConfig }) => { - const { myItemsFolderName } = openmctConfig; - - await createNotebookEntryAndTags(page, myItemsFolderName); + test('Can delete tags', async ({ page }) => { + await createNotebookEntryAndTags(page); await page.locator('[aria-label="Notebook Entries"]').click(); // Delete Driving await page.locator('text=Science Driving Add Tag >> button').nth(1).click(); @@ -154,28 +139,14 @@ test.describe('Tagging in Notebooks', () => { await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc'); await expect(page.locator('[aria-label="Search Result"]')).not.toContainText("Driving"); }); - test('Tags persist across reload', async ({ page, openmctConfig }) => { - const { myItemsFolderName } = openmctConfig; - + test('Tags persist across reload', async ({ page }) => { //Go to baseURL await page.goto('./', { waitUntil: 'networkidle' }); - // Create a clock object we can navigate to - await page.click('button:has-text("Create")'); - - // Click Clock - await page.click('text=Clock'); - // Click button:has-text("OK") - await Promise.all([ - page.waitForNavigation(), - page.locator(`[name="mctForm"] >> text=${myItemsFolderName}`).click(), - page.locator('button:has-text("OK")').click() - ]); - - await page.click('.c-disclosure-triangle'); + await createDomainObjectWithDefaults(page, 'Clock'); const ITERATIONS = 4; - await createNotebookEntryAndTags(page, myItemsFolderName, ITERATIONS); + await createNotebookEntryAndTags(page, ITERATIONS); for (let iteration = 0; iteration < ITERATIONS; iteration++) { const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`; @@ -183,6 +154,11 @@ test.describe('Tagging in Notebooks', () => { await expect(page.locator(entryLocator)).toContainText("Driving"); } + await Promise.all([ + page.waitForNavigation(), + page.goto('./#/browse/mine?hideTree=false'), + page.click('.c-disclosure-triangle') + ]); // Click Unnamed Clock await page.click('text="Unnamed Clock"'); diff --git a/e2e/tests/functional/plugins/timeConductor/timeConductor.e2e.spec.js b/e2e/tests/functional/plugins/timeConductor/timeConductor.e2e.spec.js index 6e23c4c82a..8d764526b3 100644 --- a/e2e/tests/functional/plugins/timeConductor/timeConductor.e2e.spec.js +++ b/e2e/tests/functional/plugins/timeConductor/timeConductor.e2e.spec.js @@ -142,7 +142,7 @@ test.describe('Time conductor input fields real-time mode', () => { await expect(page.locator('data-testid=conductor-end-offset-button')).toContainText('00:00:01'); // Verify url parameters persist after mode switch - await page.waitForNavigation(); + await page.waitForNavigation({ waitUntil: 'networkidle' }); expect(page.url()).toContain(`startDelta=${startDelta}`); expect(page.url()).toContain(`endDelta=${endDelta}`); }); diff --git a/e2e/tests/functional/plugins/timer/timer.e2e.spec.js b/e2e/tests/functional/plugins/timer/timer.e2e.spec.js index 648467b120..bf8dca37c7 100644 --- a/e2e/tests/functional/plugins/timer/timer.e2e.spec.js +++ b/e2e/tests/functional/plugins/timer/timer.e2e.spec.js @@ -21,14 +21,14 @@ *****************************************************************************/ const { test, expect } = require('../../../../pluginFixtures'); -const { openObjectTreeContextMenu } = require('../../../../appActions'); - -const options = { - type: 'Timer' -}; +const { openObjectTreeContextMenu, createDomainObjectWithDefaults } = require('../../../../appActions'); test.describe('Timer', () => { - test.use({ objectCreateOptions: options }); + test.beforeEach(async ({ page }) => { + await page.goto('./', { waitUntil: 'networkidle' }); + await createDomainObjectWithDefaults(page, 'timer'); + }); + test('Can perform actions on the Timer', async ({ page, openmctConfig }) => { test.info().annotations.push({ type: 'issue',