Make tree items more actionable and add AppAction for expanding the object tree (#5997)

* 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 <john.c.hill@nasa.gov>
This commit is contained in:
Jesse Mazzella 2023-01-26 09:25:15 -08:00 committed by GitHub
parent 01f724959d
commit 8d1a2e6716
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 132 additions and 29 deletions

View File

@ -144,7 +144,9 @@ async function createNotification(page, createNotificationOptions) {
* @param {string} name * @param {string} name
*/ */
async function expandTreePaneItemByName(page, 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 treeItem = treePane.locator(`role=treeitem[expanded=false][name=/${name}/]`);
const expandTriangle = treeItem.locator('.c-disclosure-triangle'); const expandTriangle = treeItem.locator('.c-disclosure-triangle');
await expandTriangle.click(); 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 * Gets the UUID of the currently focused object by parsing the current URL
* and returning the last UUID in the path. * and returning the last UUID in the path.
@ -362,6 +382,7 @@ module.exports = {
createDomainObjectWithDefaults, createDomainObjectWithDefaults,
createNotification, createNotification,
expandTreePaneItemByName, expandTreePaneItemByName,
expandEntireTree,
createPlanFromJSON, createPlanFromJSON,
openObjectTreeContextMenu, openObjectTreeContextMenu,
getHashUrlToDomainObject, getHashUrlToDomainObject,

View File

@ -21,7 +21,7 @@
*****************************************************************************/ *****************************************************************************/
const { test, expect } = require('../../pluginFixtures.js'); const { test, expect } = require('../../pluginFixtures.js');
const { createDomainObjectWithDefaults, createNotification } = require('../../appActions.js'); const { createDomainObjectWithDefaults, createNotification, expandEntireTree } = require('../../appActions.js');
test.describe('AppActions', () => { test.describe('AppActions', () => {
test('createDomainObjectsWithDefaults', async ({ page }) => { test('createDomainObjectsWithDefaults', async ({ page }) => {
@ -109,4 +109,57 @@ test.describe('AppActions', () => {
await expect(page.locator('.c-message-banner')).toHaveClass(/error/); await expect(page.locator('.c-message-banner')).toHaveClass(/error/);
await page.locator('[aria-label="Dismiss"]').click(); 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);
});
}); });

View File

@ -52,7 +52,9 @@ test.describe('Move & link item tests', () => {
// Attempt to move parent to its own grandparent // Attempt to move parent to its own grandparent
await page.locator('button[title="Show selected item in tree"]').click(); 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', { await treePane.getByRole('treeitem', {
name: 'Parent Folder' name: 'Parent Folder'
}).click({ }).click({
@ -63,28 +65,30 @@ test.describe('Move & link item tests', () => {
name: /Move/ name: /Move/
}).click(); }).click();
const locatorTree = page.locator('#locator-tree'); const createModalTree = page.getByRole('tree', {
const myItemsLocatorTreeItem = locatorTree.getByRole('treeitem', { name: "Create Modal Tree"
});
const myItemsLocatorTreeItem = createModalTree.getByRole('treeitem', {
name: myItemsFolderName name: myItemsFolderName
}); });
await myItemsLocatorTreeItem.locator('.c-disclosure-triangle').click(); await myItemsLocatorTreeItem.locator('.c-disclosure-triangle').click();
await myItemsLocatorTreeItem.click(); await myItemsLocatorTreeItem.click();
const parentFolderLocatorTreeItem = locatorTree.getByRole('treeitem', { const parentFolderLocatorTreeItem = createModalTree.getByRole('treeitem', {
name: parentFolder.name name: parentFolder.name
}); });
await parentFolderLocatorTreeItem.locator('.c-disclosure-triangle').click(); await parentFolderLocatorTreeItem.locator('.c-disclosure-triangle').click();
await parentFolderLocatorTreeItem.click(); await parentFolderLocatorTreeItem.click();
await expect(page.locator('[aria-label="Save"]')).toBeDisabled(); await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
const childFolderLocatorTreeItem = locatorTree.getByRole('treeitem', { const childFolderLocatorTreeItem = createModalTree.getByRole('treeitem', {
name: new RegExp(childFolder.name) name: new RegExp(childFolder.name)
}); });
await childFolderLocatorTreeItem.locator('.c-disclosure-triangle').click(); await childFolderLocatorTreeItem.locator('.c-disclosure-triangle').click();
await childFolderLocatorTreeItem.click(); await childFolderLocatorTreeItem.click();
await expect(page.locator('[aria-label="Save"]')).toBeDisabled(); await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
const grandchildFolderLocatorTreeItem = locatorTree.getByRole('treeitem', { const grandchildFolderLocatorTreeItem = createModalTree.getByRole('treeitem', {
name: grandchildFolder.name name: grandchildFolder.name
}); });
await grandchildFolderLocatorTreeItem.locator('.c-disclosure-triangle').click(); 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 // Attempt to move parent to its own grandparent
await page.locator('button[title="Show selected item in tree"]').click(); 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', { await treePane.getByRole('treeitem', {
name: 'Parent Folder' name: 'Parent Folder'
}).click({ }).click({
@ -206,28 +212,30 @@ test.describe('Move & link item tests', () => {
name: /Move/ name: /Move/
}).click(); }).click();
const locatorTree = page.locator('#locator-tree'); const createModalTree = page.getByRole('tree', {
const myItemsLocatorTreeItem = locatorTree.getByRole('treeitem', { name: "Create Modal Tree"
});
const myItemsLocatorTreeItem = createModalTree.getByRole('treeitem', {
name: myItemsFolderName name: myItemsFolderName
}); });
await myItemsLocatorTreeItem.locator('.c-disclosure-triangle').click(); await myItemsLocatorTreeItem.locator('.c-disclosure-triangle').click();
await myItemsLocatorTreeItem.click(); await myItemsLocatorTreeItem.click();
const parentFolderLocatorTreeItem = locatorTree.getByRole('treeitem', { const parentFolderLocatorTreeItem = createModalTree.getByRole('treeitem', {
name: parentFolder.name name: parentFolder.name
}); });
await parentFolderLocatorTreeItem.locator('.c-disclosure-triangle').click(); await parentFolderLocatorTreeItem.locator('.c-disclosure-triangle').click();
await parentFolderLocatorTreeItem.click(); await parentFolderLocatorTreeItem.click();
await expect(page.locator('[aria-label="Save"]')).toBeDisabled(); await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
const childFolderLocatorTreeItem = locatorTree.getByRole('treeitem', { const childFolderLocatorTreeItem = createModalTree.getByRole('treeitem', {
name: new RegExp(childFolder.name) name: new RegExp(childFolder.name)
}); });
await childFolderLocatorTreeItem.locator('.c-disclosure-triangle').click(); await childFolderLocatorTreeItem.locator('.c-disclosure-triangle').click();
await childFolderLocatorTreeItem.click(); await childFolderLocatorTreeItem.click();
await expect(page.locator('[aria-label="Save"]')).toBeDisabled(); await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
const grandchildFolderLocatorTreeItem = locatorTree.getByRole('treeitem', { const grandchildFolderLocatorTreeItem = createModalTree.getByRole('treeitem', {
name: grandchildFolder.name name: grandchildFolder.name
}); });
await grandchildFolderLocatorTreeItem.locator('.c-disclosure-triangle').click(); await grandchildFolderLocatorTreeItem.locator('.c-disclosure-triangle').click();

View File

@ -47,7 +47,9 @@ test.describe('Display Layout', () => {
// Expand the 'My Items' folder in the left tree // Expand the 'My Items' folder in the left tree
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click(); await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();
// Add the Sine Wave Generator to the Display Layout and save changes // 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', { const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
name: new RegExp(sineWaveObject.name) name: new RegExp(sineWaveObject.name)
}); });
@ -79,7 +81,9 @@ test.describe('Display Layout', () => {
// Expand the 'My Items' folder in the left tree // Expand the 'My Items' folder in the left tree
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click(); await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();
// Add the Sine Wave Generator to the Display Layout and save changes // 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', { const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
name: new RegExp(sineWaveObject.name) name: new RegExp(sineWaveObject.name)
}); });
@ -115,7 +119,9 @@ test.describe('Display Layout', () => {
// Expand the 'My Items' folder in the left tree // Expand the 'My Items' folder in the left tree
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click(); await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();
// Add the Sine Wave Generator to the Display Layout and save changes // 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', { const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
name: new RegExp(sineWaveObject.name) name: new RegExp(sineWaveObject.name)
}); });
@ -153,7 +159,9 @@ test.describe('Display Layout', () => {
// Expand the 'My Items' folder in the left tree // Expand the 'My Items' folder in the left tree
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click(); await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();
// Add the Sine Wave Generator to the Display Layout and save changes // 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', { const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
name: new RegExp(sineWaveObject.name) name: new RegExp(sineWaveObject.name)
}); });

