refactor: 프론트 템플릿 리펙토링

This commit is contained in:
donghyeon-ka
2026-09-18 15:16:58 +09:00
parent c10a709f2c
commit 5cc41467ae
80 changed files with 7227 additions and 4672 deletions
@@ -0,0 +1,213 @@
// @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<void>((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<string, string>({
execute,
currentData: ["existing"],
optimistic: { queryKey: key, update },
}),
{ wrapper: wrapper(client) },
);
let pending: Promise<ApplicationResult<string>> | 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<string, string>({
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<string, string>({
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<string, string>({
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<string, string>({
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);
});});