/* * Copyright 2015 Palantir Technologies, Inc. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { createRef } from "react"; import { describe, expect, it, vi } from "@blueprintjs/test-commons/vitest"; import { Classes } from "../../common"; import { Tree } from "./tree"; import { type TreeNodeInfo } from "./treeTypes"; describe("", () => { it("renders its contents", () => { const { container } = render(); expect(container.querySelector(`.${Classes.TREE}`)).toBeInTheDocument(); expect(screen.getByText("Node")).toBeInTheDocument(); }); it("handles undefined input well", () => { const { container } = render(); expect(container.querySelector(`.${Classes.TREE}`)).toBeInTheDocument(); }); it("handles empty input well", () => { const { container } = render(); expect(container.querySelector(`.${Classes.TREE}`)).toBeInTheDocument(); }); it("hasCaret forces a caret to be/not be displayed", () => { const contents: TreeNodeInfo[] = [ { hasCaret: true, id: 0, label: "Item 0" }, { childNodes: [{ id: 5, label: "Item 5" }], hasCaret: true, id: 1, label: "Item 1" }, { hasCaret: false, id: 2, label: "Item 2" }, { childNodes: [{ id: 6, label: "Item 6" }], hasCaret: false, id: 3, label: "Item 3" }, { childNodes: [{ id: 7, label: "Item 7" }], id: 4, label: "Item 4" }, ]; render(); expectHasCaret(screen.getByText("Item 0"), true); expectHasCaret(screen.getByText("Item 1"), true); expectHasCaret(screen.getByText("Item 2"), false); expectHasCaret(screen.getByText("Item 3"), false); }); it("if not specified, caret visibility is determined by the presence of children", () => { const contents: TreeNodeInfo[] = [ { id: 0, label: "Item 0" }, { childNodes: [{ id: 5, label: "Item 5" }], id: 1, label: "Item 1" }, { id: 2, label: "Item 2" }, { childNodes: [{ id: 6, label: "Item 6" }], id: 3, label: "Item 3" }, { childNodes: [{ id: 7, label: "Item 7" }], id: 4, label: "Item 4" }, ]; render(); expectHasCaret(screen.getByText("Item 0"), false); expectHasCaret(screen.getByText("Item 1"), true); expectHasCaret(screen.getByText("Item 2"), false); expectHasCaret(screen.getByText("Item 3"), true); }); it("caret direction is determined by node expansion", () => { const contents: TreeNodeInfo[] = [ { childNodes: [{ id: 5, label: "Item 5" }], hasCaret: true, id: 0, isExpanded: false, label: "Item 0" }, { hasCaret: true, id: 1, isExpanded: true, label: "Item 1" }, { hasCaret: true, id: 2, isExpanded: false, label: "Item 2" }, { childNodes: [{ id: 4, label: "Item 4" }], hasCaret: true, id: 3, isExpanded: true, label: "Item 3" }, ]; render(); expectIsExpanded(screen.getByText("Item 0"), false); expectIsExpanded(screen.getByText("Item 1"), true); expectIsExpanded(screen.getByText("Item 2"), false); expectIsExpanded(screen.getByText("Item 3"), true); }); it("event callbacks are fired correctly", async () => { const user = userEvent.setup(); const onNodeClick = vi.fn(); const onNodeCollapse = vi.fn(); const onNodeContextMenu = vi.fn(); const onNodeDoubleClick = vi.fn(); const onNodeExpand = vi.fn(); const onNodeMouseEnter = vi.fn(); const onNodeMouseLeave = vi.fn(); const contents: TreeNodeInfo[] = [ { id: 0, label: "Item 0" }, { childNodes: [{ id: 5, label: "Item 5" }], id: 1, label: "Item 1" }, { id: 2, label: "Item 2" }, { childNodes: [{ id: 6, label: "Item 6" }], id: 3, isExpanded: true, label: "Item 3" }, { childNodes: [{ id: 7, label: "Item 7" }], id: 4, label: "Item 4" }, ]; render( , ); const item0Content = screen.getByText("Item 0").closest(`.${Classes.TREE_NODE_CONTENT}`); expect(item0Content).not.toBeNull(); await user.click(item0Content!); expect(onNodeClick).toHaveBeenCalledOnce(); expect(onNodeClick).toHaveBeenCalledWith(expect.objectContaining({ id: 0 }), [0], expect.anything()); const item1Content = screen.getByText("Item 1").closest(`.${Classes.TREE_NODE_CONTENT}`); const item1Caret = item1Content!.querySelector(`.${Classes.TREE_NODE_CARET}`); expect(item1Caret).not.toBeNull(); await user.click(item1Caret!); expect(onNodeExpand).toHaveBeenCalledOnce(); // make sure that onNodeClick isn't fired again, only onNodeExpand should be expect(onNodeClick).toHaveBeenCalledTimes(1); expect(onNodeExpand).toHaveBeenCalledWith(expect.objectContaining({ id: 1 }), [1], expect.anything()); const item6Content = screen.getByText("Item 6").closest(`.${Classes.TREE_NODE_CONTENT}`); expect(item6Content).not.toBeNull(); await user.dblClick(item6Content!); expect(onNodeDoubleClick).toHaveBeenCalledOnce(); expect(onNodeDoubleClick).toHaveBeenCalledWith(expect.objectContaining({ id: 6 }), [3, 0], expect.anything()); const item3Content = screen.getByText("Item 3").closest(`.${Classes.TREE_NODE_CONTENT}`); const item3Caret = item3Content!.querySelector(`.${Classes.TREE_NODE_CARET}`); expect(item3Caret).not.toBeNull(); await user.click(item3Caret!); expect(onNodeCollapse).toHaveBeenCalledOnce(); expect(onNodeCollapse).toHaveBeenCalledWith(expect.objectContaining({ id: 3 }), [3], expect.anything()); fireEvent.contextMenu(item0Content!); expect(onNodeContextMenu).toHaveBeenCalledOnce(); expect(onNodeContextMenu).toHaveBeenCalledWith(expect.objectContaining({ id: 0 }), [0], expect.anything()); const item2Content = screen.getByText("Item 2").closest(`.${Classes.TREE_NODE_CONTENT}`); expect(item2Content).not.toBeNull(); onNodeMouseEnter.mockClear(); onNodeMouseLeave.mockClear(); fireEvent.mouseEnter(item2Content!); expect(onNodeMouseEnter).toHaveBeenCalledOnce(); expect(onNodeMouseEnter).toHaveBeenCalledWith(expect.objectContaining({ id: 2 }), [2], expect.anything()); fireEvent.mouseLeave(item2Content!); expect(onNodeMouseLeave).toHaveBeenCalledOnce(); expect(onNodeMouseLeave).toHaveBeenCalledWith(expect.objectContaining({ id: 2 }), [2], expect.anything()); }); it("if disabled, event callbacks are not fired", async () => { const user = userEvent.setup(); const onNodeClick = vi.fn(); const onNodeCollapse = vi.fn(); const onNodeContextMenu = vi.fn(); const onNodeDoubleClick = vi.fn(); const onNodeExpand = vi.fn(); const onNodeMouseEnter = vi.fn(); const onNodeMouseLeave = vi.fn(); const contents: TreeNodeInfo[] = [ { disabled: true, hasCaret: true, id: 0, isExpanded: false, label: "Item 0" }, ]; render( , ); const item0Content = screen.getByText("Item 0").closest(`.${Classes.TREE_NODE_CONTENT}`); expect(item0Content).not.toBeNull(); const item0Caret = item0Content!.querySelector(`.${Classes.TREE_NODE_CARET}`); expect(item0Caret).not.toBeNull(); await user.click(item0Content!); expect(onNodeClick).not.toHaveBeenCalled(); await user.dblClick(item0Content!); expect(onNodeDoubleClick).not.toHaveBeenCalled(); fireEvent.contextMenu(item0Content!); expect(onNodeContextMenu).not.toHaveBeenCalled(); await user.hover(item0Content!); expect(onNodeMouseEnter).not.toHaveBeenCalled(); await user.unhover(item0Content!); expect(onNodeMouseLeave).not.toHaveBeenCalled(); await user.click(item0Caret!); expect(onNodeExpand).not.toHaveBeenCalled(); await user.click(item0Caret!); expect(onNodeCollapse).not.toHaveBeenCalled(); }); it("disabled nodes are rendered correctly", () => { const contents: TreeNodeInfo[] = [ { disabled: true, id: 0, label: "Item 0" }, { id: 1, label: "Item 1" }, ]; render(); expect(screen.getByText("Item 0").closest(`.${Classes.TREE_NODE}`)).toHaveClass(Classes.DISABLED); expect(screen.getByText("Item 1").closest(`.${Classes.TREE_NODE}`)).not.toHaveClass(Classes.DISABLED); }); it("icons are rendered correctly if present", () => { const contents: TreeNodeInfo[] = [ { id: 0, label: "Item 0" }, { icon:
, id: 1, label: "Item 1" }, ]; render(); expect( screen .getByText("Item 0") .closest(`.${Classes.TREE_NODE_CONTENT}`)! .querySelector(`.${Classes.TREE_NODE_ICON}`), ).toBeNull(); expect(screen.getByText("Item 1").closest(`.${Classes.TREE_NODE}`)).toContainElement( screen.getByTestId("custom-icon-1"), ); }); it("isExpanded controls node expansion", () => { const contents: TreeNodeInfo[] = [ { id: 0, label: "Item 0" }, { childNodes: [{ id: 5, label: "Item 5" }], id: 1, label: "Item 1" }, { id: 2, label: "Item 2" }, { childNodes: [{ id: 6, label: "Item 6" }], id: 3, isExpanded: false, label: "Item 3" }, { childNodes: [{ id: 7, label: "Item 7" }], id: 4, isExpanded: true, label: "Item 4" }, ]; render(); expectIsExpanded(screen.getByText("Item 0"), false); expectIsExpanded(screen.getByText("Item 1"), false); expectIsExpanded(screen.getByText("Item 2"), false); expectIsExpanded(screen.getByText("Item 3"), false); expectIsExpanded(screen.getByText("Item 4"), true); // Item 7 is visible as a child of expanded Item 4 expect(screen.getByText("Item 7")).toBeInTheDocument(); // Item 5 and Item 6 are not visible (parents not expanded) expect(screen.queryByText("Item 5")).not.toBeInTheDocument(); expect(screen.queryByText("Item 6")).not.toBeInTheDocument(); }); it("isSelected selects nodes", () => { const contents: TreeNodeInfo[] = [ { id: 0, isSelected: false, label: "Item 0" }, { id: 1, isSelected: true, label: "Item 1" }, ]; render(); expect(screen.getByText("Item 0").closest(`.${Classes.TREE_NODE}`)).not.toHaveClass(Classes.TREE_NODE_SELECTED); expect(screen.getByText("Item 1").closest(`.${Classes.TREE_NODE}`)).toHaveClass(Classes.TREE_NODE_SELECTED); }); it("secondaryLabel renders correctly", () => { const contents: TreeNodeInfo[] = [ { id: 0, label: "Item 0" }, { id: 1, label: "Item 1", secondaryLabel: "Secondary" }, { id: 2, label: "Item 2", secondaryLabel:

Paragraph

}, ]; render(); const item0Content = screen.getByText("Item 0").closest(`.${Classes.TREE_NODE_CONTENT}`); expect(item0Content).not.toBeNull(); expect(item0Content!.querySelector(`.${Classes.TREE_NODE_SECONDARY_LABEL}`)).toBeNull(); const item1Content = screen.getByText("Item 1").closest(`.${Classes.TREE_NODE_CONTENT}`); expect(item1Content).toContainElement(screen.getByText("Secondary")); const item2Content = screen.getByText("Item 2").closest(`.${Classes.TREE_NODE_CONTENT}`); expect(item2Content).toContainElement(screen.getByText("Paragraph")); }); it("getNodeContentElement returns references to underlying node elements", async () => { const contents: TreeNodeInfo[] = [ { id: 0, label: "Item 0" }, { childNodes: [{ id: 5, label: "Item 5" }], id: 1, isExpanded: true, label: "Item 1" }, ]; const treeRef = createRef(); const { rerender } = render(); expect(treeRef.current).not.toBeNull(); expect(treeRef.current!.getNodeContentElement(5)).toContainElement(screen.getByText("Item 5")); expect(treeRef.current!.getNodeContentElement(100)).toBeUndefined(); contents[1].isExpanded = false; rerender(); // wait for animation to finish await waitFor(() => { expect(treeRef.current!.getNodeContentElement(5)).toBeUndefined(); }); }); it("allows nodes to be removed without throwing", () => { const contents: TreeNodeInfo[] = [ { id: 0, label: "Item 0" }, { id: 1, label: "Item 1" }, ]; const { rerender } = render(); const smallerContents = contents.slice(0, -1); expect(() => rerender()).not.toThrow(); }); }); function expectHasCaret(element: HTMLElement, hasCaret: boolean) { const treeNodeContent = element.closest(`.${Classes.TREE_NODE_CONTENT}`); expect(treeNodeContent).not.toBeNull(); if (hasCaret) { expect(treeNodeContent!.querySelector(`.${Classes.TREE_NODE_CARET}`)).toBeInTheDocument(); } else { expect(treeNodeContent!.querySelector(`.${Classes.TREE_NODE_CARET_NONE}`)).toBeInTheDocument(); } } function expectIsExpanded(element: HTMLElement, isExpanded: boolean) { const treeNode = element.closest(`.${Classes.TREE_NODE}`); expect(treeNode).not.toBeNull(); if (isExpanded) { expect(treeNode).toHaveClass(Classes.TREE_NODE_EXPANDED); } else { expect(treeNode).not.toHaveClass(Classes.TREE_NODE_EXPANDED); } }