fix: retain uncertain optimistic mutations

This commit is contained in:
DongHyeonka
2026-08-02 02:42:56 +09:00
parent 15645541b7
commit d9afccdd60
15 changed files with 2172 additions and 124 deletions
+45 -2
View File
@@ -25,6 +25,10 @@ describe("async UI state matrix", () => {
[{ data: ["value"], isFetching: true }, "refreshing"],
[{ data: ["value"], isStale: true, isDegraded: true }, "stale-degraded"],
[{ data: ["value"], isMutationPending: true }, "mutation-pending"],
[
{ data: ["value"], hasMutationEffectUnknown: true },
"mutation-effect-unknown",
],
[{ data: ["value"], hasMutationConflict: true }, "mutation-conflict"],
])("derives overlay state %#", (signals, indicator) => {
expect(deriveAsyncState(signals).indicator).toBe(indicator);
@@ -35,16 +39,55 @@ describe("async UI state matrix", () => {
data: ["value"],
isFetching: true,
isMutationPending: true,
hasMutationEffectUnknown: true,
hasMutationConflict: true,
});
expect(state.indicator).toBe("mutation-conflict");
expect(state.indicator).toBe("mutation-effect-unknown");
expect(state.overlay).toMatchObject({
refreshing: false,
mutationPending: false,
mutationConflict: true,
mutationEffectUnknown: true,
mutationConflict: false,
});
});
it("renders unknown mutation effects with reconciliation-only actions", async () => {
const user = userEvent.setup();
const retry = vi.fn();
const reconcile = vi.fn();
const state = deriveAsyncState({
data: ["value"],
hasMutationEffectUnknown: true,
});
render(
<AsyncSurface
state={state}
onRetry={retry}
onReconcileUnknownEffect={reconcile}
>
existing content
</AsyncSurface>,
);
expect(screen.getByRole("status")).toHaveTextContent(
"변경 결과를 확인할 수 없습니다.",
);
expect(
screen.getByText("existing content").closest("section"),
).toHaveAttribute("aria-busy", "false");
expect(
screen.queryByRole("button", { name: "다시 시도" }),
).not.toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "변경됨으로 확인" }));
await user.click(
screen.getByRole("button", { name: "변경되지 않음으로 확인" }),
);
expect(reconcile).toHaveBeenNthCalledWith(1, "APPLIED");
expect(reconcile).toHaveBeenNthCalledWith(2, "NOT_APPLIED");
expect(retry).not.toHaveBeenCalled();
});
it("keeps content visible while a non-blocking refresh runs", () => {
const state = deriveAsyncState({ data: ["value"], isFetching: true });
render(<AsyncSurface state={state}>existing content</AsyncSurface>);