Files
roboco/panel/src/hooks/__tests__/use-is-mobile.test.ts
T

82 lines
2.8 KiB
TypeScript
Raw Normal View History

import { describe, it, expect, vi, afterEach } from "vitest";
import { act, renderHook } from "@testing-library/react";
import { useIsMobile } from "../use-is-mobile";
// Minimal matchMedia stub — jsdom doesn't implement it. Tracks listeners so
// tests can flip `matches` and fire a synthetic "change" event.
//
// Note on SSR/hydration safety: useIsMobile is built on useSyncExternalStore
// with a `getServerSnapshot` that always returns `false`. React uses that same
// value for the server render AND the client's first (pre-hydration-commit)
// render, so there is no mismatch to reproduce here — RTL's `renderHook` only
// ever does a client render, so it exercises `getSnapshot` (the real
// matchMedia read), never the server path. That guarantee is structural
// (React's contract for the hook), not something a jsdom unit test observes.
function installMatchMedia(initialMatches: boolean) {
const listeners = new Set<(e: MediaQueryListEvent) => void>();
window.matchMedia = vi.fn().mockImplementation((query: string) => ({
matches: initialMatches,
media: query,
addEventListener: (_: "change", cb: (e: MediaQueryListEvent) => void) => {
listeners.add(cb);
},
removeEventListener: (
_: "change",
cb: (e: MediaQueryListEvent) => void,
) => {
listeners.delete(cb);
},
})) as unknown as typeof window.matchMedia;
return {
fireChange(matches: boolean) {
initialMatches = matches;
listeners.forEach((cb) => cb({ matches } as MediaQueryListEvent));
},
};
}
describe("useIsMobile", () => {
const originalMatchMedia = window.matchMedia;
afterEach(() => {
window.matchMedia = originalMatchMedia;
vi.restoreAllMocks();
});
it("resolves to the real matchMedia value on render", () => {
installMatchMedia(true);
const { result } = renderHook(() => useIsMobile());
expect(result.current).toBe(true);
});
it("resolves false when the query does not match", () => {
installMatchMedia(false);
const { result } = renderHook(() => useIsMobile());
expect(result.current).toBe(false);
});
it("reacts to a live matchMedia change (viewport resize)", () => {
const { fireChange } = installMatchMedia(false);
const { result } = renderHook(() => useIsMobile());
expect(result.current).toBe(false);
act(() => {
fireChange(true);
});
expect(result.current).toBe(true);
});
it("builds the query from a custom breakpoint", () => {
installMatchMedia(false);
renderHook(() => useIsMobile(1024));
expect(window.matchMedia).toHaveBeenCalledWith("(max-width: 1023px)");
});
it("defaults to the md breakpoint (768px) when none is passed", () => {
installMatchMedia(false);
renderHook(() => useIsMobile());
expect(window.matchMedia).toHaveBeenCalledWith("(max-width: 767px)");
});
});