/** * @file useChatOverlay.test.ts * @description Tests for the useChatOverlay hook that manages chat overlay state */ import { renderHook, act } from "@testing-library/react"; import { describe, it, expect, beforeEach, vi } from "vitest"; import { useChatOverlay } from "./useChatOverlay"; // Mock localStorage const localStorageMock = ((): Storage => { let store: Record = {}; return { getItem: (key: string): string | null => store[key] ?? null, setItem: (key: string, value: string): void => { store[key] = value; }, removeItem: (key: string): void => { // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete store[key]; }, clear: (): void => { store = {}; }, get length(): number { return Object.keys(store).length; }, key: (index: number): string | null => { const keys = Object.keys(store); return keys[index] ?? null; }, }; })(); Object.defineProperty(window, "localStorage", { value: localStorageMock, }); describe("useChatOverlay", () => { beforeEach(() => { localStorageMock.clear(); vi.clearAllMocks(); }); describe("initial state", () => { it("should initialize with closed and not minimized state", () => { const { result } = renderHook(() => useChatOverlay()); expect(result.current.isOpen).toBe(false); expect(result.current.isMinimized).toBe(false); }); it("should restore state from localStorage if available", () => { localStorageMock.setItem( "chatOverlayState", JSON.stringify({ isOpen: true, isMinimized: true }) ); const { result } = renderHook(() => useChatOverlay()); expect(result.current.isOpen).toBe(true); expect(result.current.isMinimized).toBe(true); }); it("should handle invalid localStorage data gracefully", () => { vi.spyOn(console, "warn").mockImplementation(() => undefined); localStorageMock.setItem("chatOverlayState", "invalid json"); const { result } = renderHook(() => useChatOverlay()); expect(result.current.isOpen).toBe(false); expect(result.current.isMinimized).toBe(false); }); it("should fall back to defaults when localStorage has wrong shape", () => { vi.spyOn(console, "warn").mockImplementation(() => undefined); // Valid JSON but wrong shape localStorageMock.setItem("chatOverlayState", JSON.stringify({ isOpen: "yes", wrong: true })); const { result } = renderHook(() => useChatOverlay()); expect(result.current.isOpen).toBe(false); expect(result.current.isMinimized).toBe(false); }); it("should fall back to defaults when localStorage has null value parsed", () => { vi.spyOn(console, "warn").mockImplementation(() => undefined); localStorageMock.setItem("chatOverlayState", "null"); const { result } = renderHook(() => useChatOverlay()); expect(result.current.isOpen).toBe(false); expect(result.current.isMinimized).toBe(false); }); it("should fall back to defaults when localStorage has array instead of object", () => { vi.spyOn(console, "warn").mockImplementation(() => undefined); localStorageMock.setItem("chatOverlayState", JSON.stringify([true, false])); const { result } = renderHook(() => useChatOverlay()); expect(result.current.isOpen).toBe(false); expect(result.current.isMinimized).toBe(false); }); }); describe("open", () => { it("should open the chat overlay", () => { const { result } = renderHook(() => useChatOverlay()); act(() => { result.current.open(); }); expect(result.current.isOpen).toBe(true); expect(result.current.isMinimized).toBe(false); }); it("should persist state to localStorage when opening", () => { const { result } = renderHook(() => useChatOverlay()); act(() => { result.current.open(); }); const stored = JSON.parse(localStorageMock.getItem("chatOverlayState") ?? "{}") as { isOpen: boolean; isMinimized: boolean; }; expect(stored.isOpen).toBe(true); expect(stored.isMinimized).toBe(false); }); }); describe("close", () => { it("should close the chat overlay", () => { const { result } = renderHook(() => useChatOverlay()); act(() => { result.current.open(); }); act(() => { result.current.close(); }); expect(result.current.isOpen).toBe(false); }); it("should persist state to localStorage when closing", () => { const { result } = renderHook(() => useChatOverlay()); act(() => { result.current.open(); }); act(() => { result.current.close(); }); const stored = JSON.parse(localStorageMock.getItem("chatOverlayState") ?? "{}") as { isOpen: boolean; isMinimized: boolean; }; expect(stored.isOpen).toBe(false); }); }); describe("minimize", () => { it("should minimize the chat overlay", () => { const { result } = renderHook(() => useChatOverlay()); act(() => { result.current.open(); }); act(() => { result.current.minimize(); }); expect(result.current.isOpen).toBe(true); expect(result.current.isMinimized).toBe(true); }); it("should persist minimized state to localStorage", () => { const { result } = renderHook(() => useChatOverlay()); act(() => { result.current.open(); }); act(() => { result.current.minimize(); }); const stored = JSON.parse(localStorageMock.getItem("chatOverlayState") ?? "{}") as { isOpen: boolean; isMinimized: boolean; }; expect(stored.isMinimized).toBe(true); }); }); describe("expand", () => { it("should expand the minimized chat overlay", () => { const { result } = renderHook(() => useChatOverlay()); act(() => { result.current.open(); result.current.minimize(); }); act(() => { result.current.expand(); }); expect(result.current.isMinimized).toBe(false); }); it("should persist expanded state to localStorage", () => { const { result } = renderHook(() => useChatOverlay()); act(() => { result.current.open(); result.current.minimize(); }); act(() => { result.current.expand(); }); const stored = JSON.parse(localStorageMock.getItem("chatOverlayState") ?? "{}") as { isOpen: boolean; isMinimized: boolean; }; expect(stored.isMinimized).toBe(false); }); }); describe("toggle", () => { it("should toggle the chat overlay open state", () => { const { result } = renderHook(() => useChatOverlay()); // Initially closed expect(result.current.isOpen).toBe(false); // Toggle to open act(() => { result.current.toggle(); }); expect(result.current.isOpen).toBe(true); // Toggle to close act(() => { result.current.toggle(); }); expect(result.current.isOpen).toBe(false); }); it("should not change minimized state when toggling", () => { const { result } = renderHook(() => useChatOverlay()); act(() => { result.current.open(); result.current.minimize(); }); expect(result.current.isMinimized).toBe(true); act(() => { result.current.toggle(); }); // Should close but keep minimized state for next open expect(result.current.isOpen).toBe(false); }); }); describe("toggleMinimize", () => { it("should toggle the minimize state", () => { const { result } = renderHook(() => useChatOverlay()); act(() => { result.current.open(); }); // Initially not minimized expect(result.current.isMinimized).toBe(false); // Toggle to minimized act(() => { result.current.toggleMinimize(); }); expect(result.current.isMinimized).toBe(true); // Toggle back to expanded act(() => { result.current.toggleMinimize(); }); expect(result.current.isMinimized).toBe(false); }); }); });