// @vitest-environment jsdom import { act, renderHook } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; import { type ApplicationResult, useApplicationMutation, } from "../../src/presentation/adapters/query/application-query.ts"; import { createFailure } from "../../src/contracts/errors.ts"; import { RESOURCE_INVALIDATION_TOPIC, queryClient, wrapper, } from "./application-query-fixture.tsx"; describe("application mutation optimistic cache lifecycle", () => { it("cancels an in-flight query before taking the optimistic snapshot", async () => { const client = queryClient(); const key = ["resource", "ordered-update"]; client.setQueryData(key, ["existing"]); let finishCancellation: () => void = () => {}; const cancellation = new Promise((resolve) => { finishCancellation = resolve; }); const cancelQueries = vi .spyOn(client, "cancelQueries") .mockImplementation(async () => cancellation); const getQueryData = vi.spyOn(client, "getQueryData"); const update = vi.fn((previous, input) => [ ...(previous as string[]), input, ]); const execute = vi.fn(async () => ({ ok: true as const, value: "created", })); const hook = renderHook( () => useApplicationMutation({ execute, currentData: ["existing"], optimistic: { queryKey: key, update }, }), { wrapper: wrapper(client) }, ); let pending: Promise> | undefined; act(() => { pending = hook.result.current.submit("created"); }); expect(cancelQueries).toHaveBeenCalledWith({ queryKey: key, exact: true, }); expect(getQueryData).not.toHaveBeenCalled(); expect(update).not.toHaveBeenCalled(); expect(execute).not.toHaveBeenCalled(); finishCancellation(); if (!pending) throw new Error("expected pending mutation"); await act(() => pending); expect(getQueryData).toHaveBeenCalledWith(key); expect(update).toHaveBeenCalledWith(["existing"], "created"); expect(execute).toHaveBeenCalledOnce(); expect(client.getQueryData(key)).toEqual(["existing", "created"]); }); it("keeps a committed optimistic update when invalidation fails", async () => { const client = queryClient(); const key = ["resource", "committed-update"]; client.setQueryData(key, ["existing"]); vi.spyOn(client, "invalidateQueries").mockRejectedValue( new Error("cache refresh failed"), ); const hook = renderHook( () => useApplicationMutation({ execute: async () => ({ ok: true, value: "created" }), invalidate: [RESOURCE_INVALIDATION_TOPIC], currentData: ["existing"], optimistic: { queryKey: key, update: (previous, input) => [ ...(previous as string[]), input, ], }, }), { wrapper: wrapper(client) }, ); let outcome; await act(async () => { outcome = await hook.result.current.submit("created"); }); expect(outcome).toEqual({ ok: true, value: "created" }); expect(client.getQueryData(key)).toEqual(["existing", "created"]); }); it("normalizes an optimistic preparation defect without running the command", async () => { const client = queryClient(); const key = ["resource", "invalid-optimistic-update"]; client.setQueryData(key, ["existing"]); const execute = vi.fn(async () => ({ ok: true as const, value: "created", })); const hook = renderHook( () => useApplicationMutation({ execute, currentData: ["existing"], optimistic: { queryKey: key, update: () => { throw new Error("private optimistic detail"); }, }, }), { wrapper: wrapper(client) }, ); let outcome; await act(async () => { outcome = await hook.result.current.submit("created"); }); expect(outcome).toMatchObject({ ok: false, error: { kind: "UNKNOWN_FAILURE", effect: "NOT_STARTED", operationId: "APPLICATION_MUTATION", userMessageKey: "error.unknown_failure", }, }); expect(JSON.stringify(outcome)).not.toContain("private optimistic detail"); expect(execute).not.toHaveBeenCalled(); expect(client.getQueryData(key)).toEqual(["existing"]); }); it("removes an optimistic cache entry when no prior data existed", async () => { const client = queryClient(); const key = ["resource", "new-optimistic-entry"]; const failure = createFailure("SERVER_FAILURE", "CREATE", 0, { effect: "NOT_APPLIED", }); const hook = renderHook( () => useApplicationMutation({ execute: async () => ({ ok: false, error: failure }), currentData: true, optimistic: { queryKey: key, update: (_previous, input) => [input], }, }), { wrapper: wrapper(client) }, ); let outcome; await act(async () => { outcome = await hook.result.current.submit("temporary"); }); expect(outcome).toEqual({ ok: false, error: failure }); expect(client.getQueryData(key)).toBeUndefined(); expect(client.getQueryState(key)).toBeUndefined(); }); it("rolls optimistic data back and exposes a resolvable conflict", async () => { const client = queryClient(); const key = ["resource", "list"]; client.setQueryData(key, ["existing"]); const conflict = createFailure("CONFLICT", "CREATE", 0, { effect: "NOT_APPLIED", }); const hook = renderHook( () => useApplicationMutation({ execute: async () => ({ ok: false, error: conflict }), invalidate: [RESOURCE_INVALIDATION_TOPIC], currentData: client.getQueryData(key), optimistic: { queryKey: key, update: (previous, input) => [ ...(previous as string[]), input, ], }, }), { wrapper: wrapper(client) }, ); let outcome; await act(async () => { outcome = await hook.result.current.submit("conflicting"); }); expect(outcome).toEqual({ ok: false, error: conflict }); expect(client.getQueryData(key)).toEqual(["existing"]); expect(hook.result.current.state.indicator).toBe("mutation-conflict"); expect(hook.result.current.state.overlay).toMatchObject({ mutationPending: false, mutationConflict: true, }); await act(() => hook.result.current.resolveConflict()); expect(hook.result.current.state.indicator).toBeNull(); expect(client.getQueryState(key)?.isInvalidated).toBe(true); });});