diff --git a/src/Explorer/Controls/TreeComponent/TreeNodeComponent.test.tsx b/src/Explorer/Controls/TreeComponent/TreeNodeComponent.test.tsx index 2b12de615..42a86cdc4 100644 --- a/src/Explorer/Controls/TreeComponent/TreeNodeComponent.test.tsx +++ b/src/Explorer/Controls/TreeComponent/TreeNodeComponent.test.tsx @@ -1,10 +1,72 @@ import { TreeItem, TreeItemLayout } from "@fluentui/react-components"; +import type { Frame } from "@playwright/test"; import PromiseSource from "Utils/PromiseSource"; import { mount, shallow } from "enzyme"; import React from "react"; import { act } from "react-dom/test-utils"; +import { DataExplorer } from "../../../../test/fx"; import { TreeNode, TreeNodeComponent } from "./TreeNodeComponent"; +jest.mock("@playwright/test", () => ({})); + +describe("Tree expansion helper", () => { + const createNode = () => { + const expandIcon = { click: jest.fn().mockResolvedValue(undefined) }; + const row = { locator: jest.fn().mockReturnValue(expandIcon), click: jest.fn() }; + const container = { getAttribute: jest.fn().mockResolvedValue("false") }; + const tree = { waitFor: jest.fn().mockResolvedValue(undefined) }; + const frame = { + getByTestId: jest.fn((id: string) => { + if (id === "TreeNode:root") { + return row; + } + if (id === "TreeNodeContainer:root") { + return container; + } + if (id === "Tree:root") { + return tree; + } + throw new Error(`Unexpected locator: ${id}`); + }), + } as unknown as Frame; + return { node: new DataExplorer(frame).treeNode("root"), row, container, tree, expandIcon }; + }; + + it("clicks the expand icon rather than the row that contains menu actions", async () => { + const { node, row, expandIcon } = createNode(); + await node.expand(); + expect(row.locator).toHaveBeenCalledWith(":scope > .fui-TreeItemLayout__expandIcon"); + expect(expandIcon.click).toHaveBeenCalledTimes(1); + expect(row.click).not.toHaveBeenCalled(); + }); + + it("does not toggle a node that is already expanded", async () => { + const { node, row, container, tree, expandIcon } = createNode(); + container.getAttribute.mockResolvedValue("true"); + await node.expand(); + expect(tree.waitFor).toHaveBeenCalled(); + expect(expandIcon.click).not.toHaveBeenCalled(); + expect(row.click).not.toHaveBeenCalled(); + }); + + it("uses the expand icon again when a collapsed node needs a retry", async () => { + const { node, row, tree, expandIcon } = createNode(); + tree.waitFor.mockRejectedValueOnce(new Error("Children are not visible yet")); + await node.expand(); + expect(expandIcon.click).toHaveBeenCalledTimes(2); + expect(row.click).not.toHaveBeenCalled(); + }); + + it("does not collapse a node that expanded while waiting for children", async () => { + const { node, container, tree, expandIcon } = createNode(); + container.getAttribute.mockResolvedValueOnce("false").mockResolvedValue("true"); + tree.waitFor.mockRejectedValueOnce(new Error("Children are not visible yet")); + await node.expand(); + expect(expandIcon.click).toHaveBeenCalledTimes(1); + expect(tree.waitFor).toHaveBeenCalledTimes(2); + }); +}); + function generateTestNode(id: string, additionalProps?: Partial): TreeNode { const node: TreeNode = { id, diff --git a/src/Explorer/Tabs/Tabs.tsx b/src/Explorer/Tabs/Tabs.tsx index e36a7da04..40966d04f 100644 --- a/src/Explorer/Tabs/Tabs.tsx +++ b/src/Explorer/Tabs/Tabs.tsx @@ -1,4 +1,4 @@ -import { Spinner, SpinnerSize, TooltipHost } from "@fluentui/react"; +import { ITooltipHost, Spinner, SpinnerSize, TooltipHost } from "@fluentui/react"; import { Menu, MenuItem, MenuList, MenuPopover, MenuTrigger } from "@fluentui/react-components"; import { CollectionTabKind } from "Contracts/ViewModels"; import Explorer from "Explorer/Explorer"; @@ -74,6 +74,7 @@ export const Tabs = ({ explorer }: TabsProps): JSX.Element => { function TabNav({ tab, active, tabKind }: { tab?: Tab; active: boolean; tabKind?: ReactTabKind }) { const [hovering, setHovering] = useState(false); const focusTab = useRef() as MutableRefObject; + const tabTooltip = useRef(null); const tabId = tab ? tab.tabId : ""; const getReactTabTitle = (): ko.Observable => { @@ -85,6 +86,7 @@ function TabNav({ tab, active, tabKind }: { tab?: Tab; active: boolean; tabKind? useEffect(() => { if (active && focusTab.current) { focusTab.current.focus(); + tabTooltip.current?.dismiss(); } }, [active]); const liElement = ( @@ -98,7 +100,7 @@ function TabNav({ tab, active, tabKind }: { tab?: Tab; active: boolean; tabKind? >
- + { diff --git a/src/Explorer/Tabs/useTabs.test.ts b/src/Explorer/Tabs/useTabs.test.ts index 9d4925a2c..1093fccd2 100644 --- a/src/Explorer/Tabs/useTabs.test.ts +++ b/src/Explorer/Tabs/useTabs.test.ts @@ -1,11 +1,69 @@ +import { act, cleanup, fireEvent, render } from "@testing-library/react"; import { DocumentsTabV2 } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2"; import * as ko from "knockout"; +import React from "react"; import * as ViewModels from "../../Contracts/ViewModels"; import { updateUserContext } from "../../UserContext"; import { useTabs } from "../../hooks/useTabs"; import { container } from "../Controls/Settings/TestUtils"; import DocumentId from "../Tree/DocumentId"; import { NewQueryTab } from "./QueryTab/QueryTab"; +import { Tabs } from "./Tabs"; +import TabsBase from "./TabsBase"; + +describe("Tab path tooltips", () => { + const renderActiveTab = () => { + const tab = Object.assign( + new TabsBase({ tabKind: ViewModels.CollectionTabKind.Query, title: "Query 1", tabPath: "" }), + { render: () => React.createElement("button", null, "Execute Query") }, + ); + tab.tabPath = ko.observable("t_34646986695_1_dbc3_1789160465183>testcontainer>Query 1"); + useTabs.setState({ openedTabs: [tab], activeTab: tab }); + return render(React.createElement(Tabs, { explorer: container })); + }; + + beforeEach(() => { + jest.useFakeTimers(); + useTabs.setState({ openedTabs: [], openedReactTabs: [], activeTab: undefined, activeReactTab: undefined }); + }); + + afterEach(() => { + cleanup(); + jest.clearAllTimers(); + jest.useRealTimers(); + useTabs.setState({ openedTabs: [], activeTab: undefined }); + }); + + it("focuses an activated tab without opening its path tooltip", () => { + const view = renderActiveTab(); + expect(document.activeElement).toBe(view.getByRole("tab", { name: "Query 1" })); + + act(() => jest.advanceTimersByTime(1000)); + expect(document.querySelector(".ms-Tooltip")).toBeNull(); + }); + + it.each(["hover", "focus"])("keeps the path tooltip available on later %s and dismissible with Escape", (trigger) => { + const view = renderActiveTab(); + const tab = view.getByRole("tab", { name: "Query 1" }); + act(() => jest.advanceTimersByTime(1000)); + + if (trigger === "hover") { + fireEvent.mouseEnter(tab); + } else { + act(() => { + view.getByRole("button", { name: "Execute Query" }).focus(); + tab.focus(); + }); + } + + act(() => jest.advanceTimersByTime(1000)); + expect(document.querySelector(".ms-Tooltip")?.textContent).toContain("testcontainer>Query 1"); + + fireEvent.keyDown(tab, { key: "Escape", keyCode: 27, which: 27 }); + expect(document.querySelector(".ms-Tooltip")).toBeNull(); + expect(document.activeElement).toBe(tab); + }); +}); describe("useTabs tests", () => { let database: ViewModels.Database; diff --git a/test/fx.ts b/test/fx.ts index c4a156ed2..337964c56 100644 --- a/test/fx.ts +++ b/test/fx.ts @@ -118,6 +118,7 @@ function tryGetStandardName(accountType: TestAccount) { : `${process.env.DE_TEST_ACCOUNT_PREFIX}-`; return `${actualPrefix}${accountType.toLocaleLowerCase()}`; } + return undefined; } // Maps a base API account type to its dedicated connection string (account key) account. @@ -328,12 +329,12 @@ class TreeNode { async expand(): Promise { const treeNodeContainer = this.frame.getByTestId(`TreeNodeContainer:${this.id}`); const tree = this.frame.getByTestId(`Tree:${this.id}`); + const expandIcon = this.element.locator(":scope > .fui-TreeItemLayout__expandIcon"); // eslint-disable-next-line prefer-arrow/prefer-arrow-functions const expandNode = async () => { if ((await treeNodeContainer.getAttribute("aria-expanded")) !== "true") { - // Click the node, to trigger loading and expansion - await this.element.click(); + await expandIcon.click(); } // Try three times to wait for the node to expand. @@ -346,7 +347,7 @@ class TreeNode { // Just try again if ((await treeNodeContainer.getAttribute("aria-expanded")) !== "true") { // We might have collapsed the node, try expanding it again, then retry. - await this.element.click(); + await expandIcon.click(); } } } diff --git a/test/sql/indexAdvisor.spec.ts b/test/sql/indexAdvisor.spec.ts index a52d1dc23..b64613827 100644 --- a/test/sql/indexAdvisor.spec.ts +++ b/test/sql/indexAdvisor.spec.ts @@ -1,6 +1,6 @@ import { expect, test, type Page } from "@playwright/test"; -import { CommandBarButton, DataExplorer, TestAccount } from "../fx"; +import { CommandBarButton, DataExplorer, generateUniqueName, TestAccount } from "../fx"; import { createTestSQLContainer, TestContainerContext } from "../testData"; // Test container context for setup and cleanup @@ -10,7 +10,10 @@ let CONTAINER_ID: string; // Set up test database and container with data before all tests test.beforeAll(async () => { - testContainer = await createTestSQLContainer({ includeTestData: true }); + testContainer = await createTestSQLContainer({ + includeTestData: true, + databaseName: generateUniqueName("db_tab_tooltip_with_a_long_resource_name"), + }); DATABASE_ID = testContainer.database.id; CONTAINER_ID = testContainer.container.id; });