frontend/src/hooks/useToolDefaults.test.tsx
Rama main · réplica de lectura del repositorio original.
import { describe, it, expect, beforeEach } from "vitest";
import { act, renderHook, waitFor } from "@testing-library/react";
import * as db from "@/lib/localStore/db";
import * as defaults from "@/lib/localStore/defaults";
import { loadPersisted, savePersisted } from "@/lib/persistence";
import { useToolDefaults } from "./useToolDefaults";
const DEFAULTS = { level: "recommended", quality: 75 };
/** The hook debounces writes at 400ms; wait past that. */
const settle = () => new Promise((r) => setTimeout(r, 600));
beforeEach(async () => {
await db.destroy();
localStorage.clear();
});
describe("useToolDefaults", () => {
it("starts at defaults with restored=false", () => {
const { result } = renderHook(() => useToolDefaults("compress-pdf", DEFAULTS));
expect(result.current[0]).toEqual(DEFAULTS);
expect(result.current[2].restored).toBe(false);
});
it("registers the slug once a non-default value is written", async () => {
const { result } = renderHook(() => useToolDefaults("compress-pdf", DEFAULTS));
act(() => result.current[1]({ level: "extreme", quality: 40 }));
await waitFor(async () => {
expect(await defaults.customizedSlugs()).toEqual(["compress-pdf"]);
});
});
it("does not register when the value equals defaults", async () => {
const { result } = renderHook(() => useToolDefaults("compress-pdf", DEFAULTS));
act(() => result.current[1]({ ...DEFAULTS }));
await settle();
expect(await defaults.customizedSlugs()).toEqual([]);
});
it("unregisters when the value returns to defaults", async () => {
const { result } = renderHook(() => useToolDefaults("compress-pdf", DEFAULTS));
act(() => result.current[1]({ level: "extreme", quality: 40 }));
await waitFor(async () => {
expect(await defaults.customizedSlugs()).toEqual(["compress-pdf"]);
});
act(() => result.current[1]({ ...DEFAULTS }));
await waitFor(async () => {
expect(await defaults.customizedSlugs()).toEqual([]);
});
});
it("rehydrates synchronously on remount — no flicker through defaults", async () => {
const first = renderHook(() => useToolDefaults("compress-pdf", DEFAULTS));
act(() => first.result.current[1]({ level: "extreme", quality: 40 }));
await settle();
first.unmount();
const second = renderHook(() => useToolDefaults("compress-pdf", DEFAULTS));
// Value is correct on the FIRST render, not after an effect.
expect(second.result.current[0]).toEqual({ level: "extreme", quality: 40 });
expect(second.result.current[2].restored).toBe(true);
});
it("merges newly-added defaults into an old snapshot", async () => {
const first = renderHook(() => useToolDefaults("compress-pdf", DEFAULTS));
act(() => first.result.current[1]({ level: "extreme", quality: 40 }));
await settle();
first.unmount();
const WITH_NEW_FIELD = { ...DEFAULTS, grayscale: false };
const second = renderHook(() => useToolDefaults("compress-pdf", WITH_NEW_FIELD));
expect(second.result.current[0]).toEqual({
level: "extreme",
quality: 40,
grayscale: false,
});
});
it("reset clears the value and the registration", async () => {
const { result } = renderHook(() => useToolDefaults("compress-pdf", DEFAULTS));
act(() => result.current[1]({ level: "extreme", quality: 40 }));
await settle();
expect(loadPersisted("compress-pdf")).not.toBeNull();
await act(async () => {
result.current[2].reset();
});
expect(result.current[0]).toEqual(DEFAULTS);
await waitFor(async () => {
expect(await defaults.customizedSlugs()).toEqual([]);
});
});
it("keeps two tools independent", async () => {
const a = renderHook(() => useToolDefaults("compress-pdf", DEFAULTS));
const b = renderHook(() => useToolDefaults("bates-numbering", DEFAULTS));
act(() => a.result.current[1]({ level: "extreme", quality: 40 }));
await settle();
expect(b.result.current[0]).toEqual(DEFAULTS);
await waitFor(async () => {
expect(await defaults.customizedSlugs()).toEqual(["compress-pdf"]);
});
});
describe("legacy key migration", () => {
// The 6 tools that already used useFormPersist keyed on short names
// ("bates", "compress") rather than registry slugs. Renaming the key
// without migrating would silently discard every existing user's saved
// settings.
it("adopts a value stored under the old key", () => {
savePersisted("bates", { level: "extreme", quality: 40 });
const { result } = renderHook(() =>
useToolDefaults("bates-numbering", DEFAULTS, { legacyKey: "bates" }),
);
expect(result.current[0]).toEqual({ level: "extreme", quality: 40 });
expect(result.current[2].restored).toBe(true);
});
it("removes the old key once migrated", async () => {
savePersisted("bates", { level: "extreme", quality: 40 });
renderHook(() => useToolDefaults("bates-numbering", DEFAULTS, { legacyKey: "bates" }));
await waitFor(() => expect(loadPersisted("bates")).toBeNull());
expect(loadPersisted("bates-numbering")).toEqual({ level: "extreme", quality: 40 });
});
it("prefers an existing new-key value over the legacy one", () => {
savePersisted("bates", { level: "light", quality: 10 });
savePersisted("bates-numbering", { level: "extreme", quality: 40 });
const { result } = renderHook(() =>
useToolDefaults("bates-numbering", DEFAULTS, { legacyKey: "bates" }),
);
expect(result.current[0]).toEqual({ level: "extreme", quality: 40 });
});
it("is a no-op when there is no legacy value", () => {
const { result } = renderHook(() =>
useToolDefaults("bates-numbering", DEFAULTS, { legacyKey: "bates" }),
);
expect(result.current[0]).toEqual(DEFAULTS);
expect(result.current[2].restored).toBe(false);
});
});
describe("setField", () => {
// Generated per-field setters must behave like useState setters, including
// the updater-function form — several tools call setX(prev => ...).
it("sets a field by value", async () => {
const { result } = renderHook(() => useToolDefaults("compress-pdf", DEFAULTS));
act(() => result.current[2].setField("level", "extreme"));
expect(result.current[0]).toEqual({ ...DEFAULTS, level: "extreme" });
});
it("sets a field with an updater function", async () => {
const { result } = renderHook(() => useToolDefaults("compress-pdf", DEFAULTS));
act(() => result.current[2].setField("quality", (q) => (q as number) + 5));
expect(result.current[0].quality).toBe(80);
});
it("leaves other fields untouched", async () => {
const { result } = renderHook(() => useToolDefaults("compress-pdf", DEFAULTS));
act(() => result.current[2].setField("quality", 10));
expect(result.current[0].level).toBe("recommended");
});
it("supports a boolean toggle via updater", async () => {
const BOOL = { caseSensitive: false };
const { result } = renderHook(() => useToolDefaults("highlight-pdf", BOOL));
act(() => result.current[2].setField("caseSensitive", (v) => !v));
expect(result.current[0].caseSensitive).toBe(true);
});
it("supports an array updater", async () => {
const ARR = { sizes: [16, 32] as number[] };
const { result } = renderHook(() => useToolDefaults("generate-favicon", ARR));
act(() => result.current[2].setField("sizes", (prev) => [...(prev as number[]), 48]));
expect(result.current[0].sizes).toEqual([16, 32, 48]);
});
});
});