diff --git a/e2e/tests/functional/recentObjects.e2e.spec.js b/e2e/tests/functional/recentObjects.e2e.spec.js index cd03ce72b1..164b4ff3f7 100644 --- a/e2e/tests/functional/recentObjects.e2e.spec.js +++ b/e2e/tests/functional/recentObjects.e2e.spec.js @@ -22,10 +22,14 @@ const { test, expect } = require('../../pluginFixtures.js'); const { createDomainObjectWithDefaults } = require('../../appActions.js'); +const { waitForAnimations } = require('../../baseFixtures.js'); test.describe('Recent Objects', () => { + /** @type {import('@playwright/test').Locator} */ let recentObjectsList; + /** @type {import('@playwright/test').Locator} */ let clock; + /** @type {import('@playwright/test').Locator} */ let folderA; test.beforeEach(async ({ page }) => { await page.goto('./', { waitUntil: 'networkidle' }); @@ -45,19 +49,16 @@ test.describe('Recent Objects', () => { }); // Drag the Recent Objects panel up a bit - await page.locator('div:nth-child(2) > .l-pane__handle').hover(); + await page.locator('.l-pane.l-pane--vertical-handle-before', { + hasText: 'Recently Viewed' + }).locator('.l-pane__handle').hover(); await page.mouse.down(); await page.mouse.move(0, 100); await page.mouse.up(); }); - test('Recent Objects CRUD operations', async ({ page }) => { + test('Navigated objects show up in recents, object renames and deletions are reflected', async ({ page }) => { // Verify that both created objects appear in the list and are in the correct order - expect(recentObjectsList.getByRole('listitem', { name: clock.name })).toBeTruthy(); - expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeTruthy(); - expect(recentObjectsList.getByRole('listitem', { name: clock.name }).locator('a').getByText(folderA.name)).toBeTruthy(); - expect(recentObjectsList.getByRole('listitem').nth(0).getByText(clock.name)).toBeTruthy(); - expect(recentObjectsList.getByRole('listitem', { name: clock.name }).locator('a').getByText(folderA.name)).toBeTruthy(); - expect(recentObjectsList.getByRole('listitem').nth(1).getByText(folderA.name)).toBeTruthy(); + assertInitialRecentObjectsListState(); // Navigate to the folder by clicking on the main object name in the recent objects list item await page.getByRole('listitem', { name: folderA.name }).getByText(folderA.name).click(); @@ -72,7 +73,7 @@ test.describe('Recent Objects', () => { // Verify rename has been applied in recent objects list item and objects paths expect(await page.getByRole('navigation', { - name: `${clock.name} Breadcrumb` + name: clock.name }).locator('a').filter({ hasText: folderA.name }).count()).toBeGreaterThan(0); @@ -102,31 +103,153 @@ test.describe('Recent Objects', () => { // Navigate to the folder by clicking on its entry in the Clock's breadcrumb const waitForFolderNavigation = page.waitForURL(`**/${folderA.uuid}?*`); await page.getByRole('navigation', { - name: `${clock.name} Breadcrumb` + name: clock.name }).locator('a').filter({ hasText: folderA.name }).click(); // Verify that the hash URL updates correctly await waitForFolderNavigation; - // eslint-disable-next-line no-useless-escape - expect(page.url()).toMatch(new RegExp(`.*${folderA.uuid}\?.*`)); + expect(page.url()).toMatch(new RegExp(`.*${folderA.uuid}?.*`)); // Navigate to My Items by clicking on its entry in the Clock's breadcrumb const waitForMyItemsNavigation = page.waitForURL(`**/mine?*`); await page.getByRole('navigation', { - name: `${clock.name} Breadcrumb` + name: clock.name }).locator('a').filter({ hasText: myItemsFolderName }).click(); // Verify that the hash URL updates correctly await waitForMyItemsNavigation; - // eslint-disable-next-line no-useless-escape - expect(page.url()).toMatch(new RegExp(`.*mine\?.*`)); + expect(page.url()).toMatch(new RegExp(`.*mine?.*`)); }); - test.fixme("Clicking on the 'target button' scrolls the object into view in the tree and highlights it", async ({ page }) => { + test("Clicking on the 'target button' scrolls the object into view in the tree and highlights it", async ({ page }) => { + const clockTreeItem = page.getByRole('tree', { name: 'Main Tree'}).getByRole('treeitem', { name: clock.name }); + const folderTreeItem = page.getByRole('tree', { name: 'Main Tree'}) + .getByRole('treeitem', { + name: folderA.name, + expanded: true + }); + + // Click the "Target" button for the Clock which is nested in a folder + await page.getByRole('button', { name: `Open and scroll to ${clock.name}`}).click(); + + // Assert that the Clock parent folder has expanded and the Clock is visible) + await expect(folderTreeItem.locator('.c-disclosure-triangle')).toHaveClass(/--expanded/); + await expect(clockTreeItem).toBeVisible(); + + // Assert that the Clock treeitem is highlighted + await expect(clockTreeItem.locator('.c-tree__item')).toHaveClass(/is-targeted-item/); + + // Wait for highlight animation to end + await waitForAnimations(clockTreeItem.locator('.c-tree__item')); + + // Assert that the Clock treeitem is no longer highlighted + await expect(clockTreeItem.locator('.c-tree__item')).not.toHaveClass(/is-targeted-item/); }); - test.fixme("Tests for context menu actions from recent objects", async ({ page }) => { + test("Persists on refresh", async ({ page }) => { + assertInitialRecentObjectsListState(); + await page.reload(); + assertInitialRecentObjectsListState(); }); + test("Displays objects and aliases uniquely", async ({ page }) => { + const mainTree = page.getByRole('tree', { name: 'Main Tree'}); + + // Navigate to the clock and reveal it in the tree + await page.goto(clock.url); + await page.getByTitle('Show selected item in tree').click(); + + // Right click the clock and create an alias using the "link" context menu action + const clockTreeItem = page.getByRole('tree', { + name: 'Main Tree' + }).getByRole('treeitem', { + name: clock.name + }); + await clockTreeItem.click({ + button: 'right' + }); + await page.getByRole('menuitem', { + name: /Create Link/ + }).click(); + await page.getByRole('tree', { name: 'Create Modal Tree'}).getByRole('treeitem').first().click(); + await page.getByRole('button', { name: 'Save' }).click(); + + // Click the newly created object alias in the tree + await mainTree.getByRole('treeitem', { + name: new RegExp(clock.name) + }).filter({ + has: page.locator('.is-alias') + }).click(); + + // Assert that two recent objects are displayed and one of them is an alias + expect(await recentObjectsList.getByRole('listitem', { name: clock.name }).count()).toBe(2); + expect(await recentObjectsList.locator('.is-alias').count()).toBe(1); + + // Assert that the alias and the original's breadcrumbs are different + const clockBreadcrumbs = recentObjectsList.getByRole('listitem', {name: clock.name}).getByRole('navigation'); + expect(await clockBreadcrumbs.count()).toBe(2); + expect(await clockBreadcrumbs.nth(0).innerText()).not.toEqual(await clockBreadcrumbs.nth(1).innerText()); + }); + test("Enforces a limit of 20 recent objects", async ({ page }) => { + // Creating 21 objects takes a while, so increase the timeout + test.slow(); + + // Assert that the list initially contains 3 objects (clock, folder, my items) + expect(await recentObjectsList.locator('.c-recentobjects-listitem').count()).toBe(3); + + let lastFolder; + let lastClock; + // Create 19 more objects (3 in beforeEach() + 18 new = 21 total) + for (let i = 0; i < 9; i++) { + lastFolder = await createDomainObjectWithDefaults(page, { + type: "Folder", + parent: lastFolder?.uuid + }); + lastClock = await createDomainObjectWithDefaults(page, { + type: "Clock", + parent: lastFolder?.uuid + }); + } + + // Assert that the list contains 20 objects + expect(await recentObjectsList.locator('.c-recentobjects-listitem').count()).toBe(20); + + // Collapse the tree + await page.getByTitle("Collapse all tree items").click(); + const lastFolderTreeItem = page.getByRole('tree', { name: 'Main Tree'}) + .getByRole('treeitem', { + name: lastFolder.name, + expanded: true + }); + const lastClockTreeItem = page.getByRole('tree', { name: 'Main Tree'}) + .getByRole('treeitem', { + name: lastClock.name + }); + + // Test "Open and Scroll To" in a deeply nested tree, while we're here + await page.getByRole('button', { name: `Open and scroll to ${lastClock.name}`}).click(); + + // Assert that the Clock parent folder has expanded and the Clock is visible) + await expect(lastFolderTreeItem.locator('.c-disclosure-triangle')).toHaveClass(/--expanded/); + await expect(lastClockTreeItem).toBeVisible(); + + // Assert that the Clock treeitem is highlighted + await expect(lastClockTreeItem.locator('.c-tree__item')).toHaveClass(/is-targeted-item/); + + // Wait for highlight animation to end + await waitForAnimations(lastClockTreeItem.locator('.c-tree__item')); + + // Assert that the Clock treeitem is no longer highlighted + await expect(lastClockTreeItem.locator('.c-tree__item')).not.toHaveClass(/is-targeted-item/); + }); + + function assertInitialRecentObjectsListState() { + expect(recentObjectsList.getByRole('listitem', { name: clock.name })).toBeTruthy(); + expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeTruthy(); + expect(recentObjectsList.getByRole('listitem', { name: clock.name }).locator('a').getByText(folderA.name)).toBeTruthy(); + expect(recentObjectsList.getByRole('listitem').nth(0).getByText(clock.name)).toBeTruthy(); + expect(recentObjectsList.getByRole('listitem', { name: clock.name }).locator('a').getByText(folderA.name)).toBeTruthy(); + expect(recentObjectsList.getByRole('listitem').nth(1).getByText(folderA.name)).toBeTruthy(); + } }); diff --git a/src/ui/components/ObjectPath.vue b/src/ui/components/ObjectPath.vue index eab36d43bd..088ca490f4 100644 --- a/src/ui/components/ObjectPath.vue +++ b/src/ui/components/ObjectPath.vue @@ -24,7 +24,7 @@