116 lines
4.1 KiB
TypeScript
116 lines
4.1 KiB
TypeScript
import { act, cleanup, renderHook } from "@testing-library/react";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
import type { DockviewPanelApi } from "dockview-react";
|
|
import { panelPortalManager } from "@/lib/layout/panel-portal-manager";
|
|
import { usePanelActive } from "./use-panel-active";
|
|
|
|
afterEach(() => {
|
|
cleanup();
|
|
});
|
|
|
|
type FakePanelApi = {
|
|
isActive: boolean;
|
|
onDidActiveChange: (cb: (event: { isActive: boolean }) => void) => { dispose: () => void };
|
|
};
|
|
|
|
type FakeApiHandle = {
|
|
fakeApi: FakePanelApi;
|
|
fireActiveChange: (isActive: boolean) => void;
|
|
};
|
|
|
|
function makeFakeApi(initialIsActive: boolean): FakeApiHandle {
|
|
let listener: ((event: { isActive: boolean }) => void) | null = null;
|
|
const fakeApi: FakePanelApi = {
|
|
isActive: initialIsActive,
|
|
onDidActiveChange: (cb) => {
|
|
listener = cb;
|
|
return { dispose: () => (listener = null) };
|
|
},
|
|
};
|
|
return {
|
|
fakeApi,
|
|
fireActiveChange: (isActive: boolean) => {
|
|
fakeApi.isActive = isActive;
|
|
listener?.({ isActive });
|
|
},
|
|
};
|
|
}
|
|
|
|
function acquirePanel(panelId: string, initialIsActive: boolean): FakeApiHandle {
|
|
const handle = makeFakeApi(initialIsActive);
|
|
// Test double: only the subset of DockviewPanelApi this hook actually
|
|
// reads/calls is implemented. Unchecked cast is intentional here — the
|
|
// full interface has 30+ unrelated members no test in this file exercises.
|
|
const dockviewApi = handle.fakeApi as unknown as DockviewPanelApi;
|
|
panelPortalManager.acquire(panelId, "chat", {}, dockviewApi);
|
|
return handle;
|
|
}
|
|
|
|
describe("usePanelActive", () => {
|
|
afterEach(() => {
|
|
// Release any panels acquired by a test so state doesn't leak across cases.
|
|
for (const id of panelPortalManager.ids()) panelPortalManager.release(id);
|
|
});
|
|
|
|
it("returns false before a portal entry/api has been registered for the panel", () => {
|
|
const { result } = renderHook(() => usePanelActive("panel-unregistered"));
|
|
expect(result.current).toBe(false);
|
|
});
|
|
|
|
it("reflects the panel's initial isActive state once registered", () => {
|
|
acquirePanel("panel-active", true);
|
|
const { result } = renderHook(() => usePanelActive("panel-active"));
|
|
expect(result.current).toBe(true);
|
|
|
|
acquirePanel("panel-inactive", false);
|
|
const { result: result2 } = renderHook(() => usePanelActive("panel-inactive"));
|
|
expect(result2.current).toBe(false);
|
|
});
|
|
|
|
it("updates when the panel's active tab changes via onDidActiveChange", () => {
|
|
const handle = acquirePanel("panel-toggle", false);
|
|
const { result } = renderHook(() => usePanelActive("panel-toggle"));
|
|
expect(result.current).toBe(false);
|
|
|
|
act(() => handle.fireActiveChange(true));
|
|
expect(result.current).toBe(true);
|
|
|
|
act(() => handle.fireActiveChange(false));
|
|
expect(result.current).toBe(false);
|
|
});
|
|
|
|
it("falls back to false again if the panel is released", () => {
|
|
const PANEL_ID = "panel-released";
|
|
acquirePanel(PANEL_ID, true);
|
|
const { result, rerender } = renderHook(({ id }: { id: string }) => usePanelActive(id), {
|
|
initialProps: { id: PANEL_ID },
|
|
});
|
|
expect(result.current).toBe(true);
|
|
|
|
panelPortalManager.release(PANEL_ID);
|
|
rerender({ id: PANEL_ID });
|
|
expect(result.current).toBe(false);
|
|
});
|
|
|
|
it("rebinds to the new panel api when Dockview remounts (replaces) an existing portal entry", () => {
|
|
const PANEL_ID = "panel-remount";
|
|
const stale = acquirePanel(PANEL_ID, false);
|
|
const { result } = renderHook(() => usePanelActive(PANEL_ID));
|
|
expect(result.current).toBe(false);
|
|
|
|
// Simulate Dockview calling api.fromJSON() during a layout restore: the
|
|
// same panelId is re-acquired with a brand-new DockviewPanelApi,
|
|
// without the old one ever being released.
|
|
const fresh = acquirePanel(PANEL_ID, false);
|
|
|
|
act(() => fresh.fireActiveChange(true));
|
|
expect(result.current).toBe(true);
|
|
|
|
// The stale api's listener must have been torn down — firing it no
|
|
// longer moves the hook (it would incorrectly flip back to false
|
|
// without the resubscribe fix).
|
|
act(() => stale.fireActiveChange(false));
|
|
expect(result.current).toBe(true);
|
|
});
|
|
});
|