From 9c4d2474a3ac145a3ff673ca618b6535addd6066 Mon Sep 17 00:00:00 2001 From: Jesse Mazzella Date: Thu, 26 Jan 2023 09:25:15 -0800 Subject: [PATCH] cherry-pick(#5997): Make tree items more actionable and add AppAction for expanding the object tree * style: add `visibility` to tree expand triangles - The purpose of this is so that Playwright can perform actionability checks on the tree items. This will make operations involving expanding tree items much easier to perform in e2e. * feat(e2e): Add AppAction to expand the entire tree * fix: wait for loading indicator * test: add test for `expandEntireTree` * test: update `expandEntireTree` and tree selectors - Use dynamic aria-label for different tree implementations - Get rid of CSS ids which are only for testing - Update percy tree scope selector * chore(lint): remove unused variable * refactor(e2e): update tree locators Co-authored-by: John Hill --- e2e/appActions.js | 23 +++++++- e2e/tests/framework/appActions.e2e.spec.js | 55 ++++++++++++++++++- .../functional/moveAndLinkObjects.e2e.spec.js | 32 +++++++---- .../displayLayout/displayLayout.e2e.spec.js | 16 ++++-- .../flexibleLayout/flexibleLayout.e2e.spec.js | 12 +++- .../plugins/notebook/tags.e2e.spec.js | 4 +- e2e/tests/functional/tree.e2e.spec.js | 4 +- .../visual/components/tree.visual.spec.js | 4 +- src/api/forms/components/controls/Locator.vue | 1 - src/styles/_controls.scss | 2 + src/ui/layout/Layout.vue | 4 +- src/ui/layout/mct-tree.vue | 4 ++ 12 files changed, 132 insertions(+), 29 deletions(-) diff --git a/e2e/appActions.js b/e2e/appActions.js index 50e56edbf04..5a302afab49 100644 --- a/e2e/appActions.js +++ b/e2e/appActions.js @@ -144,7 +144,9 @@ async function createNotification(page, createNotificationOptions) { * @param {string} name */ async function expandTreePaneItemByName(page, name) { - const treePane = page.locator('#tree-pane'); + const treePane = page.getByRole('tree', { + name: 'Main Tree' + }); const treeItem = treePane.locator(`role=treeitem[expanded=false][name=/${name}/]`); const expandTriangle = treeItem.locator('.c-disclosure-triangle'); await expandTriangle.click(); @@ -218,6 +220,24 @@ async function openObjectTreeContextMenu(page, url) { }); } +/** + * Expands the entire object tree (every expandable tree item). + * @param {import('@playwright/test').Page} page + * @param {"Main Tree" | "Create Modal Tree"} [treeName="Main Tree"] + */ +async function expandEntireTree(page, treeName = "Main Tree") { + const treeLocator = page.getByRole('tree', { + name: treeName + }); + const collapsedTreeItems = treeLocator.getByRole('treeitem', { + expanded: false + }).locator('span.c-disclosure-triangle.is-enabled'); + + while (await collapsedTreeItems.count() > 0) { + await collapsedTreeItems.nth(0).click(); + } +} + /** * Gets the UUID of the currently focused object by parsing the current URL * and returning the last UUID in the path. @@ -362,6 +382,7 @@ module.exports = { createDomainObjectWithDefaults, createNotification, expandTreePaneItemByName, + expandEntireTree, createPlanFromJSON, openObjectTreeContextMenu, getHashUrlToDomainObject, diff --git a/e2e/tests/framework/appActions.e2e.spec.js b/e2e/tests/framework/appActions.e2e.spec.js index 10ae0b11f89..f9e4ca5b0e7 100644 --- a/e2e/tests/framework/appActions.e2e.spec.js +++ b/e2e/tests/framework/appActions.e2e.spec.js @@ -21,7 +21,7 @@ *****************************************************************************/ const { test, expect } = require('../../pluginFixtures.js'); -const { createDomainObjectWithDefaults, createNotification } = require('../../appActions.js'); +const { createDomainObjectWithDefaults, createNotification, expandEntireTree } = require('../../appActions.js'); test.describe('AppActions', () => { test('createDomainObjectsWithDefaults', async ({ page }) => { @@ -109,4 +109,57 @@ test.describe('AppActions', () => { await expect(page.locator('.c-message-banner')).toHaveClass(/error/); await page.locator('[aria-label="Dismiss"]').click(); }); + test('expandEntireTree', async ({ page }) => { + await page.goto('./', { waitUntil: 'networkidle' }); + + const rootFolder = await createDomainObjectWithDefaults(page, { + type: 'Folder' + }); + const folder1 = await createDomainObjectWithDefaults(page, { + type: 'Folder', + parent: rootFolder.uuid + }); + + await createDomainObjectWithDefaults(page, { + type: 'Clock', + parent: folder1.uuid + }); + const folder2 = await createDomainObjectWithDefaults(page, { + type: 'Folder', + parent: folder1.uuid + }); + await createDomainObjectWithDefaults(page, { + type: 'Folder', + parent: folder1.uuid + }); + await createDomainObjectWithDefaults(page, { + type: 'Display Layout', + parent: folder2.uuid + }); + await createDomainObjectWithDefaults(page, { + type: 'Folder', + parent: folder2.uuid + }); + + await page.goto('./#/browse/mine'); + await expandEntireTree(page); + const treePane = page.getByRole('tree', { + name: "Main Tree" + }); + const treePaneCollapsedItems = treePane.getByRole('treeitem', { expanded: false }); + expect(await treePaneCollapsedItems.count()).toBe(0); + + await page.goto('./#/browse/mine'); + //Click the Create button + await page.click('button:has-text("Create")'); + + // Click the object specified by 'type' + await page.click(`li[role='menuitem']:text("Clock")`); + await expandEntireTree(page, "Create Modal Tree"); + const locatorTree = page.getByRole("tree", { + name: "Create Modal Tree" + }); + const locatorTreeCollapsedItems = locatorTree.locator('role=treeitem[expanded=false]'); + expect(await locatorTreeCollapsedItems.count()).toBe(0); + }); }); diff --git a/e2e/tests/functional/moveAndLinkObjects.e2e.spec.js b/e2e/tests/functional/moveAndLinkObjects.e2e.spec.js index 0a29dd9859c..137f48ed3fc 100644 --- a/e2e/tests/functional/moveAndLinkObjects.e2e.spec.js +++ b/e2e/tests/functional/moveAndLinkObjects.e2e.spec.js @@ -52,7 +52,9 @@ test.describe('Move & link item tests', () => { // Attempt to move parent to its own grandparent await page.locator('button[title="Show selected item in tree"]').click(); - const treePane = page.locator('#tree-pane'); + const treePane = page.getByRole('tree', { + name: 'Main Tree' + }); await treePane.getByRole('treeitem', { name: 'Parent Folder' }).click({ @@ -63,28 +65,30 @@ test.describe('Move & link item tests', () => { name: /Move/ }).click(); - const locatorTree = page.locator('#locator-tree'); - const myItemsLocatorTreeItem = locatorTree.getByRole('treeitem', { + const createModalTree = page.getByRole('tree', { + name: "Create Modal Tree" + }); + const myItemsLocatorTreeItem = createModalTree.getByRole('treeitem', { name: myItemsFolderName }); await myItemsLocatorTreeItem.locator('.c-disclosure-triangle').click(); await myItemsLocatorTreeItem.click(); - const parentFolderLocatorTreeItem = locatorTree.getByRole('treeitem', { + const parentFolderLocatorTreeItem = createModalTree.getByRole('treeitem', { name: parentFolder.name }); await parentFolderLocatorTreeItem.locator('.c-disclosure-triangle').click(); await parentFolderLocatorTreeItem.click(); await expect(page.locator('[aria-label="Save"]')).toBeDisabled(); - const childFolderLocatorTreeItem = locatorTree.getByRole('treeitem', { + const childFolderLocatorTreeItem = createModalTree.getByRole('treeitem', { name: new RegExp(childFolder.name) }); await childFolderLocatorTreeItem.locator('.c-disclosure-triangle').click(); await childFolderLocatorTreeItem.click(); await expect(page.locator('[aria-label="Save"]')).toBeDisabled(); - const grandchildFolderLocatorTreeItem = locatorTree.getByRole('treeitem', { + const grandchildFolderLocatorTreeItem = createModalTree.getByRole('treeitem', { name: grandchildFolder.name }); await grandchildFolderLocatorTreeItem.locator('.c-disclosure-triangle').click(); @@ -195,7 +199,9 @@ test.describe('Move & link item tests', () => { // Attempt to move parent to its own grandparent await page.locator('button[title="Show selected item in tree"]').click(); - const treePane = page.locator('#tree-pane'); + const treePane = page.getByRole('tree', { + name: 'Main Tree' + }); await treePane.getByRole('treeitem', { name: 'Parent Folder' }).click({ @@ -206,28 +212,30 @@ test.describe('Move & link item tests', () => { name: /Move/ }).click(); - const locatorTree = page.locator('#locator-tree'); - const myItemsLocatorTreeItem = locatorTree.getByRole('treeitem', { + const createModalTree = page.getByRole('tree', { + name: "Create Modal Tree" + }); + const myItemsLocatorTreeItem = createModalTree.getByRole('treeitem', { name: myItemsFolderName }); await myItemsLocatorTreeItem.locator('.c-disclosure-triangle').click(); await myItemsLocatorTreeItem.click(); - const parentFolderLocatorTreeItem = locatorTree.getByRole('treeitem', { + const parentFolderLocatorTreeItem = createModalTree.getByRole('treeitem', { name: parentFolder.name }); await parentFolderLocatorTreeItem.locator('.c-disclosure-triangle').click(); await parentFolderLocatorTreeItem.click(); await expect(page.locator('[aria-label="Save"]')).toBeDisabled(); - const childFolderLocatorTreeItem = locatorTree.getByRole('treeitem', { + const childFolderLocatorTreeItem = createModalTree.getByRole('treeitem', { name: new RegExp(childFolder.name) }); await childFolderLocatorTreeItem.locator('.c-disclosure-triangle').click(); await childFolderLocatorTreeItem.click(); await expect(page.locator('[aria-label="Save"]')).toBeDisabled(); - const grandchildFolderLocatorTreeItem = locatorTree.getByRole('treeitem', { + const grandchildFolderLocatorTreeItem = createModalTree.getByRole('treeitem', { name: grandchildFolder.name }); await grandchildFolderLocatorTreeItem.locator('.c-disclosure-triangle').click(); diff --git a/e2e/tests/functional/plugins/displayLayout/displayLayout.e2e.spec.js b/e2e/tests/functional/plugins/displayLayout/displayLayout.e2e.spec.js index e9c157bbf7c..0dd3243aaf9 100644 --- a/e2e/tests/functional/plugins/displayLayout/displayLayout.e2e.spec.js +++ b/e2e/tests/functional/plugins/displayLayout/displayLayout.e2e.spec.js @@ -47,7 +47,9 @@ test.describe('Display Layout', () => { // Expand the 'My Items' folder in the left tree await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click(); // Add the Sine Wave Generator to the Display Layout and save changes - const treePane = page.locator('#tree-pane'); + const treePane = page.getByRole('tree', { + name: 'Main Tree' + }); const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', { name: new RegExp(sineWaveObject.name) }); @@ -79,7 +81,9 @@ test.describe('Display Layout', () => { // Expand the 'My Items' folder in the left tree await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click(); // Add the Sine Wave Generator to the Display Layout and save changes - const treePane = page.locator('#tree-pane'); + const treePane = page.getByRole('tree', { + name: 'Main Tree' + }); const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', { name: new RegExp(sineWaveObject.name) }); @@ -115,7 +119,9 @@ test.describe('Display Layout', () => { // Expand the 'My Items' folder in the left tree await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click(); // Add the Sine Wave Generator to the Display Layout and save changes - const treePane = page.locator('#tree-pane'); + const treePane = page.getByRole('tree', { + name: 'Main Tree' + }); const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', { name: new RegExp(sineWaveObject.name) }); @@ -153,7 +159,9 @@ test.describe('Display Layout', () => { // Expand the 'My Items' folder in the left tree await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click(); // Add the Sine Wave Generator to the Display Layout and save changes - const treePane = page.locator('#tree-pane'); + const treePane = page.getByRole('tree', { + name: 'Main Tree' + }); const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', { name: new RegExp(sineWaveObject.name) }); diff --git a/e2e/tests/functional/plugins/flexibleLayout/flexibleLayout.e2e.spec.js b/e2e/tests/functional/plugins/flexibleLayout/flexibleLayout.e2e.spec.js index 4e28185ecec..b2f01807724 100644 --- a/e2e/tests/functional/plugins/flexibleLayout/flexibleLayout.e2e.spec.js +++ b/e2e/tests/functional/plugins/flexibleLayout/flexibleLayout.e2e.spec.js @@ -40,7 +40,9 @@ test.describe('Flexible Layout', () => { }); }); test('panes have the appropriate draggable attribute while in Edit and Browse modes', async ({ page }) => { - const treePane = page.locator('#tree-pane'); + const treePane = page.getByRole('tree', { + name: 'Main Tree' + }); const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', { name: new RegExp(sineWaveObject.name) }); @@ -70,7 +72,9 @@ test.describe('Flexible Layout', () => { await expect(dragWrapper).toHaveAttribute('draggable', 'false'); }); test('items in a flexible layout can be removed with object tree context menu when viewing the flexible layout', async ({ page }) => { - const treePane = page.locator('#tree-pane'); + const treePane = page.getByRole('tree', { + name: 'Main Tree' + }); const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', { name: new RegExp(sineWaveObject.name) }); @@ -106,7 +110,9 @@ test.describe('Flexible Layout', () => { type: 'issue', description: 'https://github.com/nasa/openmct/issues/3117' }); - const treePane = page.locator('#tree-pane'); + const treePane = page.getByRole('tree', { + name: 'Main Tree' + }); const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', { name: new RegExp(sineWaveObject.name) }); diff --git a/e2e/tests/functional/plugins/notebook/tags.e2e.spec.js b/e2e/tests/functional/plugins/notebook/tags.e2e.spec.js index a0b9076abf5..beb7d7209d9 100644 --- a/e2e/tests/functional/plugins/notebook/tags.e2e.spec.js +++ b/e2e/tests/functional/plugins/notebook/tags.e2e.spec.js @@ -198,7 +198,9 @@ test.describe('Tagging in Notebooks @addInit', () => { page.click('.c-disclosure-triangle') ]); - const treePane = page.locator('#tree-pane'); + const treePane = page.getByRole('tree', { + name: 'Main Tree' + }); // Click Clock await treePane.getByRole('treeitem', { name: clock.name diff --git a/e2e/tests/functional/tree.e2e.spec.js b/e2e/tests/functional/tree.e2e.spec.js index 691f7f1277b..81f0939aa23 100644 --- a/e2e/tests/functional/tree.e2e.spec.js +++ b/e2e/tests/functional/tree.e2e.spec.js @@ -116,7 +116,9 @@ async function getAndAssertTreeItems(page, expected) { * @param {string} name */ async function expandTreePaneItemByName(page, name) { - const treePane = page.locator('#tree-pane'); + const treePane = page.getByRole('tree', { + name: 'Main Tree' + }); const treeItem = treePane.locator(`role=treeitem[expanded=false][name=/${name}/]`); const expandTriangle = treeItem.locator('.c-disclosure-triangle'); await expandTriangle.click(); diff --git a/e2e/tests/visual/components/tree.visual.spec.js b/e2e/tests/visual/components/tree.visual.spec.js index 0ad2aca75fb..f6e7e6dc90b 100644 --- a/e2e/tests/visual/components/tree.visual.spec.js +++ b/e2e/tests/visual/components/tree.visual.spec.js @@ -57,7 +57,7 @@ test.describe('Visual - Tree Pane', () => { name: 'Z Clock' }); - const treePane = "#tree-pane"; + const treePane = "[role=tree][aria-label='Main Tree']"; await percySnapshot(page, `Tree Pane w/ collapsed tree (theme: ${theme})`, { scope: treePane @@ -94,7 +94,7 @@ test.describe('Visual - Tree Pane', () => { * @param {string} name */ async function expandTreePaneItemByName(page, name) { - const treePane = page.locator('#tree-pane'); + const treePane = page.getByTestId('tree-pane'); const treeItem = treePane.locator(`role=treeitem[expanded=false][name=/${name}/]`); const expandTriangle = treeItem.locator('.c-disclosure-triangle'); await expandTriangle.click(); diff --git a/src/api/forms/components/controls/Locator.vue b/src/api/forms/components/controls/Locator.vue index 0e84911ba8a..339912cec8a 100644 --- a/src/api/forms/components/controls/Locator.vue +++ b/src/api/forms/components/controls/Locator.vue @@ -22,7 +22,6 @@