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
*/
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,

View File

@ -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);
});
});

View File

@ -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();

View File

@ -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)
});

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 }) => {
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)
});

View File

@ -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

View File

@ -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();

View File

@ -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();

View File

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

View File

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

View File

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

View File

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