View File

@ -40,7 +40,9 @@ test.describe('Flexible Layout', () => {
}); });
}); });
test('panes have the appropriate draggable attribute while in Edit and Browse modes', async ({ page }) => { 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', { const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
name: new RegExp(sineWaveObject.name) name: new RegExp(sineWaveObject.name)
}); });
@ -70,7 +72,9 @@ test.describe('Flexible Layout', () => {
await expect(dragWrapper).toHaveAttribute('draggable', 'false'); 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 }) => { 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', { const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
name: new RegExp(sineWaveObject.name) name: new RegExp(sineWaveObject.name)
}); });
@ -106,7 +110,9 @@ test.describe('Flexible Layout', () => {
type: 'issue', type: 'issue',
description: 'https://github.com/nasa/openmct/issues/3117' 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', { const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
name: new RegExp(sineWaveObject.name) name: new RegExp(sineWaveObject.name)
}); });

View File

@ -198,7 +198,9 @@ test.describe('Tagging in Notebooks @addInit', () => {
page.click('.c-disclosure-triangle') page.click('.c-disclosure-triangle')
]); ]);
const treePane = page.locator('#tree-pane'); const treePane = page.getByRole('tree', {
name: 'Main Tree'
});
// Click Clock // Click Clock
await treePane.getByRole('treeitem', { await treePane.getByRole('treeitem', {
name: clock.name name: clock.name

View File

@ -116,7 +116,9 @@ async function getAndAssertTreeItems(page, expected) {
* @param {string} name * @param {string} name
*/ */
async function expandTreePaneItemByName(page, 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 treeItem = treePane.locator(`role=treeitem[expanded=false][name=/${name}/]`);
const expandTriangle = treeItem.locator('.c-disclosure-triangle'); const expandTriangle = treeItem.locator('.c-disclosure-triangle');
await expandTriangle.click(); await expandTriangle.click();

View File

@ -57,7 +57,7 @@ test.describe('Visual - Tree Pane', () => {
name: 'Z Clock' 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})`, { await percySnapshot(page, `Tree Pane w/ collapsed tree (theme: ${theme})`, {
scope: treePane scope: treePane
@ -94,7 +94,7 @@ test.describe('Visual - Tree Pane', () => {
* @param {string} name * @param {string} name
*/ */
async function expandTreePaneItemByName(page, 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 treeItem = treePane.locator(`role=treeitem[expanded=false][name=/${name}/]`);
const expandTriangle = treeItem.locator('.c-disclosure-triangle'); const expandTriangle = treeItem.locator('.c-disclosure-triangle');
await expandTriangle.click(); await expandTriangle.click();

View File

@ -22,7 +22,6 @@
<template> <template>
<mct-tree <mct-tree
id="locator-tree"
:is-selector-tree="true" :is-selector-tree="true"
:initial-selection="model.parent" :initial-selection="model.parent"
@tree-item-selection="handleItemSelection" @tree-item-selection="handleItemSelection"

View File

@ -270,9 +270,11 @@ button {
flex: 0 0 auto; flex: 0 0 auto;
width: $d; width: $d;
position: relative; position: relative;
visibility: hidden;
&.is-enabled { &.is-enabled {
cursor: pointer; cursor: pointer;
visibility: visible;
&:hover { &:hover {
color: $colorDisclosureCtrlHov; color: $colorDisclosureCtrlHov;

View File

@ -79,9 +79,7 @@
<multipane <multipane
type="vertical" type="vertical"
> >
<pane <pane>
id="tree-pane"
>
<mct-tree <mct-tree
ref="mctTree" ref="mctTree"
:sync-tree-navigation="triggerSync" :sync-tree-navigation="triggerSync"

View File

@ -41,6 +41,7 @@
ref="mainTree" ref="mainTree"
class="c-tree-and-search__tree c-tree" class="c-tree-and-search__tree c-tree"
role="tree" role="tree"
:aria-label="getAriaLabel"
aria-expanded="true" aria-expanded="true"
> >
@ -192,6 +193,9 @@ export default {
focusedItems() { focusedItems() {
return this.activeSearch ? this.searchResultItems : this.treeItems; return this.activeSearch ? this.searchResultItems : this.treeItems;
}, },
getAriaLabel() {
return this.isSelectorTree ? "Create Modal Tree" : "Main Tree";
},
pageThreshold() { pageThreshold() {
return Math.ceil(this.mainTreeHeight / this.itemHeight) + ITEM_BUFFER; return Math.ceil(this.mainTreeHeight / this.itemHeight) + ITEM_BUFFER;
}, },