// @ts-expect-error This is an experimental API // eslint-disable-next-line no-restricted-imports import { unstable_Activity as Activity, Fragment } from "react"; import { Root, createRoot } from "react-dom/client"; import { act } from "react-dom/test-utils"; import { ImperativePanelGroupHandle, ImperativePanelHandle, Panel, PanelGroup, PanelResizeHandle, getPanelElement, } from "."; import { assert } from "./utils/assert"; import { getPanelGroupElement } from "./utils/dom/getPanelGroupElement"; import { mockPanelGroupOffsetWidthAndHeight, verifyAttribute, } from "./utils/test-utils"; import { createRef } from "./vendor/react"; describe("PanelGroup", () => { let expectedWarnings: string[] = []; let root: Root; let container: HTMLElement; let uninstallMockOffsetWidthAndHeight: () => void; function expectWarning(expectedMessage: string) { expectedWarnings.push(expectedMessage); } beforeEach(() => { // @ts-expect-error global.IS_REACT_ACT_ENVIRONMENT = true; // JSDom doesn't support element sizes uninstallMockOffsetWidthAndHeight = mockPanelGroupOffsetWidthAndHeight(); 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(() => { uninstallMockOffsetWidthAndHeight(); jest.clearAllMocks(); jest.resetModules(); act(() => { root.unmount(); }); expect(expectedWarnings).toHaveLength(0); }); it("should recalculate layout after being hidden by Activity", () => { const panelRef = createRef(); let mostRecentLayout: number[] | null = null; const onLayout = (layout: number[]) => { mostRecentLayout = layout; }; act(() => { root.render( ); }); expect(mostRecentLayout).toEqual([60, 40]); expect(panelRef.current?.getSize()).toEqual(60); const leftPanelElement = getPanelElement("left"); const rightPanelElement = getPanelElement("right"); expect(leftPanelElement?.getAttribute("data-panel-size")).toBe("60.0"); expect(rightPanelElement?.getAttribute("data-panel-size")).toBe("40.0"); act(() => { root.render( ); }); act(() => { root.render( ); }); expect(mostRecentLayout).toEqual([60, 40]); expect(panelRef.current?.getSize()).toEqual(60); // This bug is only observable in the DOM; callbacks will not re-fire expect(leftPanelElement?.getAttribute("data-panel-size")).toBe("60.0"); expect(rightPanelElement?.getAttribute("data-panel-size")).toBe("40.0"); }); // github.com/bvaughn/react-resizable-panels/issues/303 it("should recalculate layout after panels are changed", () => { let mostRecentLayout: number[] | null = null; const onLayout = (layout: number[]) => { mostRecentLayout = layout; }; act(() => { root.render( ); }); expect(mostRecentLayout).toEqual([30, 70]); act(() => { root.render( ); }); expect(mostRecentLayout).toEqual([100]); }); describe("imperative handle API", () => { it("should report the most recently rendered group id", () => { const ref = createRef(); act(() => { root.render(); }); expect(ref.current?.getId()).toBe("one"); act(() => { root.render(); }); expect(ref.current?.getId()).toBe("two"); }); it("should get and set layouts", () => { const ref = createRef(); let mostRecentLayout: number[] | null = null; const onLayout = (layout: number[]) => { mostRecentLayout = layout; }; act(() => { root.render( ); }); expect(mostRecentLayout).toEqual([50, 50]); act(() => { ref.current?.setLayout([25, 75]); }); expect(mostRecentLayout).toEqual([25, 75]); }); }); it("should support ...rest attributes", () => { act(() => { root.render( ); }); const element = getPanelGroupElement("group", container); assert(element, ""); expect(element.tabIndex).toBe(123); expect(element.getAttribute("data-test-name")).toBe("foo"); expect(element.title).toBe("bar"); }); describe("callbacks", () => { describe("onLayout", () => { it("should be called with the initial group layout on mount", () => { let onLayout = jest.fn(); act(() => { root.render( ); }); expect(onLayout).toHaveBeenCalledTimes(1); expect(onLayout).toHaveBeenCalledWith([35, 65]); }); it("should be called any time the group layout changes", () => { let onLayout = jest.fn(); let panelGroupRef = createRef(); let panelRef = createRef(); act(() => { root.render( ); }); onLayout.mockReset(); act(() => { panelGroupRef.current?.setLayout([25, 75]); }); expect(onLayout).toHaveBeenCalledTimes(1); expect(onLayout).toHaveBeenCalledWith([25, 75]); onLayout.mockReset(); act(() => { panelRef.current?.resize(50); }); expect(onLayout).toHaveBeenCalledTimes(1); expect(onLayout).toHaveBeenCalledWith([50, 50]); }); }); }); describe("data attributes", () => { it("should initialize with the correct props based attributes", () => { act(() => { root.render( ); }); const element = getPanelGroupElement("test-group", container); assert(element, ""); verifyAttribute(element, "data-panel-group", ""); verifyAttribute(element, "data-panel-group-direction", "horizontal"); verifyAttribute(element, "data-panel-group-id", "test-group"); }); }); describe("a11y", () => { it("should pass explicit id prop to DOM", () => { act(() => { root.render( ); }); const element = container.querySelector("[data-panel-group]"); 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-panel-group]"); expect(element).not.toBeNull(); expect(element?.getAttribute("id")).toBeNull(); }); }); describe("DEV warnings", () => { it("should warn about unstable layouts without id and order props", () => { act(() => { root.render( ); }); expectWarning( "Panel id and order props recommended when panels are dynamically rendered" ); act(() => { root.render( ); }); }); it("should warn about missing resize handles", () => { expectWarning( 'Missing resize handle for PanelGroup "group-without-handle"' ); act(() => { root.render( ); }); }); it("should warn about an invalid declarative layout", () => { expectWarning("Invalid layout total size: 60%, 80%"); act(() => { root.render( ); }); }); it("should warn about an invalid layout set via the imperative api", () => { const ref = createRef(); act(() => { root.render( ); }); expectWarning("Invalid layout total size: 60%, 80%"); act(() => { ref.current?.setLayout([60, 80]); }); }); it("should warn about an empty layout", () => { act(() => { root.render( ); }); // Since the layout is empty, no warning is expected (even though the sizes won't total 100%) act(() => { root.render( ); }); }); }); });