372 lines
11 KiB
TypeScript
372 lines
11 KiB
TypeScript
// @vitest-environment jsdom
|
|
|
|
import React, { act, useEffect } from "react";
|
|
import { createRoot, type Root } from "react-dom/client";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const queryMock = vi.hoisted(() => vi.fn());
|
|
const saveMutationMock = vi.hoisted(() => vi.fn());
|
|
const authState = vi.hoisted(() => ({ isAuthenticated: true }));
|
|
const convexClient = vi.hoisted(() => ({ query: queryMock }));
|
|
|
|
vi.mock("@/convex/_generated/api", () => ({
|
|
api: {
|
|
presets: {
|
|
list: "presets.list",
|
|
save: "presets.save",
|
|
},
|
|
},
|
|
}));
|
|
|
|
vi.mock("convex/react", () => ({
|
|
useConvex: () => convexClient,
|
|
useConvexAuth: () => ({ isAuthenticated: authState.isAuthenticated }),
|
|
useMutation: (key: string) => {
|
|
if (key === "presets.save") {
|
|
return saveMutationMock;
|
|
}
|
|
return vi.fn();
|
|
},
|
|
}));
|
|
|
|
import {
|
|
CanvasPresetsProvider,
|
|
useCanvasAdjustmentPresets,
|
|
useSaveCanvasAdjustmentPreset,
|
|
} from "@/components/canvas/canvas-presets-context";
|
|
|
|
type PresetConsumerSnapshot = {
|
|
curves: string[];
|
|
colorAdjust: string[];
|
|
};
|
|
|
|
const latestSnapshotRef: { current: PresetConsumerSnapshot | null } = { current: null };
|
|
const latestSaveRef: {
|
|
current:
|
|
| ((args: {
|
|
name: string;
|
|
nodeType: "curves" | "color-adjust" | "light-adjust" | "detail-adjust";
|
|
params: unknown;
|
|
}) => Promise<unknown>)
|
|
| null;
|
|
} = { current: null };
|
|
|
|
function Harness() {
|
|
const curves = useCanvasAdjustmentPresets("curves");
|
|
const colorAdjust = useCanvasAdjustmentPresets("color-adjust");
|
|
const savePreset = useSaveCanvasAdjustmentPreset();
|
|
|
|
useEffect(() => {
|
|
latestSnapshotRef.current = {
|
|
curves: curves.map((preset) => preset.name),
|
|
colorAdjust: colorAdjust.map((preset) => preset.name),
|
|
};
|
|
latestSaveRef.current = savePreset;
|
|
|
|
return () => {
|
|
latestSnapshotRef.current = null;
|
|
latestSaveRef.current = null;
|
|
};
|
|
}, [colorAdjust, curves, savePreset]);
|
|
|
|
return null;
|
|
}
|
|
|
|
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
|
|
describe("CanvasPresetsProvider", () => {
|
|
let container: HTMLDivElement | null = null;
|
|
let root: Root | null = null;
|
|
|
|
afterEach(async () => {
|
|
if (root) {
|
|
await act(async () => {
|
|
root?.unmount();
|
|
});
|
|
}
|
|
vi.useRealTimers();
|
|
container?.remove();
|
|
container = null;
|
|
root = null;
|
|
latestSnapshotRef.current = null;
|
|
latestSaveRef.current = null;
|
|
authState.isAuthenticated = true;
|
|
queryMock.mockReset();
|
|
saveMutationMock.mockReset();
|
|
});
|
|
|
|
it("loads presets with an imperative one-off query and exposes them by node type", async () => {
|
|
queryMock.mockResolvedValue([
|
|
{ _id: "preset-1", name: "Film Fade", nodeType: "curves", params: {} },
|
|
{ _id: "preset-2", name: "Warm Pop", nodeType: "color-adjust", params: {} },
|
|
{ _id: "preset-3", name: "Crisp Contrast", nodeType: "curves", params: {} },
|
|
]);
|
|
|
|
container = document.createElement("div");
|
|
document.body.appendChild(container);
|
|
root = createRoot(container);
|
|
|
|
await act(async () => {
|
|
root?.render(
|
|
React.createElement(CanvasPresetsProvider, { enabled: true }, React.createElement(Harness)),
|
|
);
|
|
});
|
|
|
|
await vi.waitFor(() => {
|
|
expect(latestSnapshotRef.current).toEqual({
|
|
curves: ["Film Fade", "Crisp Contrast"],
|
|
colorAdjust: ["Warm Pop"],
|
|
});
|
|
});
|
|
|
|
expect(queryMock).toHaveBeenCalledTimes(1);
|
|
expect(queryMock).toHaveBeenCalledWith("presets.list", {});
|
|
});
|
|
|
|
it("refreshes provider state after saving a preset through context", async () => {
|
|
queryMock
|
|
.mockResolvedValueOnce([
|
|
{ _id: "preset-1", name: "Film Fade", nodeType: "curves", params: {} },
|
|
])
|
|
.mockResolvedValueOnce([
|
|
{ _id: "preset-2", name: "Studio Glow", nodeType: "curves", params: {} },
|
|
{ _id: "preset-1", name: "Film Fade", nodeType: "curves", params: {} },
|
|
]);
|
|
saveMutationMock.mockResolvedValue("preset-2");
|
|
|
|
container = document.createElement("div");
|
|
document.body.appendChild(container);
|
|
root = createRoot(container);
|
|
|
|
await act(async () => {
|
|
root?.render(
|
|
React.createElement(CanvasPresetsProvider, { enabled: true }, React.createElement(Harness)),
|
|
);
|
|
});
|
|
|
|
await vi.waitFor(() => {
|
|
expect(latestSnapshotRef.current?.curves).toEqual(["Film Fade"]);
|
|
});
|
|
|
|
await act(async () => {
|
|
await latestSaveRef.current?.({
|
|
name: "Studio Glow",
|
|
nodeType: "curves",
|
|
params: { contrast: 10 },
|
|
});
|
|
});
|
|
|
|
await vi.waitFor(() => {
|
|
expect(latestSnapshotRef.current?.curves).toEqual(["Studio Glow", "Film Fade"]);
|
|
});
|
|
|
|
expect(saveMutationMock).toHaveBeenCalledWith({
|
|
name: "Studio Glow",
|
|
nodeType: "curves",
|
|
params: { contrast: 10 },
|
|
});
|
|
expect(queryMock).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("waits for auth before fetching presets", async () => {
|
|
authState.isAuthenticated = false;
|
|
queryMock.mockResolvedValue([
|
|
{ _id: "preset-1", name: "Recovered", nodeType: "curves", params: {} },
|
|
]);
|
|
|
|
container = document.createElement("div");
|
|
document.body.appendChild(container);
|
|
root = createRoot(container);
|
|
|
|
await act(async () => {
|
|
root?.render(
|
|
React.createElement(CanvasPresetsProvider, { enabled: true }, React.createElement(Harness)),
|
|
);
|
|
});
|
|
|
|
expect(queryMock).not.toHaveBeenCalled();
|
|
expect(latestSnapshotRef.current).toEqual({ curves: [], colorAdjust: [] });
|
|
|
|
authState.isAuthenticated = true;
|
|
await act(async () => {
|
|
root?.render(
|
|
React.createElement(CanvasPresetsProvider, { enabled: true }, React.createElement(Harness)),
|
|
);
|
|
});
|
|
|
|
await vi.waitFor(() => {
|
|
expect(latestSnapshotRef.current?.curves).toEqual(["Recovered"]);
|
|
});
|
|
|
|
expect(queryMock).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("retries after repeated transient preset fetch failures", async () => {
|
|
vi.useFakeTimers();
|
|
queryMock
|
|
.mockRejectedValueOnce(new Error("temporary-1"))
|
|
.mockRejectedValueOnce(new Error("temporary-2"))
|
|
.mockResolvedValueOnce([
|
|
{ _id: "preset-1", name: "Recovered", nodeType: "curves", params: {} },
|
|
]);
|
|
|
|
container = document.createElement("div");
|
|
document.body.appendChild(container);
|
|
root = createRoot(container);
|
|
|
|
await act(async () => {
|
|
root?.render(
|
|
React.createElement(CanvasPresetsProvider, { enabled: true }, React.createElement(Harness)),
|
|
);
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(queryMock).toHaveBeenCalledTimes(2);
|
|
expect(latestSnapshotRef.current?.curves).toEqual([]);
|
|
|
|
await act(async () => {
|
|
vi.advanceTimersByTime(1000);
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
await vi.waitFor(() => {
|
|
expect(latestSnapshotRef.current?.curves).toEqual(["Recovered"]);
|
|
});
|
|
expect(queryMock).toHaveBeenCalledTimes(3);
|
|
});
|
|
|
|
it("does not fail the save flow when only the refresh step fails", async () => {
|
|
vi.useFakeTimers();
|
|
queryMock
|
|
.mockResolvedValueOnce([
|
|
{ _id: "preset-1", name: "Film Fade", nodeType: "curves", params: {} },
|
|
])
|
|
.mockRejectedValueOnce(new Error("refresh-temporary"))
|
|
.mockResolvedValueOnce([
|
|
{ _id: "preset-2", name: "Studio Glow", nodeType: "curves", params: {} },
|
|
{ _id: "preset-1", name: "Film Fade", nodeType: "curves", params: {} },
|
|
]);
|
|
saveMutationMock.mockResolvedValue("preset-2");
|
|
|
|
container = document.createElement("div");
|
|
document.body.appendChild(container);
|
|
root = createRoot(container);
|
|
|
|
await act(async () => {
|
|
root?.render(
|
|
React.createElement(CanvasPresetsProvider, { enabled: true }, React.createElement(Harness)),
|
|
);
|
|
});
|
|
|
|
await vi.waitFor(() => {
|
|
expect(latestSnapshotRef.current?.curves).toEqual(["Film Fade"]);
|
|
});
|
|
|
|
const savePromise = latestSaveRef.current?.({
|
|
name: "Studio Glow",
|
|
nodeType: "curves",
|
|
params: { contrast: 10 },
|
|
});
|
|
|
|
await act(async () => {
|
|
await savePromise;
|
|
});
|
|
|
|
expect(saveMutationMock).toHaveBeenCalledTimes(1);
|
|
|
|
await vi.waitFor(() => {
|
|
expect(latestSnapshotRef.current?.curves).toEqual(["Studio Glow", "Film Fade"]);
|
|
});
|
|
expect(queryMock).toHaveBeenCalledTimes(3);
|
|
});
|
|
it("ignores stale failed refreshes after a newer refresh succeeds", async () => {
|
|
vi.useFakeTimers();
|
|
|
|
let rejectOlderRefresh: ((error: Error) => void) | null = null;
|
|
const olderRefreshPromise = new Promise<never>((_, reject) => {
|
|
rejectOlderRefresh = reject;
|
|
});
|
|
|
|
queryMock
|
|
.mockResolvedValueOnce([
|
|
{ _id: "preset-1", name: "Film Fade", nodeType: "curves", params: {} },
|
|
])
|
|
.mockImplementationOnce(() => olderRefreshPromise)
|
|
.mockResolvedValueOnce([
|
|
{ _id: "preset-3", name: "Cinematic Glow", nodeType: "curves", params: {} },
|
|
{ _id: "preset-2", name: "Studio Glow", nodeType: "curves", params: {} },
|
|
{ _id: "preset-1", name: "Film Fade", nodeType: "curves", params: {} },
|
|
]);
|
|
saveMutationMock.mockResolvedValue("preset-2");
|
|
|
|
container = document.createElement("div");
|
|
document.body.appendChild(container);
|
|
root = createRoot(container);
|
|
|
|
await act(async () => {
|
|
root?.render(
|
|
React.createElement(CanvasPresetsProvider, { enabled: true }, React.createElement(Harness)),
|
|
);
|
|
});
|
|
|
|
await vi.waitFor(() => {
|
|
expect(latestSnapshotRef.current?.curves).toEqual(["Film Fade"]);
|
|
});
|
|
|
|
const firstSavePromise = latestSaveRef.current?.({
|
|
name: "Studio Glow",
|
|
nodeType: "curves",
|
|
params: { contrast: 10 },
|
|
});
|
|
|
|
await act(async () => {
|
|
await Promise.resolve();
|
|
});
|
|
|
|
const secondSavePromise = latestSaveRef.current?.({
|
|
name: "Cinematic Glow",
|
|
nodeType: "curves",
|
|
params: { contrast: 20 },
|
|
});
|
|
|
|
await act(async () => {
|
|
await secondSavePromise;
|
|
});
|
|
|
|
await vi.waitFor(() => {
|
|
expect(latestSnapshotRef.current?.curves).toEqual([
|
|
"Cinematic Glow",
|
|
"Studio Glow",
|
|
"Film Fade",
|
|
]);
|
|
});
|
|
|
|
await act(async () => {
|
|
rejectOlderRefresh?.(new Error("older refresh failed"));
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
await act(async () => {
|
|
await firstSavePromise;
|
|
});
|
|
|
|
expect(latestSnapshotRef.current?.curves).toEqual([
|
|
"Cinematic Glow",
|
|
"Studio Glow",
|
|
"Film Fade",
|
|
]);
|
|
|
|
await act(async () => {
|
|
vi.advanceTimersByTime(1000);
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(queryMock).toHaveBeenCalledTimes(3);
|
|
});
|
|
|
|
});
|