import { Root, createRoot } from "react-dom/client"; import { act } from "react-dom/test-utils"; import { Panel, PanelGroup, PanelResizeHandle, type PanelResizeHandleProps, } from "."; import { assert } from "./utils/assert"; import * as cursorUtils from "./utils/cursor"; import { getResizeHandleElement } from "./utils/dom/getResizeHandleElement"; import { dispatchPointerEvent, mockBoundingClientRect, verifyAttribute, } from "./utils/test-utils"; jest.mock("./utils/cursor", () => ({ getCursorStyle: jest.fn(), resetGlobalCursorStyle: jest.fn(), setGlobalCursorStyle: jest.fn(), })); describe("PanelResizeHandle", () => { let expectedWarnings: string[] = []; let root: Root; let container: HTMLElement; beforeEach(() => { // @ts-expect-error global.IS_REACT_ACT_ENVIRONMENT = true; container = document.createElement("div"); document.body.appendChild(container); expectedWarnings = []; root = createRoot(container); jest.spyOn(console, "warn").mockImplementation((actualMessage: string) => { const match = expectedWarnings.findIndex((expectedMessage) => { return actualMessage.includes(expectedMessage); }); if (match >= 0) { expectedWarnings.splice(match, 1); return; } throw Error(`Unexpected warning: ${actualMessage}`); }); }); afterEach(() => { jest.clearAllMocks(); jest.resetModules(); act(() => { root.unmount(); }); expect(expectedWarnings).toHaveLength(0); }); it("should support ...rest attributes", () => { act(() => { root.render( ); }); const element = getResizeHandleElement("handle", container); assert(element, ""); expect(element.tabIndex).toBe(123); expect(element.getAttribute("data-test-name")).toBe("foo"); expect(element.title).toBe("bar"); }); function setupMockedGroup({ leftProps = {}, rightProps = {}, }: { leftProps?: Partial; rightProps?: Partial; } = {}) { act(() => { root.render( ); }); const leftElement = getResizeHandleElement("handle-left", container); const rightElement = getResizeHandleElement("handle-right", container); assert(leftElement, ""); assert(rightElement, ""); // JSDom doesn't properly handle bounding rects mockBoundingClientRect(leftElement, { x: 50, y: 0, height: 50, width: 2, }); mockBoundingClientRect(rightElement, { x: 100, y: 0, height: 50, width: 2, }); return { leftElement, rightElement, }; } describe("callbacks", () => { describe("onDragging", () => { it("should fire when dragging starts/stops", () => { const onDragging = jest.fn(); const { leftElement } = setupMockedGroup({ leftProps: { onDragging }, }); act(() => { dispatchPointerEvent("pointermove", leftElement); }); expect(onDragging).not.toHaveBeenCalled(); act(() => { dispatchPointerEvent("pointerdown", leftElement); }); expect(onDragging).toHaveBeenCalledTimes(1); expect(onDragging).toHaveBeenCalledWith(true); act(() => { dispatchPointerEvent("pointerup", leftElement); }); expect(onDragging).toHaveBeenCalledTimes(2); expect(onDragging).toHaveBeenCalledWith(false); }); it("should only fire for the handle that has been dragged", () => { const onDraggingLeft = jest.fn(); const onDraggingRight = jest.fn(); const { leftElement } = setupMockedGroup({ leftProps: { onDragging: onDraggingLeft }, rightProps: { onDragging: onDraggingRight }, }); act(() => { dispatchPointerEvent("pointermove", leftElement); }); expect(onDraggingLeft).not.toHaveBeenCalled(); expect(onDraggingRight).not.toHaveBeenCalled(); act(() => { dispatchPointerEvent("pointerdown", leftElement); }); expect(onDraggingLeft).toHaveBeenCalledTimes(1); expect(onDraggingLeft).toHaveBeenCalledWith(true); expect(onDraggingRight).not.toHaveBeenCalled(); act(() => { dispatchPointerEvent("pointerup", leftElement); }); expect(onDraggingLeft).toHaveBeenCalledTimes(2); expect(onDraggingLeft).toHaveBeenCalledWith(false); expect(onDraggingRight).not.toHaveBeenCalled(); }); }); }); describe("data attributes", () => { it("should initialize with the correct props based attributes", () => { const { leftElement, rightElement } = setupMockedGroup(); verifyAttribute(leftElement, "data-panel-group-id", "test-group"); verifyAttribute(leftElement, "data-resize-handle", ""); verifyAttribute(leftElement, "data-panel-group-direction", "horizontal"); verifyAttribute(leftElement, "data-panel-resize-handle-enabled", "true"); verifyAttribute( leftElement, "data-panel-resize-handle-id", "handle-left" ); verifyAttribute(rightElement, "data-panel-group-id", "test-group"); verifyAttribute(rightElement, "data-resize-handle", ""); verifyAttribute(rightElement, "data-panel-group-direction", "horizontal"); verifyAttribute(rightElement, "data-panel-resize-handle-enabled", "true"); verifyAttribute( rightElement, "data-panel-resize-handle-id", "handle-right" ); }); it("should update data-resize-handle-active and data-resize-handle-state when dragging starts/stops", () => { const { leftElement, rightElement } = setupMockedGroup(); verifyAttribute(leftElement, "data-resize-handle-active", null); verifyAttribute(rightElement, "data-resize-handle-active", null); verifyAttribute(leftElement, "data-resize-handle-state", "inactive"); verifyAttribute(rightElement, "data-resize-handle-state", "inactive"); act(() => { dispatchPointerEvent("pointermove", leftElement); }); verifyAttribute(leftElement, "data-resize-handle-active", null); verifyAttribute(rightElement, "data-resize-handle-active", null); verifyAttribute(leftElement, "data-resize-handle-state", "hover"); verifyAttribute(rightElement, "data-resize-handle-state", "inactive"); act(() => { dispatchPointerEvent("pointerdown", leftElement); }); verifyAttribute(leftElement, "data-resize-handle-active", "pointer"); verifyAttribute(rightElement, "data-resize-handle-active", null); verifyAttribute(leftElement, "data-resize-handle-state", "drag"); verifyAttribute(rightElement, "data-resize-handle-state", "inactive"); act(() => { dispatchPointerEvent("pointermove", leftElement); }); verifyAttribute(leftElement, "data-resize-handle-active", "pointer"); verifyAttribute(rightElement, "data-resize-handle-active", null); verifyAttribute(leftElement, "data-resize-handle-state", "drag"); verifyAttribute(rightElement, "data-resize-handle-state", "inactive"); act(() => { dispatchPointerEvent("pointerup", leftElement); }); verifyAttribute(leftElement, "data-resize-handle-active", null); verifyAttribute(rightElement, "data-resize-handle-active", null); verifyAttribute(leftElement, "data-resize-handle-state", "hover"); verifyAttribute(rightElement, "data-resize-handle-state", "inactive"); act(() => { dispatchPointerEvent("pointermove", rightElement); }); verifyAttribute(leftElement, "data-resize-handle-active", null); verifyAttribute(rightElement, "data-resize-handle-active", null); verifyAttribute(leftElement, "data-resize-handle-state", "inactive"); verifyAttribute(rightElement, "data-resize-handle-state", "hover"); }); it("should update data-resize-handle-active when focused", () => { const { leftElement, rightElement } = setupMockedGroup(); verifyAttribute(leftElement, "data-resize-handle-active", null); verifyAttribute(rightElement, "data-resize-handle-active", null); act(() => { leftElement.focus(); }); expect(document.activeElement).toBe(leftElement); verifyAttribute(leftElement, "data-resize-handle-active", "keyboard"); verifyAttribute(rightElement, "data-resize-handle-active", null); act(() => { leftElement.blur(); }); expect(document.activeElement).not.toBe(leftElement); verifyAttribute(leftElement, "data-resize-handle-active", null); verifyAttribute(rightElement, "data-resize-handle-active", null); }); }); describe("a11y", () => { it("should pass explicit id prop to DOM", () => { act(() => { root.render( ); }); const element = container.querySelector("[data-resize-handle]"); expect(element).not.toBeNull(); expect(element?.getAttribute("id")).toBe("explicit-id"); }); it("should not pass auto-generated id prop to DOM", () => { act(() => { root.render( ); }); const element = container.querySelector("[data-resize-handle]"); expect(element).not.toBeNull(); expect(element?.getAttribute("id")).toBeNull(); }); }); it("resets the global cursor style on unmount", () => { const onDraggingLeft = jest.fn(); const { leftElement } = setupMockedGroup({ leftProps: { onDragging: onDraggingLeft }, rightProps: {}, }); act(() => { dispatchPointerEvent("pointermove", leftElement); }); act(() => { dispatchPointerEvent("pointerdown", leftElement); }); expect(onDraggingLeft).toHaveBeenCalledTimes(1); expect(onDraggingLeft).toHaveBeenCalledWith(true); expect(cursorUtils.resetGlobalCursorStyle).not.toHaveBeenCalled(); expect(cursorUtils.setGlobalCursorStyle).toHaveBeenCalled(); onDraggingLeft.mockReset(); act(() => { dispatchPointerEvent("pointermove", leftElement); }); expect(onDraggingLeft).not.toHaveBeenCalled(); act(() => { dispatchPointerEvent("pointerup", leftElement); }); expect(onDraggingLeft).toHaveBeenCalledTimes(1); expect(onDraggingLeft).toHaveBeenCalledWith(false); onDraggingLeft.mockReset(); act(() => { dispatchPointerEvent("pointermove", leftElement); }); expect(onDraggingLeft).not.toHaveBeenCalled(); act(() => { root.unmount(); }); expect(cursorUtils.resetGlobalCursorStyle).toHaveBeenCalled(); }); });