import useBodyScrollLock from "./use-body-scroll-lock"; import { cleanup, renderHook } from "@testing-library/react"; describe("useBodyScrollLock", () => { beforeEach(() => { cleanup(); document.body.style.overflow = ""; document.body.style.marginRight = ""; }); afterEach(() => { cleanup(); document.body.style.overflow = ""; document.body.style.marginRight = ""; }); describe("Scroll locking (isOpen=true)", () => { it("sets body overflow to hidden when isOpen is true", () => { renderHook(() => useBodyScrollLock(true, false)); expect(document.body.style.overflow).toBe("hidden"); }); it("sets marginRight based on scrollbar width when isOpen is true", () => { renderHook(() => useBodyScrollLock(true, false)); const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth; expect(document.body.style.marginRight).toBe(`${scrollbarWidth}px`); }); }); describe("Scroll unlocking (isOpen=false)", () => { it("resets overflow to unset when isOpen=false and hideScrollOnIsOpenFalse=true", () => { renderHook(() => useBodyScrollLock(false, true)); expect(document.body.style.overflow).toBe("unset"); }); it("preserves existing overflow when isOpen=false and hideScrollOnIsOpenFalse=false", () => { document.body.style.overflow = "auto"; renderHook(() => useBodyScrollLock(false, false)); expect(document.body.style.overflow).toBe("auto"); }); it("attempts to set marginRight to unset when isOpen is false", () => { const calls: string[] = []; const original = Object.getOwnPropertyDescriptor( CSSStyleDeclaration.prototype, "marginRight" ); Object.defineProperty(document.body.style, "marginRight", { set(val: string) { calls.push(val); }, get() { return ""; }, configurable: true, }); renderHook(() => useBodyScrollLock(false, false)); expect(calls).toContain("unset"); // Restore if (original) { Object.defineProperty( CSSStyleDeclaration.prototype, "marginRight", original ); } }); it("attempts to set marginRight to unset for isOpen false regardless of hideScroll", () => { const calls: string[] = []; Object.defineProperty(document.body.style, "marginRight", { set(val: string) { calls.push(val); }, get() { return ""; }, configurable: true, }); renderHook(() => useBodyScrollLock(false, true)); expect(calls).toContain("unset"); }); }); describe("Cleanup on unmount", () => { it("resets body overflow to unset on unmount", () => { const { unmount } = renderHook(() => useBodyScrollLock(true, false)); expect(document.body.style.overflow).toBe("hidden"); unmount(); expect(document.body.style.overflow).toBe("unset"); }); it("resets overflow on unmount even when isOpen was false", () => { const { unmount } = renderHook(() => useBodyScrollLock(false, true)); unmount(); expect(document.body.style.overflow).toBe("unset"); }); }); describe("Dependency changes", () => { it("locks scroll when isOpen changes from false to true", () => { const { rerender } = renderHook( ({ isOpen, hide }) => useBodyScrollLock(isOpen, hide), { initialProps: { isOpen: false, hide: false } } ); expect(document.body.style.overflow).not.toBe("hidden"); rerender({ isOpen: true, hide: false }); expect(document.body.style.overflow).toBe("hidden"); }); it("unlocks scroll when isOpen changes from true to false with hideScroll=true", () => { const { rerender } = renderHook( ({ isOpen, hide }) => useBodyScrollLock(isOpen, hide), { initialProps: { isOpen: true, hide: true } } ); expect(document.body.style.overflow).toBe("hidden"); rerender({ isOpen: false, hide: true }); expect(document.body.style.overflow).toBe("unset"); }); it("runs cleanup then effect when dependencies change", () => { const { rerender } = renderHook( ({ isOpen, hide }) => useBodyScrollLock(isOpen, hide), { initialProps: { isOpen: true, hide: false } } ); expect(document.body.style.overflow).toBe("hidden"); // Change hideScrollOnIsOpenFalse - triggers cleanup + re-run rerender({ isOpen: true, hide: true }); expect(document.body.style.overflow).toBe("hidden"); }); }); });