memos/web/tests/sidebar-width.test.tsx
Johnny abe87d9440
feat(sidebar): make the desktop rail resizable (#6148)
Signed-off-by: Johnny <johnnyjoygg@gmail.com>
2026-08-06 19:18:38 +08:00

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);
});
});