From dda4b3f4f9250c01c0c06a60b21e16dc9c1d990c Mon Sep 17 00:00:00 2001 From: Jesse Mazzella Date: Thu, 17 Nov 2022 13:30:25 -0800 Subject: [PATCH 1/7] 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. --- src/styles/_controls.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/styles/_controls.scss b/src/styles/_controls.scss index 9a94c4c9802..c97e739faf9 100644 --- a/src/styles/_controls.scss +++ b/src/styles/_controls.scss @@ -270,9 +270,11 @@ button { flex: 0 0 auto; width: $d; position: relative; + visibility: hidden; &.is-enabled { cursor: pointer; + visibility: visible; &:hover { color: $colorDisclosureCtrlHov; From b37a02147ce157388f7ac537efd3c813e2649512 Mon Sep 17 00:00:00 2001 From: Jesse Mazzella Date: Thu, 17 Nov 2022 13:32:15 -0800 Subject: [PATCH 2/7] feat(e2e): Add AppAction to expand the entire tree --- e2e/appActions.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/e2e/appActions.js b/e2e/appActions.js index 50e56edbf04..acc213c672f 100644 --- a/e2e/appActions.js +++ b/e2e/appActions.js @@ -218,6 +218,20 @@ async function openObjectTreeContextMenu(page, url) { }); } +/** + * Expands the entire object tree (every expandable tree item). + * @param {import('@playwright/test').Page} page + */ +async function expandEntireTree(page) { + const treePane = page.locator('#tree-pane'); + const collapsedTreeItems = treePane.locator('role=treeitem[expanded=false]'); + let count = await collapsedTreeItems.count(); + while (count > 0) { + await collapsedTreeItems.first().locator('.c-disclosure-triangle').click(); + count = await collapsedTreeItems.count(); + } +} + /** * Gets the UUID of the currently focused object by parsing the current URL * and returning the last UUID in the path. @@ -362,6 +376,7 @@ module.exports = { createDomainObjectWithDefaults, createNotification, expandTreePaneItemByName, + expandEntireTree, createPlanFromJSON, openObjectTreeContextMenu, getHashUrlToDomainObject, From a175d3a1ea316817742b601dcb532bfb9e0ac312 Mon Sep 17 00:00:00 2001 From: Jesse Mazzella Date: Tue, 27 Dec 2022 17:37:20 -0800 Subject: [PATCH 3/7] fix: wait for loading indicator --- e2e/appActions.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/e2e/appActions.js b/e2e/appActions.js index acc213c672f..c695b3c6649 100644 --- a/e2e/appActions.js +++ b/e2e/appActions.js @@ -227,7 +227,10 @@ async function expandEntireTree(page) { const collapsedTreeItems = treePane.locator('role=treeitem[expanded=false]'); let count = await collapsedTreeItems.count(); while (count > 0) { - await collapsedTreeItems.first().locator('.c-disclosure-triangle').click(); + await collapsedTreeItems.locator('.c-disclosure-triangle.is-enabled').first().click(); + // Wait for the loading indicator to appear then disappear + await page.locator('.is-loading').isVisible(); + await page.locator('.is-loading').isHidden(); count = await collapsedTreeItems.count(); } } From f00024a92ed1b03d34c0876bce6dd13f947cdcb1 Mon Sep 17 00:00:00 2001 From: Jesse Mazzella Date: Tue, 27 Dec 2022 17:37:30 -0800 Subject: [PATCH 4/7] test: add test for `expandEntireTree` --- e2e/tests/framework/appActions.e2e.spec.js | 36 +++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/e2e/tests/framework/appActions.e2e.spec.js b/e2e/tests/framework/appActions.e2e.spec.js index 10ae0b11f89..871a85c67c3 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,38 @@ 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: 'Display Layout', + parent: folder2.uuid + }); + await createDomainObjectWithDefaults(page, { + type: 'Folder', + parent: folder2.uuid + }); + + await expandEntireTree(page); + const treePane = page.locator('#tree-pane'); + const collapsedTreeItems = treePane.locator('role=treeitem[expanded=false]'); + const count = await collapsedTreeItems.count(); + expect(count).toBe(0); + }); }); From ed68c5652c40a3f86562843175a650f239a9391c Mon Sep 17 00:00:00 2001 From: Jesse Mazzella Date: Wed, 25 Jan 2023 10:23:40 -0800 Subject: [PATCH 5/7] 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 --- e2e/appActions.js | 25 ++++++++------- e2e/tests/framework/appActions.e2e.spec.js | 27 +++++++++++++--- .../functional/moveAndLinkObjects.e2e.spec.js | 32 ++++++++++++------- .../displayLayout/displayLayout.e2e.spec.js | 16 +++++++--- .../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/ui/layout/Layout.vue | 4 +-- src/ui/layout/mct-tree.vue | 4 +++ 10 files changed, 82 insertions(+), 39 deletions(-) diff --git a/e2e/appActions.js b/e2e/appActions.js index c695b3c6649..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(); @@ -221,17 +223,18 @@ 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) { - const treePane = page.locator('#tree-pane'); - const collapsedTreeItems = treePane.locator('role=treeitem[expanded=false]'); - let count = await collapsedTreeItems.count(); - while (count > 0) { - await collapsedTreeItems.locator('.c-disclosure-triangle.is-enabled').first().click(); - // Wait for the loading indicator to appear then disappear - await page.locator('.is-loading').isVisible(); - await page.locator('.is-loading').isHidden(); - count = await collapsedTreeItems.count(); +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(); } } diff --git a/e2e/tests/framework/appActions.e2e.spec.js b/e2e/tests/framework/appActions.e2e.spec.js index 871a85c67c3..d0102f1300c 100644 --- a/e2e/tests/framework/appActions.e2e.spec.js +++ b/e2e/tests/framework/appActions.e2e.spec.js @@ -128,6 +128,10 @@ test.describe('AppActions', () => { type: 'Folder', parent: folder1.uuid }); + const folder3 = await createDomainObjectWithDefaults(page, { + type: 'Folder', + parent: folder1.uuid + }); await createDomainObjectWithDefaults(page, { type: 'Display Layout', parent: folder2.uuid @@ -137,10 +141,25 @@ test.describe('AppActions', () => { parent: folder2.uuid }); + await page.goto('./#/browse/mine'); await expandEntireTree(page); - const treePane = page.locator('#tree-pane'); - const collapsedTreeItems = treePane.locator('role=treeitem[expanded=false]'); - const count = await collapsedTreeItems.count(); - expect(count).toBe(0); + 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 a3fcc5342e5..5cbd0196914 100644 --- a/e2e/tests/functional/plugins/displayLayout/displayLayout.e2e.spec.js +++ b/e2e/tests/functional/plugins/displayLayout/displayLayout.e2e.spec.js @@ -48,7 +48,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) }); @@ -80,7 +82,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) }); @@ -116,7 +120,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) }); @@ -155,7 +161,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/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 @@