1import { useMutate } from "@clo/react-mutation";
2import { assertEquals } from "@std/assert";
3import { act, render, screen } from "@testing-library/react";
4import { userEvent } from "@testing-library/user-event";
5import { test, vi } from "vitest";
6import { createTestMutationClient, IterableStream } from "./share.ts";
7
8test("setError should manually set error state on the hook", async () => {
9 vi.useFakeTimers({ shouldAdvanceTime: true });
10 const user = userEvent.setup({ delay: null });
11
12 const { client, successMessages, errorMessages } = createTestMutationClient();
13
14 const mutTest = client.define({
15 id: "test-1",
16 mutate: async () => {
17 return "success";
18 },
19 describe: "Test the action",
20 describeResult: "Tested the action",
21 optimistic: () => {},
22 });
23
24 const manualError = new Error("Manual error");
25
26 let renders: Array<{
27 status: string;
28 result: string | undefined;
29 error: unknown;
30 errorMessage: string | undefined;
31 isError: boolean;
32 isSuccess: boolean;
33 }> = [];
34
35 function TestComponent() {
36 const { setError, status, result, error, errorMessage, isError, isSuccess } = useMutate(
37 mutTest,
38 );
39 renders.push({ status, result, error, errorMessage, isError, isSuccess });
40 return (
41 <button
42 data-testid="set-error-btn"
43 onClick={() => {
44 setError(manualError);
45 }}
46 >
47 Set Error
48 </button>
49 );
50 }
51
52 render(<TestComponent />);
53
54 // initial state - idle
55 assertEquals(renders, [
56 {
57 status: "idle",
58 result: undefined,
59 error: undefined,
60 errorMessage: undefined,
61 isError: false,
62 isSuccess: false,
63 },
64 ]);
65 assertEquals(successMessages, []);
66 assertEquals(errorMessages, []);
67 renders = [];
68 vi.runAllTimers();
69
70 // manually set error using setError
71 await act(() => user.click(screen.getByTestId("set-error-btn")));
72 assertEquals(renders, [
73 {
74 status: "error",
75 result: undefined,
76 error: manualError,
77 errorMessage: "Manual error",
78 isError: true,
79 isSuccess: false,
80 },
81 ]);
82 // setError should not trigger global error/success handlers
83 assertEquals(successMessages, []);
84 assertEquals(errorMessages, []);
85 renders = [];
86});
87
88test("setError should override success state", async () => {
89 vi.useFakeTimers({ shouldAdvanceTime: true });
90 const user = userEvent.setup({ delay: null });
91
92 const { client, successMessages, errorMessages } = createTestMutationClient();
93 const s = new IterableStream<string>();
94
95 const mutTest = client.define({
96 id: "test-2",
97 mutate: async () => {
98 return (await s.next()).value;
99 },
100 describe: "Test the action",
101 describeResult: "Tested the action",
102 optimistic: () => {},
103 });
104
105 const customError = "Custom error message";
106
107 let renders: Array<{
108 status: string;
109 result: string | undefined;
110 error: unknown;
111 isError: boolean;
112 isSuccess: boolean;
113 }> = [];
114
115 function TestComponent() {
116 const { run, setError, status, result, error, isError, isSuccess } = useMutate(mutTest);
117 renders.push({ status, result, error, isError, isSuccess });
118 return (
119 <div>
120 <button
121 data-testid="run-btn"
122 onClick={() => {
123 run();
124 }}
125 >
126 Run
127 </button>
128 <button
129 data-testid="set-error-btn"
130 onClick={() => {
131 setError(customError);
132 }}
133 >
134 Set Error
135 </button>
136 </div>
137 );
138 }
139
140 render(<TestComponent />);
141
142 // initial state
143 assertEquals(renders, [
144 {
145 status: "idle",
146 result: undefined,
147 error: undefined,
148 isError: false,
149 isSuccess: false,
150 },
151 ]);
152 renders = [];
153 vi.runAllTimers();
154
155 // run mutation - should succeed
156 await act(() => user.click(screen.getByTestId("run-btn")));
157 assertEquals(renders, [
158 {
159 status: "mutating",
160 result: undefined,
161 error: undefined,
162 isError: false,
163 isSuccess: false,
164 },
165 ]);
166 renders = [];
167
168 await act(async () => {
169 s.push("success result");
170 vi.advanceTimersByTime(100);
171 });
172
173 // verify success state
174 assertEquals(renders, [
175 {
176 status: "success",
177 result: "success result",
178 error: undefined,
179 isError: false,
180 isSuccess: true,
181 },
182 ]);
183 assertEquals(successMessages, []);
184 assertEquals(errorMessages, []);
185 renders = [];
186
187 // now manually set error - should override success state
188 await act(() => user.click(screen.getByTestId("set-error-btn")));
189 assertEquals(renders, [
190 {
191 status: "error",
192 result: undefined,
193 error: customError,
194 isError: true,
195 isSuccess: false,
196 },
197 ]);
198 // setError should not trigger global error handler
199 assertEquals(successMessages, []);
200 assertEquals(errorMessages, []);
201 renders = [];
202});
203
204test("setError should work with different error types", async () => {
205 vi.useFakeTimers({ shouldAdvanceTime: true });
206 const user = userEvent.setup({ delay: null });
207
208 const { client } = createTestMutationClient();
209
210 const mutTest = client.define({
211 id: "test-3",
212 mutate: async () => {
213 return "success";
214 },
215 describe: "Test the action",
216 describeResult: null,
217 optimistic: () => {},
218 });
219
220 let lastErrorMessage: string | undefined;
221
222 function TestComponent() {
223 const { setError, errorMessage } = useMutate(mutTest);
224 lastErrorMessage = errorMessage;
225 return (
226 <div>
227 <button
228 data-testid="set-string-error"
229 onClick={() => setError("String error")}
230 >
231 String
232 </button>
233 <button
234 data-testid="set-error-object"
235 onClick={() => setError(new Error("Error object"))}
236 >
237 Error
238 </button>
239 <button
240 data-testid="set-number-error"
241 onClick={() => setError(42)}
242 >
243 Number
244 </button>
245 </div>
246 );
247 }
248
249 render(<TestComponent />);
250 vi.runAllTimers();
251
252 // Test string error
253 await act(() => user.click(screen.getByTestId("set-string-error")));
254 assertEquals(lastErrorMessage, "String error");
255
256 // Test Error object
257 await act(() => user.click(screen.getByTestId("set-error-object")));
258 assertEquals(lastErrorMessage, "Error object");
259
260 // Test number (should be converted to string)
261 await act(() => user.click(screen.getByTestId("set-number-error")));
262 assertEquals(lastErrorMessage, "42");
263});