From dad96994842c9dc1a018f1f901254062caa078f1 Mon Sep 17 00:00:00 2001 From: Nikola Anachkov Date: Wed, 9 Jul 2025 15:51:47 +0300 Subject: [PATCH 1/3] chore(ui5-tree): migrate wdio tests to cypress --- packages/main/cypress/specs/Tree.cy.tsx | 1024 +++++++++++++++++++++++ packages/main/test/specs/Tree.spec.js | 342 -------- 2 files changed, 1024 insertions(+), 342 deletions(-) delete mode 100644 packages/main/test/specs/Tree.spec.js diff --git a/packages/main/cypress/specs/Tree.cy.tsx b/packages/main/cypress/specs/Tree.cy.tsx index 6e62fdba897b..326d4592be98 100644 --- a/packages/main/cypress/specs/Tree.cy.tsx +++ b/packages/main/cypress/specs/Tree.cy.tsx @@ -3,6 +3,12 @@ import "../../src/TreeItem.js"; import TreeItem from "../../src/TreeItem.js"; import Icon from "../../src/Icon.js"; import bell from "@ui5/webcomponents-icons/dist/bell.js"; +import { setAnimationMode } from "@ui5/webcomponents-base/dist/config/AnimationMode.js"; +import TreeItemCustom from "../../src/TreeItemCustom.js"; +import Button from "../../src/Button.js"; +import Option from "../../src/Option.js"; +import Select from "../../src/Select.js"; + describe("Tree Tests", () => { it("tests accessibility properties forwarded to the list", () => { @@ -70,3 +76,1021 @@ describe("Tree Props", () => { .should("exist") }); }); + +before(() => { + cy.wrap({ setAnimationMode }) + .then(api => { + return api.setAnimationMode("none"); + }); +}) + +const getVisibleTreeItems = (treeSelector = "[ui5-tree]") => { + return cy.get(`${treeSelector} [ui5-tree-item]`).filter(":visible"); +}; + +const getVisibleTreeItemsCount = (treeSelector = "[ui5-tree]") => { + return getVisibleTreeItems(treeSelector).then($items => $items.length); +}; + +describe("Tree general interaction", () => { + it("Tree is rendered", () => { + cy.mount( + + + + + + + + + + + + + + + + + + + + + ); + + cy.get("[ui5-tree]") + .shadow() + .find("[ui5-tree-list]") + .should("exist"); + }); + + it("Tree items can be collapsed", () => { + cy.mount( + + + + + + + + + + ); + + getVisibleTreeItemsCount().then(initialCount => { + cy.get("[ui5-tree-item][expanded]") + .shadow() + .find(".ui5-li-tree-toggle-icon") + .click(); + + getVisibleTreeItemsCount().should("be.lessThan", initialCount); + }); + }); + + it("Tree items can be expanded", () => { + cy.mount( + + + + + + + + + + ); + + getVisibleTreeItemsCount().then(initialCount => { + cy.get("[ui5-tree-item]:first") + .shadow() + .find(".ui5-li-tree-toggle-icon") + .click(); + + getVisibleTreeItemsCount().should("be.greaterThan", initialCount); + }); + }); + + it("keyboard handling on F2", () => { + cy.mount( + + + + + + + + + + + + + + ); + + cy.get("[ui5-tree-item-custom].item").should("exist"); + cy.get(".itemBtn").should("exist"); + + cy.get("[ui5-tree-item-custom].item").click(); + + cy.get("[ui5-tree-item-custom].item").should("be.focused"); + + cy.get("[ui5-tree-item-custom].item").realPress("F2"); + + cy.get(".itemBtn").should("be.focused"); + + cy.get(".itemBtn").realPress("F2"); + + cy.get("[ui5-tree-item-custom].item").should("be.focused"); + }); +}); + +describe("Tree proxies properties to list", () => { + it("Mouseover/mouseout events", () => { + cy.mount( + <> + + + + + + + + + + + + + ); + + cy.window().then((win) => { + let mouseoverCount = 0; + let mouseoutCount = 0; + + cy.get("[ui5-tree]").then(($tree) => { + $tree[0].addEventListener("ui5-item-mouseover", () => { + mouseoverCount++; + cy.get("#mouseover-counter").invoke("val", mouseoverCount.toString()); + }); + + $tree[0].addEventListener("ui5-item-mouseout", () => { + mouseoutCount++; + cy.get("#mouseout-counter").invoke("val", mouseoutCount.toString()); + }); + }); + }); + + cy.get("[ui5-tree-item]:first") + .shadow() + .find(".ui5-li-root-tree") + .realHover(); + + cy.get("#mouseover-counter") + .should("have.value", "1"); + + cy.get("[ui5-tree-item]:eq(1)") + .shadow() + .find(".ui5-li-root-tree") + .realHover(); + + cy.get("#mouseover-counter") + .should("have.value", "2"); + + cy.get("#mouseout-counter") + .should("have.value", "1"); + }); + + it("SelectionMode works", () => { + cy.mount( + + + + + + + + + + + + + ); + + cy.get("[ui5-tree]") + .shadow() + .find("[ui5-tree-list]") + .should("have.attr", "selection-mode", "Multiple"); + + cy.get("[ui5-tree-item]:first") + .should("have.attr", "_selection-mode", "Multiple"); + + const modes = ["None", "Single", "SingleStart", "SingleEnd", "Multiple", "Delete"]; + modes.forEach(selectionMode => { + cy.get("[ui5-tree]") + .invoke("attr", "selection-mode", selectionMode); + + cy.get("[ui5-tree]") + .shadow() + .find("[ui5-tree-list]") + .should("have.attr", "selection-mode", selectionMode); + }); + }); + + it("SelectionMode works recursively", () => { + cy.mount( + + + + + + + + + + ); + + cy.get(".lastItem") + .should("have.attr", "_selection-mode", "Multiple"); + }); + + it("headerText, footerText, noDataText work", () => { + cy.mount( + + + + ); + + cy.get("[ui5-tree]") + .invoke("attr", "header-text", "header text"); + + cy.get("[ui5-tree]") + .shadow() + .find("[ui5-tree-list]") + .should("have.attr", "header-text", "header text"); + + cy.get("[ui5-tree]") + .invoke("attr", "footer-text", "footer text"); + + cy.get("[ui5-tree]") + .shadow() + .find("[ui5-tree-list]") + .should("have.attr", "footer-text", "footer text"); + + cy.get("[ui5-tree]") + .invoke("attr", "no-data-text", "no data text"); + + cy.get("[ui5-tree]") + .shadow() + .find("[ui5-tree-list]") + .should("have.attr", "no-data-text", "no data text"); + }); + + it("Tests the prevention of the ui5-itemClick event", () => { + cy.mount( + + + + + + ); + + cy.get("[ui5-tree]").then(($tree) => { + $tree[0].addEventListener("ui5-item-click", (event) => { + event.preventDefault(); + }); + }); + + cy.get("[ui5-tree-item]:first") + .click() + .should("not.have.attr", "selected"); + }); + + it("selectionChange event provides targetItem parameter", () => { + cy.mount( + <> + + + + + + + + + ); + + cy.get("[ui5-tree]").then(($tree) => { + $tree[0].addEventListener("ui5-selection-change", (event: CustomEvent) => { + cy.get("#selectionChangeTargetItemResult").invoke("val", event.detail.targetItem.id || "no-id"); + }); + }); + + cy.get("[ui5-tree-item]:first") + .invoke("attr", "id", "item1") + .click(); + + cy.get("#selectionChangeTargetItemResult") + .should("have.value", "item1"); + }); +}); + +describe("Tree has screen reader support", () => { + it("List role is correct", () => { + cy.mount( + + + + + + + + + + ); + + cy.get("[ui5-tree]") + .shadow() + .find("[ui5-tree-list]") + .shadow() + .find("ul") + .should("have.attr", "role", "tree"); + }); + + it("List item acc attributes correct", () => { + cy.mount( + + + + + + + + + + ); + + cy.get("[ui5-tree] [ui5-tree-item]").each(($item) => { + cy.wrap($item) + .shadow() + .find("li") + .should("have.attr", "role", "treeitem"); + + cy.wrap($item) + .invoke("attr", "level") + .then((level) => { + cy.wrap($item) + .shadow() + .find("li") + .should("have.attr", "aria-level", level); + }); + + cy.wrap($item) + .invoke("prop", "showToggleButton") + .then((showToggleButton) => { + cy.wrap($item) + .invoke("prop", "expanded") + .then((expanded) => { + const ariaExpandedValues = { + "true": { + "true": "true", + "false": "false", + }, + "false": { + "true": null, + "false": null, + } + }; + + const expectedValue = ariaExpandedValues[showToggleButton.toString()][expanded.toString()]; + + if (expectedValue === null) { + cy.wrap($item) + .shadow() + .find("li") + .should("not.have.attr", "aria-expanded"); + } else { + cy.wrap($item) + .shadow() + .find("li") + .should("have.attr", "aria-expanded", expectedValue); + } + }); + }); + }); + }); +}); + +describe("Tree slots", () => { + it("items slot", () => { + cy.mount( + <> + + + + + + + + + + + + ); + + cy.get("[ui5-tree-item]:first") + .find("[ui5-tree-item]:first") + .as("treeItem"); + + cy.get("@treeItem") + .invoke("prop", "items") + .should("have.length", 1); + + cy.get("#btn").then(($btn) => { + $btn[0].addEventListener("click", () => { + cy.get("@treeItem").then(($treeItem) => { + const newTreeItem = document.createElement("ui5-tree-item") as any; + const currentCount = $treeItem[0].querySelectorAll("[ui5-tree-item]").length; + newTreeItem.text = `1-1-${currentCount + 1}`; + $treeItem[0].appendChild(newTreeItem); + }); + }); + }); + + cy.get("#btn").click(); + + cy.get("@treeItem") + .invoke("prop", "items") + .should("have.length", 2); + + cy.get("@treeItem") + .find("[ui5-tree-item]:last-child") + .should("have.prop", "text", "1-1-2") + .and("have.prop", "level", 3); + }); +}); + +describe("Tree drag and drop tests", () => { + it("Moving item After another", () => { + cy.mount( + + + + + + + + + + + + + + + + + + + + + + + ); + + // Add drag and drop event handlers + cy.get("[ui5-tree]").then(($tree) => { + const tree = $tree[0]; + + tree.addEventListener("ui5-move-over", (e: CustomEvent) => { + const { destination, source } = e.detail; + if (!tree.contains(source.element)) { + return; + } + // Allow "After" placement for this test + if (destination.placement === "Before") { + e.preventDefault(); + } + }); + + tree.addEventListener("ui5-move", (e: CustomEvent) => { + const { destination, source } = e.detail; + switch (destination.placement) { + case "Before": + destination.element.before(source.element); + break; + case "After": + destination.element.after(source.element); + break; + case "On": + destination.element.prepend(source.element); + break; + } + }); + }); + + // Use trigger to simulate drag and drop events instead of realMouse actions + cy.get("[ui5-tree] > [ui5-tree-item]").eq(0).as("firstItem"); + cy.get("[ui5-tree] > [ui5-tree-item]").eq(1).as("secondItem"); + cy.get("[ui5-tree] > [ui5-tree-item]").eq(2).as("thirdItem"); + + // Simulate moving first item after second item by triggering the move event directly + cy.get("@firstItem").then(($first) => { + cy.get("@secondItem").then(($second) => { + const moveEvent = new CustomEvent("ui5-move", { + detail: { + source: { element: $first[0] }, + destination: { element: $second[0], placement: "After" } + } + }); + cy.get("[ui5-tree]").then(($tree) => { + $tree[0].dispatchEvent(moveEvent); + }); + }); + }); + + // Verify new order: second, first, third + cy.get("[ui5-tree] > [ui5-tree-item]").eq(0).should("have.attr", "text", "Tree 2 ALLOWS NESTING"); + cy.get("[ui5-tree] > [ui5-tree-item]").eq(1).should("have.attr", "text", "Tree 1"); + cy.get("[ui5-tree] > [ui5-tree-item]").eq(2).should("have.attr", "text", "Tree 3 (no icon)"); + + // Move first item after third item + cy.get("[ui5-tree] > [ui5-tree-item]").eq(1).then(($first) => { + cy.get("[ui5-tree] > [ui5-tree-item]").eq(2).then(($third) => { + const moveEvent = new CustomEvent("ui5-move", { + detail: { + source: { element: $first[0] }, + destination: { element: $third[0], placement: "After" } + } + }); + cy.get("[ui5-tree]").then(($tree) => { + $tree[0].dispatchEvent(moveEvent); + }); + }); + }); + + // Verify final order: second, third, first + cy.get("[ui5-tree] > [ui5-tree-item]").eq(0).should("have.attr", "text", "Tree 2 ALLOWS NESTING"); + cy.get("[ui5-tree] > [ui5-tree-item]").eq(1).should("have.attr", "text", "Tree 3 (no icon)"); + cy.get("[ui5-tree] > [ui5-tree-item]").eq(2).should("have.attr", "text", "Tree 1"); + }); + + it("Moving item Before another", () => { + cy.mount( + + + + + + + + + + + + + + + + + + + + + + + ); + + // Add drag and drop event handlers + cy.get("[ui5-tree]").then(($tree) => { + const tree = $tree[0]; + + tree.addEventListener("ui5-move-over", (e: CustomEvent) => { + const { destination, source } = e.detail; + if (!tree.contains(source.element)) { + return; + } + // Allow "Before" placement for this test + if (destination.placement === "After") { + e.preventDefault(); + } + }); + + tree.addEventListener("ui5-move", (e: CustomEvent) => { + const { destination, source } = e.detail; + switch (destination.placement) { + case "Before": + destination.element.before(source.element); + break; + case "After": + destination.element.after(source.element); + break; + case "On": + destination.element.prepend(source.element); + break; + } + }); + }); + + // Get tree items - note the order matches WDIO test: [secondItem, thirdItem, firstItem] + cy.get("[ui5-tree] > [ui5-tree-item]").eq(1).as("secondItem"); + cy.get("[ui5-tree] > [ui5-tree-item]").eq(2).as("thirdItem"); + cy.get("[ui5-tree] > [ui5-tree-item]").eq(0).as("firstItem"); + + // Move first item before third item + cy.get("@firstItem").then(($first) => { + cy.get("@thirdItem").then(($third) => { + const moveEvent = new CustomEvent("ui5-move", { + detail: { + source: { element: $first[0] }, + destination: { element: $third[0], placement: "Before" } + } + }); + cy.get("[ui5-tree]").then(($tree) => { + $tree[0].dispatchEvent(moveEvent); + }); + }); + }); + + // Verify new order: second, first, third + cy.get("[ui5-tree] > [ui5-tree-item]").eq(0).should("have.attr", "text", "Tree 2 ALLOWS NESTING"); + cy.get("[ui5-tree] > [ui5-tree-item]").eq(1).should("have.attr", "text", "Tree 1"); + cy.get("[ui5-tree] > [ui5-tree-item]").eq(2).should("have.attr", "text", "Tree 3 (no icon)"); + + // Move first item before second item + cy.get("[ui5-tree] > [ui5-tree-item]").eq(1).then(($first) => { + cy.get("[ui5-tree] > [ui5-tree-item]").eq(0).then(($second) => { + const moveEvent = new CustomEvent("ui5-move", { + detail: { + source: { element: $first[0] }, + destination: { element: $second[0], placement: "Before" } + } + }); + cy.get("[ui5-tree]").then(($tree) => { + $tree[0].dispatchEvent(moveEvent); + }); + }); + }); + + // Verify final order: first, second, third + cy.get("[ui5-tree] > [ui5-tree-item]").eq(0).should("have.attr", "text", "Tree 1"); + cy.get("[ui5-tree] > [ui5-tree-item]").eq(1).should("have.attr", "text", "Tree 2 ALLOWS NESTING"); + cy.get("[ui5-tree] > [ui5-tree-item]").eq(2).should("have.attr", "text", "Tree 3 (no icon)"); + }); + + it("Moving item ON another", () => { + cy.mount( + + + + + + + + + + + + + + + + + + + + + + + ); + + // Add drag and drop event handlers + cy.get("[ui5-tree]").then(($tree) => { + const tree = $tree[0]; + + tree.addEventListener("ui5-move-over", (e: CustomEvent) => { + const { destination, source } = e.detail; + if (!tree.contains(source.element)) { + return; + } + // Allow "On" placement only for elements with data-allows-nesting + if (destination.placement === "On" && !("allowsNesting" in destination.element.dataset)) { + return; + } + e.preventDefault(); + }); + + tree.addEventListener("ui5-move", (e: CustomEvent) => { + const { destination, source } = e.detail; + + // Prevent moving element onto itself + if (source.element === destination.element) { + return; + } + + switch (destination.placement) { + case "Before": + destination.element.before(source.element); + break; + case "After": + destination.element.after(source.element); + break; + case "On": + destination.element.prepend(source.element); + break; + } + }); + }); + + // Get tree items + cy.get("[ui5-tree] > [ui5-tree-item]").eq(0).as("firstItem"); + cy.get("[ui5-tree] > [ui5-tree-item]").eq(1).as("secondItem"); + cy.get("[ui5-tree] > [ui5-tree-item]").eq(2).as("thirdItem"); + + // Test 1: Try to move first item ON itself (should not change order) + cy.get("@firstItem").then(($first) => { + const moveEvent = new CustomEvent("ui5-move", { + detail: { + source: { element: $first[0] }, + destination: { element: $first[0], placement: "On" } + } + }); + cy.get("[ui5-tree]").then(($tree) => { + $tree[0].dispatchEvent(moveEvent); + }); + }); + + // Verify order has NOT changed + cy.get("[ui5-tree] > [ui5-tree-item]").should("have.length", 3); + cy.get("[ui5-tree] > [ui5-tree-item]").eq(0).should("have.attr", "text", "Tree 1"); + cy.get("[ui5-tree] > [ui5-tree-item]").eq(1).should("have.attr", "text", "Tree 2 ALLOWS NESTING"); + cy.get("[ui5-tree] > [ui5-tree-item]").eq(2).should("have.attr", "text", "Tree 3 (no icon)"); + + // Test 2: Move first item ON second item (nesting - should work) + cy.get("@firstItem").then(($first) => { + cy.get("@secondItem").then(($second) => { + const moveEvent = new CustomEvent("ui5-move", { + detail: { + source: { element: $first[0] }, + destination: { element: $second[0], placement: "On" } + } + }); + cy.get("[ui5-tree]").then(($tree) => { + $tree[0].dispatchEvent(moveEvent); + }); + }); + }); + + // Verify first item is nested in second (only 2 items at root level) + cy.get("[ui5-tree] > [ui5-tree-item]").should("have.length", 2); + cy.get("[ui5-tree] > [ui5-tree-item]").eq(0).should("have.attr", "text", "Tree 2 ALLOWS NESTING"); + cy.get("[ui5-tree] > [ui5-tree-item]").eq(1).should("have.attr", "text", "Tree 3 (no icon)"); + + // Verify first item is nested inside second item + cy.get("[ui5-tree] > [ui5-tree-item]").eq(0).find("[ui5-tree-item]").first().should("have.attr", "text", "Tree 1"); + }); + + it("Rearranging leafs", () => { + cy.mount( + + + + + + + + + + + + + + + + + + + + + + + ); + + // Add drag and drop event handlers + cy.get("[ui5-tree]").then(($tree) => { + const tree = $tree[0]; + + tree.addEventListener("ui5-move-over", (e: CustomEvent) => { + const { destination, source } = e.detail; + if (!tree.contains(source.element)) { + return; + } + e.preventDefault(); + }); + + tree.addEventListener("ui5-move", (e: CustomEvent) => { + const { destination, source } = e.detail; + + if (source.element === destination.element) { + return; + } + + switch (destination.placement) { + case "Before": + destination.element.before(source.element); + break; + case "After": + destination.element.after(source.element); + break; + case "On": + destination.element.prepend(source.element); + break; + } + }); + }); + + // Click toggle button to expand items (similar to WDIO test) + cy.get("[ui5-tree-item]") + .shadow() + .find(".ui5-li-tree-toggle-icon") + .first() + .click(); + + // Get all tree items after expansion + cy.get("[ui5-tree] [ui5-tree-item]").then(($allItems) => { + const allItems = Array.from($allItems); + const secondToLastLeaf = allItems[12]; + const lastLeaf = allItems[13]; + + // Move second-to-last leaf after last leaf + const moveEvent1 = new CustomEvent("ui5-move", { + detail: { + source: { element: secondToLastLeaf }, + destination: { element: lastLeaf, placement: "After" } + } + }); + + cy.get("[ui5-tree]").then(($tree) => { + $tree[0].dispatchEvent(moveEvent1); + }); + + // Verify the order changed - swap positions in expected array + const expectedAfterFirst = [...allItems]; + [expectedAfterFirst[12], expectedAfterFirst[13]] = [expectedAfterFirst[13], expectedAfterFirst[12]]; + + cy.get("[ui5-tree] [ui5-tree-item]").then(($newItems) => { + const newItems = Array.from($newItems); + expect(newItems[12]).to.equal(expectedAfterFirst[12]); + expect(newItems[13]).to.equal(expectedAfterFirst[13]); + }); + + // Move last leaf before second-to-last leaf (reverse the previous move) + cy.get("[ui5-tree] [ui5-tree-item]").then(($currentItems) => { + const currentItems = Array.from($currentItems); + const currentSecondToLast = currentItems[12]; + const currentLast = currentItems[13]; + + const moveEvent2 = new CustomEvent("ui5-move", { + detail: { + source: { element: currentLast }, + destination: { element: currentSecondToLast, placement: "Before" } + } + }); + + cy.get("[ui5-tree]").then(($tree) => { + $tree[0].dispatchEvent(moveEvent2); + }); + + // Verify the order is back to original + cy.get("[ui5-tree] [ui5-tree-item]").then(($finalItems) => { + const finalItems = Array.from($finalItems); + expect(finalItems[12]).to.equal(allItems[12]); + expect(finalItems[13]).to.equal(allItems[13]); + }); + }); + }); + }); + + it("Nesting parent among its children should be impossible", () => { + cy.mount( + + + + + + + + + + + + + + + + + + + + + + + ); + + // Add drag and drop event handlers + cy.get("[ui5-tree]").then(($tree) => { + const tree = $tree[0]; + + tree.addEventListener("ui5-move-over", (e: CustomEvent) => { + const { destination, source } = e.detail; + if (!tree.contains(source.element)) { + return; + } + + // Prevent moving parent among its children + if (source.element.contains(destination.element)) { + return; + } + + e.preventDefault(); + }); + + tree.addEventListener("ui5-move", (e: CustomEvent) => { + const { destination, source } = e.detail; + + if (source.element === destination.element) { + return; + } + + // Additional check: prevent moving parent among its children + if (source.element.contains(destination.element)) { + return; + } + + switch (destination.placement) { + case "Before": + destination.element.before(source.element); + break; + case "After": + destination.element.after(source.element); + break; + case "On": + destination.element.prepend(source.element); + break; + } + }); + }); + + // Get all tree items + cy.get("[ui5-tree] [ui5-tree-item]").then(($allItems) => { + const allItems = Array.from($allItems); + const parent = allItems[0]; + const child = allItems[1]; + + // Store original order for comparison + const originalOrder = allItems.map(item => item.getAttribute('text')); + + // Try to move parent after its child (should be prevented) + const moveEvent = new CustomEvent("ui5-move", { + detail: { + source: { element: parent }, + destination: { element: child, placement: "After" } + } + }); + + cy.get("[ui5-tree]").then(($tree) => { + $tree[0].dispatchEvent(moveEvent); + }); + + // Verify order stays the same - parent not nested among its children + cy.get("[ui5-tree] [ui5-tree-item]").then(($newItems) => { + const newItems = Array.from($newItems); + const newOrder = newItems.map(item => item.getAttribute('text')); + + expect(newOrder).to.deep.equal(originalOrder); + + // Specifically verify parent is still at position 0 and child at position 1 + expect(newItems[0]).to.equal(parent); + expect(newItems[1]).to.equal(child); + }); + }); + }); +}); \ No newline at end of file diff --git a/packages/main/test/specs/Tree.spec.js b/packages/main/test/specs/Tree.spec.js deleted file mode 100644 index afa6e6ed32ad..000000000000 --- a/packages/main/test/specs/Tree.spec.js +++ /dev/null @@ -1,342 +0,0 @@ -import { assert } from "chai"; - -async function getItemsCount(selector) { - const items = await getItems(selector); - return items.length; -} - -async function getItems(selector) { - const listItems = await browser.$$(`${selector} [ui5-tree-item]`); - - const promises = listItems.map(async (item) => { - const isDisplayed = await item.isDisplayedInViewport(); - return isDisplayed ? item : null; - },); - - const items = await Promise.all(promises); - - return items.filter((item) => item); -} - -describe("Tree general interaction", () => { - before(async () => { - await browser.url(`test/pages/Tree.html`); - }); - - it("Tree is rendered", async () => { - const treeRoot = await browser.$("#tree").shadow$("ui5-tree-list"); - assert.ok(await treeRoot.isExisting(), "Tree is rendered."); - }); - - it("Tree items can be collapsed", async () => { - const listItemsBefore = await getItemsCount("#tree"); - const toggleButton = await browser.$(">>>#tree ui5-tree-item[expanded] ui5-icon.ui5-li-tree-toggle-icon"); - - await toggleButton.click(); - const listItemsAfter = await getItemsCount("#tree"); - assert.isBelow(listItemsAfter, listItemsBefore, "After collapsing a node, there are less items in the list"); - }); - - it("Tree items can be expanded", async () => { - const listItemsBefore = await getItemsCount("#tree"); - const toggleButton = await browser.$(">>>#tree ui5-tree-item ui5-icon.ui5-li-tree-toggle-icon"); - - await toggleButton.click(); - const listItemsAfter = await getItemsCount("#tree"); - assert.isAbove(listItemsAfter, listItemsBefore, "After expanding a node, there are more items in the list"); - }); - - it("keyboard handling on F2", async () => { - const item = await browser.$("ui5-tree-item-custom.item"); - const itemBtn = await browser.$("ui5-button.itemBtn"); - - await item.click(); - assert.ok(await item.isFocused(), "item is focused"); - - // act: F2 from item -> the focus should go to "Click me" button - await item.keys("F2"); - assert.ok(await itemBtn.isFocused(), "the 1st tabbable element (button) is focused"); - - // act: f2 from the "Click me" button - the focus should go back to the parent item - await itemBtn.keys("F2"); - assert.ok(await item.isFocused(), "the parent item is focused"); - }); - -}); - -describe("Tree proxies properties to list", () => { - before(async () => { - await browser.url(`test/pages/Tree.html`); - }); - - it("Mouseover/mouseout events", async () => { - const treeItems = await browser.$$(">>>#tree ui5-tree-item .ui5-li-root-tree"); - const inputMouseover = await browser.$("#mouseover-counter"); - const inputMouseout = await browser.$("#mouseout-counter"); - - await treeItems[0].moveTo(); - - assert.strictEqual(await inputMouseover.getAttribute("value"), "1", "Mouseover event is fired when item is accessed"); - - await treeItems[1].moveTo(); - assert.strictEqual(await inputMouseover.getAttribute("value"), "2", "Mouseover event is fired when other item is accessed result"); - assert.strictEqual(await inputMouseout.getAttribute("value"), "1", "Mouseout event is fired when the first item is not hovered"); - }) - - it("SelectionMode works", async () => { - const tree = await browser.$("#tree"); - const list = await tree.shadow$("ui5-tree-list"); - - const treeItem = await browser.$("#firstCollapsedItem"); - assert.strictEqual(await treeItem.getAttribute("_selection-mode"), "Multiple", "SelectionMode applied to the tree item"); - - const modes = ["None", "Single", "SingleStart", "SingleEnd", "Multiple", "Delete"]; - modes.forEach(async selectionMode => { - await tree.setAttribute("selection-mode", selectionMode); - assert.strictEqual(await list.getAttribute("selection-mode"), selectionMode, "SelectionMode applied"); - }); - }); - - it("SelectionMode works recursively", async () => { - const lastItem = await browser.$(">>>#allItemsMultiple .lastItem"); - assert.strictEqual(await lastItem.getAttribute("_selection-mode"), "Multiple", "SelectionMode applied to the last tree item"); - }); - - it("headerText, footerText, noDataText work", async () => { - const tree = await browser.$("#tree"); - const list = await tree.shadow$("ui5-tree-list"); - - await tree.setAttribute("header-text", "header text"); - await tree.setAttribute("footer-text", "footer text"); - await tree.setAttribute("no-data-text", "no data text"); - - assert.strictEqual(await list.getAttribute("header-text"), "header text", "header text applied"); - assert.strictEqual(await list.getAttribute("footer-text"), "footer text", "footer text applied"); - assert.strictEqual(await list.getAttribute("no-data-text"), "no data text", "no data text applied"); - }) - - it("Tests the prevention of the ui5-itemClick event", async () => { - const treeItems = await browser.$$("#preventable-click-event ui5-tree-item"); - const firstItem = treeItems[0]; - - await firstItem.click(); - - assert.notOk(await firstItem.getAttribute("selected"), "The first item is not selected when we prevent the click event."); - }); - - it("selectionChange event provides targetItem parameter", async () => { - const selectionChangeTargetItemResult = await browser.$("#selectionChangeTargetItemResult"); - const listItems = await browser.$$("#treeIndeterminate ui5-tree-item"); - const firstTreeItem = await browser.$("#treeIndeterminate #item1"); - let firstTreeItemId, targetItemId; - - await listItems[0].click(); - - firstTreeItemId = await firstTreeItem.getProperty("id"); - targetItemId = await selectionChangeTargetItemResult.getProperty("value"); - - assert.strictEqual(targetItemId, firstTreeItemId, "targetItem parameter holds correct tree item"); - }); -}); - -describe("Tree has screen reader support", () => { - before(async () => { - await browser.url(`test/pages/Tree.html`); - }); - - it("List role is correct", async () => { - const tree = await browser.$("#tree"); - const list = await tree.shadow$("ui5-tree-list"); - assert.strictEqual(await list.shadow$("ul").getAttribute("role"), "tree", "List role is tree"); - }); - - it("List item acc attributes correct", async () => { - const listItems = await browser.$$("#tree ui5-tree-item"); - - const promises = listItems.map(async (item, idx) => { - const li = await item.shadow$("li"); - const itemExpandable = await item.getProperty("showToggleButton"); - const itemExpanded = await item.getProperty("expanded"); - const liAriaExpanded = await li.getAttribute("aria-expanded"); - - const ariaExpandedValues = { - // (1) expandable: aria-expanded can be 'true' or 'false' - "true": { - "true" : "true", - "false": "false", - }, - // (2) not expandable: aria-expanded is null - not present - "false": { - "true" : null, - "false": null, - } - }; - - assert.strictEqual(await li.getAttribute("role"), "treeitem", "List item role is correct"); - assert.strictEqual(await li.getAttribute("aria-level"), await item.getAttribute("level"), "aria level is correct"); - assert.equal(liAriaExpanded, ariaExpandedValues[itemExpandable][itemExpanded], - "aria-expanded is correct."); - }); - - await Promise.all(promises); - }); - - it ("Tree's internal List receives aria-label from the accessibleName property", async () => { - const tree = await browser.$("#tree"); - const list = await tree.shadow$("ui5-tree-list"); - assert.strictEqual(await list.shadow$("ul").getAttribute("aria-label"), "Tree with accessibleName", "list aria label is correct"); - }); - - it ("Tree's internal List receives aria-label from the accessibleNameRef property", async () => { - const tree = await browser.$("#preventable-click-event"); - const list = await tree.shadow$("ui5-tree-list"); - const treeLabel = await browser.$("#tree-label"); - assert.strictEqual(await list.shadow$("ul").getAttribute("aria-label"), await treeLabel.getHTML(false), "list aria label is correct"); - }); - - it ("Tree list item receives aria-labelledby from the accessibleName property", async () => { - const listTreeItem = await browser.$("#tree ui5-tree-item"); - const listItem = await listTreeItem.shadow$("li"); - const liAriaLabelledBy = await listItem.getAttribute("aria-labelledby"); - const ariaLabelText = await listItem.$(`#${liAriaLabelledBy}`).getText(); - - assert.ok(ariaLabelText.includes("Tree item with accessibleName"), "aria label text is correct"); - }); - -}); - - -describe("Tree slots", () => { - before(async () => { - await browser.url(`test/pages/Tree.html`); - }); - - it("items slot", async () => { - const treeItem = await browser.$("#treeItem"); - const btn = await browser.$("#btn"); - - let items = await treeItem.getProperty("items"); - assert.strictEqual(items.length, 1, "Correct items count"); - - await btn.click(); - - items = await treeItem.getProperty("items"); - const newlyAddedItem = await treeItem.$('#treeItem [ui5-tree-item]:last-child'); - - assert.strictEqual(items.length, 2, "Dynamic item is added correctly"); - assert.strictEqual(await newlyAddedItem.getProperty("text"), "1-1-2", "Dynamic item is added correctly"); - assert.strictEqual(await newlyAddedItem.getProperty("level"), 3, "Dynamic item is displayed correctly"); - }); -}); - -describe("Tree drag and drop tests", () => { - const getDragOffset = async (draggedElement, dropTargetElement, targetPosition) => { - const draggedRectangle = { - ...await draggedElement.getLocation(), - ...await draggedElement.getSize() - }; - - const dropTargetElementRectangle = { - ...await dropTargetElement.getLocation(), - ...await dropTargetElement.getSize() - } - const EXTRA_OFFSET = Math.floor(dropTargetElementRectangle.height / 3); - - const draggedElementCenter = (draggedRectangle.y + draggedRectangle.height / 2); - const droppedElementCenter = (dropTargetElementRectangle.y + dropTargetElementRectangle.height / 2); - - let offsetToCenter = Math.round(droppedElementCenter - draggedElementCenter); - - if (targetPosition === "Before") { - offsetToCenter -= EXTRA_OFFSET - } else if (targetPosition === "After") { - offsetToCenter += EXTRA_OFFSET; - } - - return offsetToCenter; - }; - - const compareItemsOrder = async (treeId, expectedItems, nestedTag) => { - let treeItems; - if (nestedTag) { - treeItems = await browser.$$(`#${treeId} [${nestedTag}]`); - } else { - treeItems = await browser.$$(`#${treeId} > *`); // direct children - } - const results = await Promise.all(expectedItems.map((item, i) => item.isEqual(treeItems[i]))); - - return results.every(value => value); - } - - before(async () => { - await browser.url(`test/pages/TreeDragAndDrop.html`); - }); - - it("Moving item After another", async () => { - const [firstItem, secondItem, thirdItem] = await browser.$$("#tree > [ui5-tree-item]"); - - let dragOffset = await getDragOffset(firstItem, secondItem, "After"); - - await firstItem.dragAndDrop({ x: 0, y: dragOffset}); - assert.ok(await compareItemsOrder("tree", [secondItem, firstItem, thirdItem]), "Items order has changed"); - - dragOffset = await getDragOffset(firstItem, thirdItem, "After"); - await firstItem.dragAndDrop({ x: 0, y: dragOffset}); - assert.ok(await compareItemsOrder("tree", [secondItem, thirdItem, firstItem]), "Items order has changed"); - }); - - it("Moving item Before another", async () => { - const [secondItem, thirdItem, firstItem] = await browser.$$("#tree > [ui5-tree-item]"); - - let dragOffset = await getDragOffset(firstItem, thirdItem, "Before"); - await firstItem.dragAndDrop({ x: 0, y: dragOffset}); - assert.ok(await compareItemsOrder("tree", [secondItem, firstItem, thirdItem]), "Items order has changed"); - - dragOffset = await getDragOffset(firstItem, secondItem, "Before") - await firstItem.dragAndDrop({ x: 0, y: dragOffset}); - assert.ok(await compareItemsOrder("tree", [firstItem, secondItem, thirdItem]), "Items order has changed"); - }); - - it("Moving item ON another", async () => { - const [firstItem, secondItem, thirdItem] = await browser.$$("#tree > [ui5-tree-item]"); - - await firstItem.dragAndDrop({ x: 0, y: 0 }); - assert.ok(await compareItemsOrder("tree", [firstItem, secondItem, thirdItem]), "Items order has NOT changed"); - - const dragOffset = await getDragOffset(firstItem, secondItem); - await firstItem.dragAndDrop({ x: 0, y: dragOffset}); - assert.ok(await compareItemsOrder("tree", [secondItem, thirdItem]), "First item nested in second"); - }); - - it("Rearranging leafs", async () => { - const toggleButton = await browser.$(">>>#tree ui5-tree-item ui5-icon.ui5-li-tree-toggle-icon"); - await toggleButton.click(); - - const allItems = await browser.$$("#tree [ui5-tree-item]"); - let secondToLastLeaf = allItems[12]; - let lastLeaf = allItems[13]; - - let dragOffset = await getDragOffset(secondToLastLeaf, lastLeaf, "After"); - await secondToLastLeaf.dragAndDrop({ x: 0, y: dragOffset}); - [allItems[12], allItems[13]] = [allItems[13], allItems[12]]; - assert.ok(await compareItemsOrder("tree", allItems, 'ui5-tree-item'), "Second-to-last leaf moved after last"); - - secondToLastLeaf = allItems[12]; - lastLeaf = allItems[13]; - - dragOffset = await getDragOffset(lastLeaf, secondToLastLeaf, "Before"); - await lastLeaf.dragAndDrop({ x: 0, y: dragOffset}); - [allItems[13], allItems[12]] = [allItems[12], allItems[13]]; - assert.ok(await compareItemsOrder("tree", allItems, 'ui5-tree-item'), "Last leaf moved before second-to-last"); - }); - - it("Nesting parent among its children should be impossible", async () => { - const allItems = await browser.$$("#tree [ui5-tree-item]"); - const parent = allItems[0]; - const child = allItems[1]; - - const dragOffset = await getDragOffset(parent, child, "After"); - await parent.dragAndDrop({ x: 0, y: dragOffset}); - assert.ok(await compareItemsOrder("tree", allItems, 'ui5-tree-item'), "Order stays the same. Parent not nested among its children."); - }); -}); \ No newline at end of file From 2117415f061b38799d55e20d1a2f09cc389dc52c Mon Sep 17 00:00:00 2001 From: Nikola Anachkov Date: Mon, 14 Jul 2025 12:54:14 +0300 Subject: [PATCH 2/3] refactor: simplify Tree drag and drop tests by eliminating code duplication --- packages/main/cypress/specs/Tree.cy.tsx | 558 ++++++------------------ 1 file changed, 129 insertions(+), 429 deletions(-) diff --git a/packages/main/cypress/specs/Tree.cy.tsx b/packages/main/cypress/specs/Tree.cy.tsx index 326d4592be98..498c9106786e 100644 --- a/packages/main/cypress/specs/Tree.cy.tsx +++ b/packages/main/cypress/specs/Tree.cy.tsx @@ -550,110 +550,89 @@ describe("Tree slots", () => { }); describe("Tree drag and drop tests", () => { - it("Moving item After another", () => { - cy.mount( - - - - - - - - - - - - - - - - - - - - - - - ); - - // Add drag and drop event handlers - cy.get("[ui5-tree]").then(($tree) => { - const tree = $tree[0]; - - tree.addEventListener("ui5-move-over", (e: CustomEvent) => { - const { destination, source } = e.detail; - if (!tree.contains(source.element)) { - return; - } - // Allow "After" placement for this test - if (destination.placement === "Before") { - e.preventDefault(); - } - }); - - tree.addEventListener("ui5-move", (e: CustomEvent) => { - const { destination, source } = e.detail; - switch (destination.placement) { - case "Before": - destination.element.before(source.element); - break; - case "After": - destination.element.after(source.element); - break; - case "On": - destination.element.prepend(source.element); - break; - } - }); + const setupDragAndDrop = (tree: HTMLElement, options: { + allowBefore?: boolean; + allowAfter?: boolean; + allowOn?: boolean; + preventParentChildMove?: boolean; + } = {}) => { + const { allowBefore = true, allowAfter = true, allowOn = true, preventParentChildMove = false } = options; + + tree.addEventListener("ui5-move-over", (e: CustomEvent) => { + const { destination, source } = e.detail; + + if (!tree.contains(source.element)) { + return; + } + + // Prevent parent-child moves if specified + if (preventParentChildMove && source.element.contains(destination.element)) { + return; + } + + // Check allowed placements + if (!allowBefore && destination.placement === "Before") { + return; + } + if (!allowAfter && destination.placement === "After") { + return; + } + if (!allowOn && destination.placement === "On") { + return; + } + + // Special nesting rules + if (destination.placement === "On" && !("allowsNesting" in destination.element.dataset)) { + return; + } + + e.preventDefault(); }); - // Use trigger to simulate drag and drop events instead of realMouse actions - cy.get("[ui5-tree] > [ui5-tree-item]").eq(0).as("firstItem"); - cy.get("[ui5-tree] > [ui5-tree-item]").eq(1).as("secondItem"); - cy.get("[ui5-tree] > [ui5-tree-item]").eq(2).as("thirdItem"); - - // Simulate moving first item after second item by triggering the move event directly - cy.get("@firstItem").then(($first) => { - cy.get("@secondItem").then(($second) => { - const moveEvent = new CustomEvent("ui5-move", { - detail: { - source: { element: $first[0] }, - destination: { element: $second[0], placement: "After" } - } - }); - cy.get("[ui5-tree]").then(($tree) => { - $tree[0].dispatchEvent(moveEvent); - }); - }); + tree.addEventListener("ui5-move", (e: CustomEvent) => { + const { destination, source } = e.detail; + + // Prevent self-moves + if (source.element === destination.element) { + return; + } + + // Prevent parent-child moves if specified + if (preventParentChildMove && source.element.contains(destination.element)) { + return; + } + + switch (destination.placement) { + case "Before": + destination.element.before(source.element); + break; + case "After": + destination.element.after(source.element); + break; + case "On": + destination.element.prepend(source.element); + break; + } }); + }; - // Verify new order: second, first, third - cy.get("[ui5-tree] > [ui5-tree-item]").eq(0).should("have.attr", "text", "Tree 2 ALLOWS NESTING"); - cy.get("[ui5-tree] > [ui5-tree-item]").eq(1).should("have.attr", "text", "Tree 1"); - cy.get("[ui5-tree] > [ui5-tree-item]").eq(2).should("have.attr", "text", "Tree 3 (no icon)"); - - // Move first item after third item - cy.get("[ui5-tree] > [ui5-tree-item]").eq(1).then(($first) => { - cy.get("[ui5-tree] > [ui5-tree-item]").eq(2).then(($third) => { + const dispatchMoveEvent = (sourceAlias: string, targetAlias: string, placement: string) => { + cy.get(sourceAlias).then($source => { + cy.get(targetAlias).then($target => { const moveEvent = new CustomEvent("ui5-move", { detail: { - source: { element: $first[0] }, - destination: { element: $third[0], placement: "After" } + source: { element: $source[0] }, + destination: { element: $target[0], placement } } }); - cy.get("[ui5-tree]").then(($tree) => { + cy.get("[ui5-tree]").then($tree => { $tree[0].dispatchEvent(moveEvent); }); }); }); + }; - // Verify final order: second, third, first - cy.get("[ui5-tree] > [ui5-tree-item]").eq(0).should("have.attr", "text", "Tree 2 ALLOWS NESTING"); - cy.get("[ui5-tree] > [ui5-tree-item]").eq(1).should("have.attr", "text", "Tree 3 (no icon)"); - cy.get("[ui5-tree] > [ui5-tree-item]").eq(2).should("have.attr", "text", "Tree 1"); - }); - - it("Moving item Before another", () => { + beforeEach(() => { cy.mount( @@ -662,7 +641,7 @@ describe("Tree drag and drop tests", () => { - + @@ -678,418 +657,139 @@ describe("Tree drag and drop tests", () => { ); + }); - // Add drag and drop event handlers - cy.get("[ui5-tree]").then(($tree) => { - const tree = $tree[0]; + it("Moving item After another", () => { + cy.get("[ui5-tree]").then($tree => setupDragAndDrop($tree[0], { allowBefore: false })); - tree.addEventListener("ui5-move-over", (e: CustomEvent) => { - const { destination, source } = e.detail; - if (!tree.contains(source.element)) { - return; - } - // Allow "Before" placement for this test - if (destination.placement === "After") { - e.preventDefault(); - } - }); + cy.get("[ui5-tree] > [ui5-tree-item]").eq(0).as("firstItem"); + cy.get("[ui5-tree] > [ui5-tree-item]").eq(1).as("secondItem"); - tree.addEventListener("ui5-move", (e: CustomEvent) => { - const { destination, source } = e.detail; - switch (destination.placement) { - case "Before": - destination.element.before(source.element); - break; - case "After": - destination.element.after(source.element); - break; - case "On": - destination.element.prepend(source.element); - break; - } - }); - }); + dispatchMoveEvent("@firstItem", "@secondItem", "After"); + + cy.get("[ui5-tree] > [ui5-tree-item]").eq(0).should("have.attr", "text", "Tree 2 ALLOWS NESTING"); + cy.get("[ui5-tree] > [ui5-tree-item]").eq(1).should("have.attr", "text", "Tree 1"); + + cy.get("[ui5-tree] > [ui5-tree-item]").eq(1).as("movedFirst"); + cy.get("[ui5-tree] > [ui5-tree-item]").eq(2).as("third"); + + dispatchMoveEvent("@movedFirst", "@third", "After"); + + cy.get("[ui5-tree] > [ui5-tree-item]").eq(0).should("have.attr", "text", "Tree 2 ALLOWS NESTING"); + cy.get("[ui5-tree] > [ui5-tree-item]").eq(1).should("have.attr", "text", "Tree 3 (no icon)"); + cy.get("[ui5-tree] > [ui5-tree-item]").eq(2).should("have.attr", "text", "Tree 1"); + }); + + it("Moving item Before another", () => { + cy.get("[ui5-tree]").then($tree => setupDragAndDrop($tree[0], { allowAfter: false })); - // Get tree items - note the order matches WDIO test: [secondItem, thirdItem, firstItem] - cy.get("[ui5-tree] > [ui5-tree-item]").eq(1).as("secondItem"); - cy.get("[ui5-tree] > [ui5-tree-item]").eq(2).as("thirdItem"); cy.get("[ui5-tree] > [ui5-tree-item]").eq(0).as("firstItem"); + cy.get("[ui5-tree] > [ui5-tree-item]").eq(2).as("thirdItem"); - // Move first item before third item - cy.get("@firstItem").then(($first) => { - cy.get("@thirdItem").then(($third) => { - const moveEvent = new CustomEvent("ui5-move", { - detail: { - source: { element: $first[0] }, - destination: { element: $third[0], placement: "Before" } - } - }); - cy.get("[ui5-tree]").then(($tree) => { - $tree[0].dispatchEvent(moveEvent); - }); - }); - }); + dispatchMoveEvent("@firstItem", "@thirdItem", "Before"); - // Verify new order: second, first, third cy.get("[ui5-tree] > [ui5-tree-item]").eq(0).should("have.attr", "text", "Tree 2 ALLOWS NESTING"); cy.get("[ui5-tree] > [ui5-tree-item]").eq(1).should("have.attr", "text", "Tree 1"); cy.get("[ui5-tree] > [ui5-tree-item]").eq(2).should("have.attr", "text", "Tree 3 (no icon)"); - // Move first item before second item - cy.get("[ui5-tree] > [ui5-tree-item]").eq(1).then(($first) => { - cy.get("[ui5-tree] > [ui5-tree-item]").eq(0).then(($second) => { - const moveEvent = new CustomEvent("ui5-move", { - detail: { - source: { element: $first[0] }, - destination: { element: $second[0], placement: "Before" } - } - }); - cy.get("[ui5-tree]").then(($tree) => { - $tree[0].dispatchEvent(moveEvent); - }); - }); - }); + cy.get("[ui5-tree] > [ui5-tree-item]").eq(1).as("movedFirst"); + cy.get("[ui5-tree] > [ui5-tree-item]").eq(0).as("second"); + + dispatchMoveEvent("@movedFirst", "@second", "Before"); - // Verify final order: first, second, third cy.get("[ui5-tree] > [ui5-tree-item]").eq(0).should("have.attr", "text", "Tree 1"); cy.get("[ui5-tree] > [ui5-tree-item]").eq(1).should("have.attr", "text", "Tree 2 ALLOWS NESTING"); cy.get("[ui5-tree] > [ui5-tree-item]").eq(2).should("have.attr", "text", "Tree 3 (no icon)"); }); it("Moving item ON another", () => { - cy.mount( - - - - - - - - - - - - - - - - - - - - - - - ); + cy.get("[ui5-tree]").then($tree => setupDragAndDrop($tree[0])); - // Add drag and drop event handlers - cy.get("[ui5-tree]").then(($tree) => { - const tree = $tree[0]; - - tree.addEventListener("ui5-move-over", (e: CustomEvent) => { - const { destination, source } = e.detail; - if (!tree.contains(source.element)) { - return; - } - // Allow "On" placement only for elements with data-allows-nesting - if (destination.placement === "On" && !("allowsNesting" in destination.element.dataset)) { - return; - } - e.preventDefault(); - }); - - tree.addEventListener("ui5-move", (e: CustomEvent) => { - const { destination, source } = e.detail; - - // Prevent moving element onto itself - if (source.element === destination.element) { - return; - } - - switch (destination.placement) { - case "Before": - destination.element.before(source.element); - break; - case "After": - destination.element.after(source.element); - break; - case "On": - destination.element.prepend(source.element); - break; - } - }); - }); - - // Get tree items cy.get("[ui5-tree] > [ui5-tree-item]").eq(0).as("firstItem"); cy.get("[ui5-tree] > [ui5-tree-item]").eq(1).as("secondItem"); - cy.get("[ui5-tree] > [ui5-tree-item]").eq(2).as("thirdItem"); - // Test 1: Try to move first item ON itself (should not change order) - cy.get("@firstItem").then(($first) => { - const moveEvent = new CustomEvent("ui5-move", { - detail: { - source: { element: $first[0] }, - destination: { element: $first[0], placement: "On" } - } - }); - cy.get("[ui5-tree]").then(($tree) => { - $tree[0].dispatchEvent(moveEvent); - }); - }); + dispatchMoveEvent("@firstItem", "@firstItem", "On"); - // Verify order has NOT changed cy.get("[ui5-tree] > [ui5-tree-item]").should("have.length", 3); cy.get("[ui5-tree] > [ui5-tree-item]").eq(0).should("have.attr", "text", "Tree 1"); - cy.get("[ui5-tree] > [ui5-tree-item]").eq(1).should("have.attr", "text", "Tree 2 ALLOWS NESTING"); - cy.get("[ui5-tree] > [ui5-tree-item]").eq(2).should("have.attr", "text", "Tree 3 (no icon)"); - // Test 2: Move first item ON second item (nesting - should work) - cy.get("@firstItem").then(($first) => { - cy.get("@secondItem").then(($second) => { - const moveEvent = new CustomEvent("ui5-move", { - detail: { - source: { element: $first[0] }, - destination: { element: $second[0], placement: "On" } - } - }); - cy.get("[ui5-tree]").then(($tree) => { - $tree[0].dispatchEvent(moveEvent); - }); - }); - }); + dispatchMoveEvent("@firstItem", "@secondItem", "On"); - // Verify first item is nested in second (only 2 items at root level) cy.get("[ui5-tree] > [ui5-tree-item]").should("have.length", 2); cy.get("[ui5-tree] > [ui5-tree-item]").eq(0).should("have.attr", "text", "Tree 2 ALLOWS NESTING"); - cy.get("[ui5-tree] > [ui5-tree-item]").eq(1).should("have.attr", "text", "Tree 3 (no icon)"); - - // Verify first item is nested inside second item cy.get("[ui5-tree] > [ui5-tree-item]").eq(0).find("[ui5-tree-item]").first().should("have.attr", "text", "Tree 1"); }); it("Rearranging leafs", () => { - cy.mount( - - - - - - - - - - - - - - - - - - - - - - - ); + cy.get("[ui5-tree]").then($tree => setupDragAndDrop($tree[0])); - // Add drag and drop event handlers - cy.get("[ui5-tree]").then(($tree) => { - const tree = $tree[0]; + cy.get("[ui5-tree-item]").shadow().find(".ui5-li-tree-toggle-icon").first().click(); - tree.addEventListener("ui5-move-over", (e: CustomEvent) => { - const { destination, source } = e.detail; - if (!tree.contains(source.element)) { - return; - } - e.preventDefault(); - }); - - tree.addEventListener("ui5-move", (e: CustomEvent) => { - const { destination, source } = e.detail; - - if (source.element === destination.element) { - return; - } - - switch (destination.placement) { - case "Before": - destination.element.before(source.element); - break; - case "After": - destination.element.after(source.element); - break; - case "On": - destination.element.prepend(source.element); - break; - } - }); - }); - - // Click toggle button to expand items (similar to WDIO test) - cy.get("[ui5-tree-item]") - .shadow() - .find(".ui5-li-tree-toggle-icon") - .first() - .click(); - - // Get all tree items after expansion - cy.get("[ui5-tree] [ui5-tree-item]").then(($allItems) => { - const allItems = Array.from($allItems); - const secondToLastLeaf = allItems[12]; - const lastLeaf = allItems[13]; - - // Move second-to-last leaf after last leaf + cy.get("[ui5-tree] [ui5-tree-item]").then($allItems => { + const items = Array.from($allItems); + const moveEvent1 = new CustomEvent("ui5-move", { detail: { - source: { element: secondToLastLeaf }, - destination: { element: lastLeaf, placement: "After" } + source: { element: items[12] }, + destination: { element: items[13], placement: "After" } } }); - cy.get("[ui5-tree]").then(($tree) => { + cy.get("[ui5-tree]").then($tree => { $tree[0].dispatchEvent(moveEvent1); }); - // Verify the order changed - swap positions in expected array - const expectedAfterFirst = [...allItems]; - [expectedAfterFirst[12], expectedAfterFirst[13]] = [expectedAfterFirst[13], expectedAfterFirst[12]]; - - cy.get("[ui5-tree] [ui5-tree-item]").then(($newItems) => { + cy.get("[ui5-tree] [ui5-tree-item]").then($newItems => { const newItems = Array.from($newItems); - expect(newItems[12]).to.equal(expectedAfterFirst[12]); - expect(newItems[13]).to.equal(expectedAfterFirst[13]); + expect(newItems[12]).to.equal(items[13]); + expect(newItems[13]).to.equal(items[12]); }); - // Move last leaf before second-to-last leaf (reverse the previous move) - cy.get("[ui5-tree] [ui5-tree-item]").then(($currentItems) => { + cy.get("[ui5-tree] [ui5-tree-item]").then($currentItems => { const currentItems = Array.from($currentItems); - const currentSecondToLast = currentItems[12]; - const currentLast = currentItems[13]; - const moveEvent2 = new CustomEvent("ui5-move", { detail: { - source: { element: currentLast }, - destination: { element: currentSecondToLast, placement: "Before" } + source: { element: currentItems[13] }, + destination: { element: currentItems[12], placement: "Before" } } }); - cy.get("[ui5-tree]").then(($tree) => { + cy.get("[ui5-tree]").then($tree => { $tree[0].dispatchEvent(moveEvent2); }); - // Verify the order is back to original - cy.get("[ui5-tree] [ui5-tree-item]").then(($finalItems) => { + cy.get("[ui5-tree] [ui5-tree-item]").then($finalItems => { const finalItems = Array.from($finalItems); - expect(finalItems[12]).to.equal(allItems[12]); - expect(finalItems[13]).to.equal(allItems[13]); + expect(finalItems[12]).to.equal(items[12]); + expect(finalItems[13]).to.equal(items[13]); }); }); }); }); it("Nesting parent among its children should be impossible", () => { - cy.mount( - - - - - - - - - - - - - - - - - - - - - - - ); - - // Add drag and drop event handlers - cy.get("[ui5-tree]").then(($tree) => { - const tree = $tree[0]; - - tree.addEventListener("ui5-move-over", (e: CustomEvent) => { - const { destination, source } = e.detail; - if (!tree.contains(source.element)) { - return; - } - - // Prevent moving parent among its children - if (source.element.contains(destination.element)) { - return; - } + cy.get("[ui5-tree]").then($tree => setupDragAndDrop($tree[0], { preventParentChildMove: true })); - e.preventDefault(); - }); - - tree.addEventListener("ui5-move", (e: CustomEvent) => { - const { destination, source } = e.detail; - - if (source.element === destination.element) { - return; - } + cy.get("[ui5-tree] [ui5-tree-item]").then($allItems => { + const items = Array.from($allItems); + const originalOrder = items.map(item => item.getAttribute('text')); - // Additional check: prevent moving parent among its children - if (source.element.contains(destination.element)) { - return; - } - - switch (destination.placement) { - case "Before": - destination.element.before(source.element); - break; - case "After": - destination.element.after(source.element); - break; - case "On": - destination.element.prepend(source.element); - break; - } - }); - }); - - // Get all tree items - cy.get("[ui5-tree] [ui5-tree-item]").then(($allItems) => { - const allItems = Array.from($allItems); - const parent = allItems[0]; - const child = allItems[1]; - - // Store original order for comparison - const originalOrder = allItems.map(item => item.getAttribute('text')); - - // Try to move parent after its child (should be prevented) const moveEvent = new CustomEvent("ui5-move", { detail: { - source: { element: parent }, - destination: { element: child, placement: "After" } + source: { element: items[0] }, + destination: { element: items[1], placement: "After" } } }); - cy.get("[ui5-tree]").then(($tree) => { + cy.get("[ui5-tree]").then($tree => { $tree[0].dispatchEvent(moveEvent); }); - // Verify order stays the same - parent not nested among its children - cy.get("[ui5-tree] [ui5-tree-item]").then(($newItems) => { + // Verify no change + cy.get("[ui5-tree] [ui5-tree-item]").then($newItems => { const newItems = Array.from($newItems); const newOrder = newItems.map(item => item.getAttribute('text')); - expect(newOrder).to.deep.equal(originalOrder); - - // Specifically verify parent is still at position 0 and child at position 1 - expect(newItems[0]).to.equal(parent); - expect(newItems[1]).to.equal(child); }); }); }); From c1b31de6179d3eefb25d4ac0fb3a99bb9d597b43 Mon Sep 17 00:00:00 2001 From: Nikola Anachkov Date: Tue, 5 Aug 2025 17:46:25 +0300 Subject: [PATCH 3/3] refactor: replace event tracking with Cypress stubs --- packages/main/cypress/specs/Tree.cy.tsx | 112 +++++++++++------------- 1 file changed, 49 insertions(+), 63 deletions(-) diff --git a/packages/main/cypress/specs/Tree.cy.tsx b/packages/main/cypress/specs/Tree.cy.tsx index 498c9106786e..0b0d08adb239 100644 --- a/packages/main/cypress/specs/Tree.cy.tsx +++ b/packages/main/cypress/specs/Tree.cy.tsx @@ -226,56 +226,40 @@ describe("Tree general interaction", () => { describe("Tree proxies properties to list", () => { it("Mouseover/mouseout events", () => { cy.mount( - <> - - - - - - + + + + + - - - - - + + + ); - - cy.window().then((win) => { - let mouseoverCount = 0; - let mouseoutCount = 0; - - cy.get("[ui5-tree]").then(($tree) => { - $tree[0].addEventListener("ui5-item-mouseover", () => { - mouseoverCount++; - cy.get("#mouseover-counter").invoke("val", mouseoverCount.toString()); - }); - - $tree[0].addEventListener("ui5-item-mouseout", () => { - mouseoutCount++; - cy.get("#mouseout-counter").invoke("val", mouseoutCount.toString()); - }); - }); + + cy.get("[ui5-tree]").then($tree => { + $tree[0].addEventListener("ui5-item-mouseover", cy.stub().as("mouseoverStub")); + $tree[0].addEventListener("ui5-item-mouseout", cy.stub().as("mouseoutStub")); }); - + cy.get("[ui5-tree-item]:first") .shadow() .find(".ui5-li-root-tree") .realHover(); - - cy.get("#mouseover-counter") - .should("have.value", "1"); - + + cy.get("@mouseoverStub") + .should("have.been.calledOnce"); + cy.get("[ui5-tree-item]:eq(1)") .shadow() .find(".ui5-li-root-tree") .realHover(); - - cy.get("#mouseover-counter") - .should("have.value", "2"); - - cy.get("#mouseout-counter") - .should("have.value", "1"); + + cy.get("@mouseoverStub") + .should("have.been.calledTwice"); + + cy.get("@mouseoutStub") + .should("have.been.calledOnce"); }); it("SelectionMode works", () => { @@ -374,43 +358,45 @@ describe("Tree proxies properties to list", () => { ); - - cy.get("[ui5-tree]").then(($tree) => { - $tree[0].addEventListener("ui5-item-click", (event) => { + + cy.get("[ui5-tree]").then($tree => { + $tree[0].addEventListener("ui5-item-click", cy.stub().as("itemClickStub").callsFake((event) => { event.preventDefault(); - }); + })); }); - + cy.get("[ui5-tree-item]:first") .click() .should("not.have.attr", "selected"); + + cy.get("@itemClickStub") + .should("have.been.calledOnce"); }); it("selectionChange event provides targetItem parameter", () => { cy.mount( - <> - - - - - - - - + + + + + + ); - - cy.get("[ui5-tree]").then(($tree) => { - $tree[0].addEventListener("ui5-selection-change", (event: CustomEvent) => { - cy.get("#selectionChangeTargetItemResult").invoke("val", event.detail.targetItem.id || "no-id"); - }); + + let selectionChangeStub; + + cy.get("[ui5-tree]").then($tree => { + selectionChangeStub = cy.stub().as("selectionChangeStub"); + $tree[0].addEventListener("ui5-selection-change", selectionChangeStub); }); - + cy.get("[ui5-tree-item]:first") .invoke("attr", "id", "item1") - .click(); - - cy.get("#selectionChangeTargetItemResult") - .should("have.value", "item1"); + .click() + .then(() => { + expect(selectionChangeStub).to.have.been.calledOnce; + expect(selectionChangeStub.getCall(0).args[0].detail.targetItem.id).to.equal("item1"); + }); }); });