139 lines
4.8 KiB
TypeScript
139 lines
4.8 KiB
TypeScript
import { act, renderHook } from "@testing-library/react";
|
|
import { beforeEach, describe, expect, it } from "vitest";
|
|
import useSidebarWidth, { SIDEBAR_DEFAULT_WIDTH, SIDEBAR_MAX_WIDTH, SIDEBAR_MIN_WIDTH } from "@/components/AppSidebar/useSidebarWidth";
|
|
|
|
// The desktop rail's width is a persisted preference that a narrow window may only *cap*,
|
|
// never overwrite. That distinction is the whole reason the hook keeps `preferredWidth` and
|
|
// the rendered `width` apart, so it is what these tests pin down.
|
|
|
|
const STORAGE_KEY = "memos-sidebar-width";
|
|
|
|
// jsdom's default viewport (1024) leaves the 40% share above SIDEBAR_MAX_WIDTH, so the
|
|
// ceiling is the absolute one unless a test deliberately narrows the window.
|
|
const WIDE_VIEWPORT = 1024;
|
|
|
|
const setViewportWidth = (width: number) => {
|
|
Object.defineProperty(window, "innerWidth", { configurable: true, writable: true, value: width });
|
|
};
|
|
|
|
const resizeViewportTo = (width: number) => {
|
|
act(() => {
|
|
setViewportWidth(width);
|
|
window.dispatchEvent(new Event("resize"));
|
|
});
|
|
};
|
|
|
|
describe("useSidebarWidth", () => {
|
|
beforeEach(() => {
|
|
localStorage.clear();
|
|
setViewportWidth(WIDE_VIEWPORT);
|
|
});
|
|
|
|
it("starts at the default width with nothing stored", () => {
|
|
const { result } = renderHook(() => useSidebarWidth());
|
|
|
|
expect(result.current.width).toBe(SIDEBAR_DEFAULT_WIDTH);
|
|
expect(result.current.minWidth).toBe(SIDEBAR_MIN_WIDTH);
|
|
expect(result.current.maxWidth).toBe(SIDEBAR_MAX_WIDTH);
|
|
});
|
|
|
|
it("restores a persisted width on the first render", () => {
|
|
localStorage.setItem(STORAGE_KEY, "312");
|
|
|
|
const { result } = renderHook(() => useSidebarWidth());
|
|
|
|
// Read synchronously in the state initializer, so the stored width is the first painted
|
|
// width rather than a correction applied after the default flashes.
|
|
expect(result.current.width).toBe(312);
|
|
});
|
|
|
|
it("persists a committed width", () => {
|
|
const { result } = renderHook(() => useSidebarWidth());
|
|
|
|
act(() => result.current.setWidth(320));
|
|
|
|
expect(result.current.width).toBe(320);
|
|
expect(localStorage.getItem(STORAGE_KEY)).toBe("320");
|
|
});
|
|
|
|
it("clamps a commit below the floor", () => {
|
|
const { result } = renderHook(() => useSidebarWidth());
|
|
|
|
act(() => result.current.setWidth(80));
|
|
|
|
expect(result.current.width).toBe(SIDEBAR_MIN_WIDTH);
|
|
expect(localStorage.getItem(STORAGE_KEY)).toBe(String(SIDEBAR_MIN_WIDTH));
|
|
});
|
|
|
|
it("clamps a commit above the ceiling", () => {
|
|
const { result } = renderHook(() => useSidebarWidth());
|
|
|
|
act(() => result.current.setWidth(900));
|
|
|
|
expect(result.current.width).toBe(SIDEBAR_MAX_WIDTH);
|
|
expect(localStorage.getItem(STORAGE_KEY)).toBe(String(SIDEBAR_MAX_WIDTH));
|
|
});
|
|
|
|
it("rounds fractional widths, which a drag can produce on a scaled display", () => {
|
|
const { result } = renderHook(() => useSidebarWidth());
|
|
|
|
act(() => result.current.setWidth(287.6));
|
|
|
|
expect(result.current.width).toBe(288);
|
|
});
|
|
|
|
it("caps the rendered width on a narrow window without discarding the preference", () => {
|
|
localStorage.setItem(STORAGE_KEY, String(SIDEBAR_MAX_WIDTH));
|
|
const { result } = renderHook(() => useSidebarWidth());
|
|
expect(result.current.width).toBe(SIDEBAR_MAX_WIDTH);
|
|
|
|
resizeViewportTo(700);
|
|
|
|
// 40% of 700, and the stored preference is left alone so it can come back.
|
|
expect(result.current.width).toBe(280);
|
|
expect(result.current.maxWidth).toBe(280);
|
|
expect(localStorage.getItem(STORAGE_KEY)).toBe(String(SIDEBAR_MAX_WIDTH));
|
|
});
|
|
|
|
it("restores the full preference once the window has room again", () => {
|
|
localStorage.setItem(STORAGE_KEY, String(SIDEBAR_MAX_WIDTH));
|
|
const { result } = renderHook(() => useSidebarWidth());
|
|
|
|
resizeViewportTo(700);
|
|
expect(result.current.width).toBe(280);
|
|
|
|
resizeViewportTo(WIDE_VIEWPORT);
|
|
expect(result.current.width).toBe(SIDEBAR_MAX_WIDTH);
|
|
});
|
|
|
|
it("never lets the viewport cap fall below the floor", () => {
|
|
// A window this narrow only renders the mobile sheet, but the ceiling must not invert
|
|
// past the floor and produce a clamp range that cannot be satisfied.
|
|
resizeViewportTo(320);
|
|
|
|
const { result } = renderHook(() => useSidebarWidth());
|
|
|
|
expect(result.current.maxWidth).toBe(SIDEBAR_MIN_WIDTH);
|
|
expect(result.current.width).toBe(SIDEBAR_MIN_WIDTH);
|
|
});
|
|
|
|
it.each([
|
|
["a non-numeric value", "wide"],
|
|
["an empty value", ""],
|
|
["a negative value", "-40"],
|
|
])("falls back to the default for %s", (_label, stored) => {
|
|
localStorage.setItem(STORAGE_KEY, stored);
|
|
|
|
const { result } = renderHook(() => useSidebarWidth());
|
|
|
|
expect(result.current.width).toBe(SIDEBAR_DEFAULT_WIDTH);
|
|
});
|
|
|
|
it("raises a stored width that predates the current floor", () => {
|
|
localStorage.setItem(STORAGE_KEY, "120");
|
|
|
|
const { result } = renderHook(() => useSidebarWidth());
|
|
|
|
expect(result.current.width).toBe(SIDEBAR_MIN_WIDTH);
|
|
});
|
|
});
|