Files
wehub-resource-sync 4cddfcf2f3
Backend Tests / Tests (other) (push) Failing after 1s
Backend Tests / Static Checks (push) Failing after 2s
Backend Tests / Tests (internal) (push) Failing after 2s
Backend Tests / Tests (server) (push) Failing after 1s
Backend Tests / Tests (store) (push) Failing after 1s
Build Canary Image / build-frontend (push) Failing after 1s
Frontend Tests / Lint (push) Failing after 1s
Build Canary Image / build-push (linux/amd64) (push) Has been skipped
Build Canary Image / build-push (linux/arm64) (push) Has been skipped
Build Canary Image / merge (push) Has been skipped
Frontend Tests / Build (push) Failing after 1s
Release Please / release-please (push) Failing after 0s
Proto Linter / Lint Protos (push) Failing after 2s
chore: import upstream snapshot with attribution
2026-07-13 12:02:24 +08:00

195 lines
4.8 KiB
TypeScript

import { act, renderHook } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { useDebouncedEffect, useLocalStorage } from "@/hooks";
const localStorageMock = (() => {
let store = new Map<string, string>();
let shouldThrowOnSet = false;
return {
getItem: (key: string) => store.get(key) ?? null,
setItem: (key: string, value: string) => {
if (shouldThrowOnSet) {
throw new Error("localStorage setItem unavailable");
}
store.set(key, value);
},
removeItem: (key: string) => {
store.delete(key);
},
clear: () => {
store = new Map<string, string>();
},
setShouldThrowOnSet: (value: boolean) => {
shouldThrowOnSet = value;
},
};
})();
Object.defineProperty(window, "localStorage", {
configurable: true,
value: localStorageMock,
});
describe("useLocalStorage", () => {
beforeEach(() => {
localStorageMock.setShouldThrowOnSet(false);
window.localStorage.clear();
});
afterEach(() => {
localStorageMock.setShouldThrowOnSet(false);
window.localStorage.clear();
});
it("uses the default value when storage is empty", () => {
const { result } = renderHook(() => useLocalStorage("hook-test-empty", false));
expect(result.current[0]).toBe(false);
});
it("reads and writes JSON values", () => {
window.localStorage.setItem("hook-test-existing", JSON.stringify(true));
const { result } = renderHook(() => useLocalStorage("hook-test-existing", false));
expect(result.current[0]).toBe(true);
act(() => {
result.current[1](false);
});
expect(result.current[0]).toBe(false);
expect(window.localStorage.getItem("hook-test-existing")).toBe("false");
});
it("supports updater functions", () => {
const { result } = renderHook(() => useLocalStorage("hook-test-updater", false));
act(() => {
result.current[1]((current) => !current);
});
expect(result.current[0]).toBe(true);
expect(window.localStorage.getItem("hook-test-updater")).toBe("true");
});
it("falls back to the default value for malformed storage", () => {
window.localStorage.setItem("hook-test-malformed", "{bad json");
const { result } = renderHook(() => useLocalStorage("hook-test-malformed", true));
expect(result.current[0]).toBe(true);
});
it("keeps in-memory state when the default object reference changes and persistence is unavailable", () => {
localStorageMock.setShouldThrowOnSet(true);
const initialDefaultValue = { enabled: false };
const updatedValue = { enabled: true };
const { result, rerender } = renderHook(({ defaultValue }) => useLocalStorage("hook-test-object", defaultValue), {
initialProps: { defaultValue: initialDefaultValue },
});
expect(result.current[0]).toBe(initialDefaultValue);
act(() => {
result.current[1](updatedValue);
});
expect(result.current[0]).toBe(updatedValue);
rerender({ defaultValue: { enabled: false } });
expect(result.current[0]).toBe(updatedValue);
});
});
describe("useDebouncedEffect", () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.runOnlyPendingTimers();
vi.useRealTimers();
});
it("runs the latest callback after the delay", () => {
const calls: string[] = [];
const { rerender } = renderHook(
({ value }) => {
useDebouncedEffect(
() => {
calls.push(value);
},
100,
[value],
);
},
{ initialProps: { value: "first" } },
);
act(() => {
vi.advanceTimersByTime(99);
});
expect(calls).toEqual([]);
rerender({ value: "second" });
act(() => {
vi.advanceTimersByTime(100);
});
expect(calls).toEqual(["second"]);
});
it("uses the latest callback when unrelated props rerender before the delay", () => {
const calls: string[] = [];
const { rerender } = renderHook(
({ dependency, value }) => {
useDebouncedEffect(
() => {
calls.push(value);
},
100,
[dependency],
);
},
{ initialProps: { dependency: "same", value: "first" } },
);
act(() => {
vi.advanceTimersByTime(50);
});
expect(calls).toEqual([]);
rerender({ dependency: "same", value: "second" });
act(() => {
vi.advanceTimersByTime(50);
});
expect(calls).toEqual(["second"]);
});
it("clears the pending timeout on unmount", () => {
const calls: string[] = [];
const { unmount } = renderHook(() => {
useDebouncedEffect(
() => {
calls.push("called");
},
100,
[],
);
});
unmount();
act(() => {
vi.advanceTimersByTime(100);
});
expect(calls).toEqual([]);
});
});