@fragno-dev/core 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +61 -0
- package/.turbo/turbo-types$colon$check.log +2 -0
- package/dist/api/api.d.ts +2 -0
- package/dist/api/api.js +3 -0
- package/dist/api-CBDGZiLC.d.ts +278 -0
- package/dist/api-CBDGZiLC.d.ts.map +1 -0
- package/dist/api-DgHfYjq2.js +54 -0
- package/dist/api-DgHfYjq2.js.map +1 -0
- package/dist/client/client.d.ts +3 -0
- package/dist/client/client.js +6 -0
- package/dist/client/client.svelte.d.ts +33 -0
- package/dist/client/client.svelte.d.ts.map +1 -0
- package/dist/client/client.svelte.js +123 -0
- package/dist/client/client.svelte.js.map +1 -0
- package/dist/client/react.d.ts +58 -0
- package/dist/client/react.d.ts.map +1 -0
- package/dist/client/react.js +80 -0
- package/dist/client/react.js.map +1 -0
- package/dist/client/vanilla.d.ts +61 -0
- package/dist/client/vanilla.d.ts.map +1 -0
- package/dist/client/vanilla.js +136 -0
- package/dist/client/vanilla.js.map +1 -0
- package/dist/client/vue.d.ts +39 -0
- package/dist/client/vue.d.ts.map +1 -0
- package/dist/client/vue.js +108 -0
- package/dist/client/vue.js.map +1 -0
- package/dist/client-DWjxKDnE.js +703 -0
- package/dist/client-DWjxKDnE.js.map +1 -0
- package/dist/client-XFdAy-IQ.d.ts +287 -0
- package/dist/client-XFdAy-IQ.d.ts.map +1 -0
- package/dist/integrations/astro.d.ts +18 -0
- package/dist/integrations/astro.d.ts.map +1 -0
- package/dist/integrations/astro.js +16 -0
- package/dist/integrations/astro.js.map +1 -0
- package/dist/integrations/next-js.d.ts +15 -0
- package/dist/integrations/next-js.d.ts.map +1 -0
- package/dist/integrations/next-js.js +17 -0
- package/dist/integrations/next-js.js.map +1 -0
- package/dist/integrations/react-ssr.d.ts +19 -0
- package/dist/integrations/react-ssr.d.ts.map +1 -0
- package/dist/integrations/react-ssr.js +38 -0
- package/dist/integrations/react-ssr.js.map +1 -0
- package/dist/integrations/svelte-kit.d.ts +21 -0
- package/dist/integrations/svelte-kit.d.ts.map +1 -0
- package/dist/integrations/svelte-kit.js +18 -0
- package/dist/integrations/svelte-kit.js.map +1 -0
- package/dist/mod.d.ts +3 -0
- package/dist/mod.js +177 -0
- package/dist/mod.js.map +1 -0
- package/dist/route-Bp6eByhz.js +331 -0
- package/dist/route-Bp6eByhz.js.map +1 -0
- package/dist/ssr-tJHqcNSw.js +48 -0
- package/dist/ssr-tJHqcNSw.js.map +1 -0
- package/package.json +127 -0
- package/src/api/api.test.ts +140 -0
- package/src/api/api.ts +106 -0
- package/src/api/error.ts +47 -0
- package/src/api/fragment.test.ts +509 -0
- package/src/api/fragment.ts +277 -0
- package/src/api/internal/path-runtime.test.ts +121 -0
- package/src/api/internal/path-type.test.ts +602 -0
- package/src/api/internal/path.ts +322 -0
- package/src/api/internal/response-stream.ts +118 -0
- package/src/api/internal/route.test.ts +56 -0
- package/src/api/internal/route.ts +9 -0
- package/src/api/request-input-context.test.ts +437 -0
- package/src/api/request-input-context.ts +201 -0
- package/src/api/request-middleware.test.ts +544 -0
- package/src/api/request-middleware.ts +126 -0
- package/src/api/request-output-context.test.ts +626 -0
- package/src/api/request-output-context.ts +175 -0
- package/src/api/route.test.ts +176 -0
- package/src/api/route.ts +152 -0
- package/src/client/client-builder.test.ts +264 -0
- package/src/client/client-error.test.ts +15 -0
- package/src/client/client-error.ts +141 -0
- package/src/client/client-types.test.ts +493 -0
- package/src/client/client.ssr.test.ts +173 -0
- package/src/client/client.svelte.test.ts +837 -0
- package/src/client/client.svelte.ts +278 -0
- package/src/client/client.test.ts +1690 -0
- package/src/client/client.ts +1035 -0
- package/src/client/component.test.svelte +21 -0
- package/src/client/internal/ndjson-streaming.test.ts +457 -0
- package/src/client/internal/ndjson-streaming.ts +248 -0
- package/src/client/react.test.ts +947 -0
- package/src/client/react.ts +241 -0
- package/src/client/vanilla.test.ts +867 -0
- package/src/client/vanilla.ts +265 -0
- package/src/client/vue.test.ts +754 -0
- package/src/client/vue.ts +242 -0
- package/src/http/http-status.ts +60 -0
- package/src/integrations/astro.ts +17 -0
- package/src/integrations/next-js.ts +31 -0
- package/src/integrations/react-ssr.ts +40 -0
- package/src/integrations/svelte-kit.ts +41 -0
- package/src/mod.ts +20 -0
- package/src/util/async.test.ts +85 -0
- package/src/util/async.ts +96 -0
- package/src/util/content-type.test.ts +136 -0
- package/src/util/content-type.ts +84 -0
- package/src/util/nanostores.test.ts +28 -0
- package/src/util/nanostores.ts +65 -0
- package/src/util/ssr.ts +75 -0
- package/src/util/types-util.ts +16 -0
- package/tsconfig.json +10 -0
- package/tsdown.config.ts +21 -0
- package/vitest.config.ts +10 -0
|
@@ -0,0 +1,867 @@
|
|
|
1
|
+
import { test, expect, describe, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { atom } from "nanostores";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { createClientBuilder } from "./client";
|
|
5
|
+
import { useFragno } from "./vanilla";
|
|
6
|
+
import { defineRoute } from "../api/route";
|
|
7
|
+
import { defineFragment } from "../api/fragment";
|
|
8
|
+
import type { FragnoPublicClientConfig } from "../mod";
|
|
9
|
+
import { FragnoClientFetchNetworkError } from "./client-error";
|
|
10
|
+
import { waitForAsyncIterator } from "../util/async";
|
|
11
|
+
|
|
12
|
+
// Mock fetch globally
|
|
13
|
+
global.fetch = vi.fn();
|
|
14
|
+
|
|
15
|
+
describe("createVanillaListeners", () => {
|
|
16
|
+
const testFragmentDefinition = defineFragment("test-fragment");
|
|
17
|
+
const testRoutes = [
|
|
18
|
+
defineRoute({
|
|
19
|
+
method: "GET",
|
|
20
|
+
path: "/users",
|
|
21
|
+
outputSchema: z.array(z.object({ id: z.number(), name: z.string() })),
|
|
22
|
+
handler: async (_ctx, { json }) => json([{ id: 1, name: "John" }]),
|
|
23
|
+
}),
|
|
24
|
+
defineRoute({
|
|
25
|
+
method: "GET",
|
|
26
|
+
path: "/users/:id",
|
|
27
|
+
outputSchema: z.object({ id: z.number(), name: z.string() }),
|
|
28
|
+
handler: async ({ pathParams }, { json }) =>
|
|
29
|
+
json({ id: Number(pathParams["id"]), name: "John" }),
|
|
30
|
+
}),
|
|
31
|
+
defineRoute({
|
|
32
|
+
method: "GET",
|
|
33
|
+
path: "/search",
|
|
34
|
+
outputSchema: z.array(z.string()),
|
|
35
|
+
handler: async (_ctx, { json }) => json(["result1", "result2"]),
|
|
36
|
+
}),
|
|
37
|
+
] as const;
|
|
38
|
+
|
|
39
|
+
const clientConfig: FragnoPublicClientConfig = {
|
|
40
|
+
baseUrl: "http://localhost:3000",
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
vi.clearAllMocks();
|
|
45
|
+
(global.fetch as ReturnType<typeof vi.fn>).mockReset();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
afterEach(() => {
|
|
49
|
+
vi.restoreAllMocks();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("should create vanilla listeners for a simple GET route", async () => {
|
|
53
|
+
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
|
54
|
+
headers: new Headers(),
|
|
55
|
+
ok: true,
|
|
56
|
+
json: async () => [{ id: 1, name: "John" }],
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const client = createClientBuilder(testFragmentDefinition, clientConfig, testRoutes);
|
|
60
|
+
const clientObj = {
|
|
61
|
+
users: client.createHook("/users"),
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const { users } = useFragno(clientObj);
|
|
65
|
+
const userStore = users();
|
|
66
|
+
|
|
67
|
+
// Subscribe to trigger the fetch - this will keep the store active
|
|
68
|
+
const unsubscribe = userStore.subscribe(() => {});
|
|
69
|
+
|
|
70
|
+
// Wait for data to load
|
|
71
|
+
await vi.waitFor(() => {
|
|
72
|
+
const state = userStore.get();
|
|
73
|
+
expect(state.loading).toBe(false);
|
|
74
|
+
expect(state.error).toBeUndefined();
|
|
75
|
+
expect(state.data).toBeDefined();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const state = userStore.get();
|
|
79
|
+
expect(state.data).toEqual([{ id: 1, name: "John" }]);
|
|
80
|
+
expect(state.error).toBeUndefined();
|
|
81
|
+
|
|
82
|
+
unsubscribe();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("should support listen and subscribe methods", async () => {
|
|
86
|
+
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
|
87
|
+
headers: new Headers(),
|
|
88
|
+
ok: true,
|
|
89
|
+
json: async () => [{ id: 1, name: "John" }],
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const client = createClientBuilder(testFragmentDefinition, clientConfig, testRoutes);
|
|
93
|
+
const clientObj = {
|
|
94
|
+
users: client.createHook("/users"),
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const { users } = useFragno(clientObj);
|
|
98
|
+
const userStore = users();
|
|
99
|
+
|
|
100
|
+
const listenCallback = vi.fn();
|
|
101
|
+
const subscribeCallback = vi.fn();
|
|
102
|
+
|
|
103
|
+
userStore.listen(listenCallback);
|
|
104
|
+
userStore.subscribe(subscribeCallback);
|
|
105
|
+
|
|
106
|
+
// Wait for initial load
|
|
107
|
+
await vi.waitFor(() => {
|
|
108
|
+
const state = userStore.get();
|
|
109
|
+
expect(state.loading).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
expect(listenCallback).toHaveBeenCalled();
|
|
113
|
+
expect(subscribeCallback).toHaveBeenCalled();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("should support refetch functionality", async () => {
|
|
117
|
+
let callCount = 0;
|
|
118
|
+
(global.fetch as ReturnType<typeof vi.fn>).mockImplementation(async () => {
|
|
119
|
+
callCount++;
|
|
120
|
+
return {
|
|
121
|
+
headers: new Headers(),
|
|
122
|
+
ok: true,
|
|
123
|
+
json: async () => [{ id: callCount, name: `John${callCount}` }],
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const client = createClientBuilder(testFragmentDefinition, clientConfig, testRoutes);
|
|
128
|
+
const clientObj = {
|
|
129
|
+
useUsers: client.createHook("/users"),
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const { useUsers } = useFragno(clientObj);
|
|
133
|
+
const userStore = useUsers();
|
|
134
|
+
|
|
135
|
+
const stateAfterInitialLoad = await waitForAsyncIterator(
|
|
136
|
+
userStore,
|
|
137
|
+
(state) => state.loading === false,
|
|
138
|
+
);
|
|
139
|
+
expect(stateAfterInitialLoad).toEqual({
|
|
140
|
+
loading: false,
|
|
141
|
+
data: [{ id: 1, name: "John1" }],
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Refetch data
|
|
145
|
+
userStore.refetch();
|
|
146
|
+
|
|
147
|
+
const stateAfterRefetch = await waitForAsyncIterator(
|
|
148
|
+
userStore,
|
|
149
|
+
(state) => state.loading === false,
|
|
150
|
+
);
|
|
151
|
+
expect(stateAfterRefetch).toEqual({
|
|
152
|
+
loading: false,
|
|
153
|
+
data: [{ id: 2, name: "John2" }],
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
expect(fetch).toHaveBeenCalledTimes(2);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("should create vanilla listeners with path parameters", async () => {
|
|
160
|
+
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
|
161
|
+
headers: new Headers(),
|
|
162
|
+
ok: true,
|
|
163
|
+
json: async () => ({ id: 123, name: "John" }),
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const client = createClientBuilder(testFragmentDefinition, clientConfig, testRoutes);
|
|
167
|
+
const clientObj = {
|
|
168
|
+
user: client.createHook("/users/:id"),
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const { user } = useFragno(clientObj);
|
|
172
|
+
const userStore = user({ path: { id: "123" } });
|
|
173
|
+
|
|
174
|
+
// Subscribe to trigger the fetch - this will keep the store active
|
|
175
|
+
const unsubscribe = userStore.subscribe(() => {});
|
|
176
|
+
|
|
177
|
+
await vi.waitFor(() => {
|
|
178
|
+
const state = userStore.get();
|
|
179
|
+
expect(state.loading).toBe(false);
|
|
180
|
+
expect(state.data).toBeDefined();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
expect(userStore.get().data).toEqual({ id: 123, name: "John" });
|
|
184
|
+
expect(fetch).toHaveBeenCalledWith("http://localhost:3000/api/test-fragment/users/123");
|
|
185
|
+
|
|
186
|
+
unsubscribe();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("should create vanilla listeners with query parameters", async () => {
|
|
190
|
+
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
|
191
|
+
headers: new Headers(),
|
|
192
|
+
ok: true,
|
|
193
|
+
json: async () => ["result1", "result2"],
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const client = createClientBuilder(testFragmentDefinition, clientConfig, testRoutes);
|
|
197
|
+
const clientObj = {
|
|
198
|
+
search: client.createHook("/search"),
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const { search } = useFragno(clientObj);
|
|
202
|
+
const searchStore = search({ query: { q: "test" } });
|
|
203
|
+
|
|
204
|
+
// Subscribe to trigger the fetch - this will keep the store active
|
|
205
|
+
const unsubscribe = searchStore.subscribe(() => {});
|
|
206
|
+
|
|
207
|
+
await vi.waitFor(() => {
|
|
208
|
+
const state = searchStore.get();
|
|
209
|
+
expect(state.loading).toBe(false);
|
|
210
|
+
expect(state.data).toBeDefined();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
expect(searchStore.get().data).toEqual(["result1", "result2"]);
|
|
214
|
+
expect(fetch).toHaveBeenCalledWith("http://localhost:3000/api/test-fragment/search?q=test");
|
|
215
|
+
|
|
216
|
+
unsubscribe();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("should support async iteration over hook state changes", async () => {
|
|
220
|
+
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
|
221
|
+
headers: new Headers(),
|
|
222
|
+
ok: true,
|
|
223
|
+
json: async () => [{ id: 1, name: "John" }],
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const client = createClientBuilder(testFragmentDefinition, clientConfig, testRoutes);
|
|
227
|
+
const clientObj = {
|
|
228
|
+
users: client.createHook("/users"),
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const { users } = useFragno(clientObj);
|
|
232
|
+
const userStore = users();
|
|
233
|
+
|
|
234
|
+
// Subscribe to trigger the fetch - this will keep the store active
|
|
235
|
+
const unsubscribe = userStore.subscribe(() => {});
|
|
236
|
+
|
|
237
|
+
// Use waitForAsyncIterator to wait for the loaded state
|
|
238
|
+
const finalState = await waitForAsyncIterator(
|
|
239
|
+
userStore,
|
|
240
|
+
(state) => state.loading === false && state.data !== undefined,
|
|
241
|
+
{ timeout: 3000 },
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
// Verify final state
|
|
245
|
+
expect(finalState).toEqual({
|
|
246
|
+
loading: false,
|
|
247
|
+
error: undefined,
|
|
248
|
+
data: [{ id: 1, name: "John" }],
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
unsubscribe();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("should support reactive path parameters with atoms", async () => {
|
|
255
|
+
const idAtom = atom("1");
|
|
256
|
+
|
|
257
|
+
(global.fetch as ReturnType<typeof vi.fn>)
|
|
258
|
+
.mockResolvedValueOnce({
|
|
259
|
+
headers: new Headers(),
|
|
260
|
+
ok: true,
|
|
261
|
+
json: async () => ({ id: 1, name: "John" }),
|
|
262
|
+
})
|
|
263
|
+
.mockResolvedValueOnce({
|
|
264
|
+
headers: new Headers(),
|
|
265
|
+
ok: true,
|
|
266
|
+
json: async () => ({ id: 2, name: "Jane" }),
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const client = createClientBuilder(testFragmentDefinition, clientConfig, testRoutes);
|
|
270
|
+
const clientObj = {
|
|
271
|
+
user: client.createHook("/users/:id"),
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const { user } = useFragno(clientObj);
|
|
275
|
+
const userStore = user({ path: { id: idAtom } });
|
|
276
|
+
|
|
277
|
+
// Subscribe to trigger the fetch - this will keep the store active
|
|
278
|
+
const unsubscribe = userStore.subscribe(() => {});
|
|
279
|
+
|
|
280
|
+
await vi.waitFor(() => {
|
|
281
|
+
const state = userStore.get();
|
|
282
|
+
expect(state.loading).toBe(false);
|
|
283
|
+
expect(state.data).toBeDefined();
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
expect(userStore.get().data).toEqual({ id: 1, name: "John" });
|
|
287
|
+
|
|
288
|
+
// Update the atom value
|
|
289
|
+
idAtom.set("2");
|
|
290
|
+
|
|
291
|
+
await vi.waitFor(() => {
|
|
292
|
+
const state = userStore.get();
|
|
293
|
+
expect(state.data).toEqual({ id: 2, name: "Jane" });
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
expect(fetch).toHaveBeenCalledTimes(2);
|
|
297
|
+
|
|
298
|
+
unsubscribe();
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test("errors then success", async () => {
|
|
302
|
+
(global.fetch as ReturnType<typeof vi.fn>)
|
|
303
|
+
.mockRejectedValueOnce("Network error")
|
|
304
|
+
.mockResolvedValueOnce({
|
|
305
|
+
headers: new Headers(),
|
|
306
|
+
ok: true,
|
|
307
|
+
json: async () => ({ id: 1, name: "John" }),
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
const client = createClientBuilder(testFragmentDefinition, clientConfig, testRoutes);
|
|
311
|
+
const clientObj = {
|
|
312
|
+
user: client.createHook("/users/:id", {
|
|
313
|
+
onErrorRetry: () => 1, // Wait only 1ms before retrying
|
|
314
|
+
}),
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const { user } = useFragno(clientObj);
|
|
318
|
+
const userStore = user({ path: { id: "123" } });
|
|
319
|
+
|
|
320
|
+
const asyncIterator = userStore[Symbol.asyncIterator]();
|
|
321
|
+
|
|
322
|
+
// Initial state: store is loading
|
|
323
|
+
{
|
|
324
|
+
const { value } = await asyncIterator.next();
|
|
325
|
+
expect(value).toBeDefined();
|
|
326
|
+
const { data, error, loading } = value!;
|
|
327
|
+
expect(data).toBeUndefined();
|
|
328
|
+
expect(error).toBeUndefined();
|
|
329
|
+
expect(loading).toBe(true);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// First "result": loading is false, error is now set.
|
|
333
|
+
{
|
|
334
|
+
const { value } = await asyncIterator.next();
|
|
335
|
+
expect(value).toBeDefined();
|
|
336
|
+
const { data, error, loading } = value!;
|
|
337
|
+
expect(data).toBeUndefined();
|
|
338
|
+
expect(error).toBeInstanceOf(FragnoClientFetchNetworkError);
|
|
339
|
+
expect(loading).toBe(false);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Retry initiated: loading is true
|
|
343
|
+
{
|
|
344
|
+
const { value } = await asyncIterator.next();
|
|
345
|
+
expect(value).toBeDefined();
|
|
346
|
+
const { data, error, loading } = value!;
|
|
347
|
+
expect(data).toBeUndefined();
|
|
348
|
+
expect(error).toBeUndefined();
|
|
349
|
+
expect(loading).toBe(true);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Retry succeeded: data is now available.
|
|
353
|
+
{
|
|
354
|
+
const { value } = await asyncIterator.next();
|
|
355
|
+
expect(value).toBeDefined();
|
|
356
|
+
const { data, error, loading } = value!;
|
|
357
|
+
expect(data).toEqual({ id: 1, name: "John" });
|
|
358
|
+
expect(error).toBeUndefined();
|
|
359
|
+
expect(loading).toBe(false);
|
|
360
|
+
}
|
|
361
|
+
const { done } = await asyncIterator.return();
|
|
362
|
+
expect(done).toBe(true);
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
describe("createVanillaMutator", () => {
|
|
367
|
+
const testFragmentDefinition = defineFragment("test-fragment-mutator");
|
|
368
|
+
const testRoutes = [
|
|
369
|
+
defineRoute({
|
|
370
|
+
method: "POST",
|
|
371
|
+
path: "/users",
|
|
372
|
+
inputSchema: z.object({ name: z.string(), email: z.string() }),
|
|
373
|
+
outputSchema: z.object({ id: z.number(), name: z.string(), email: z.string() }),
|
|
374
|
+
handler: async (_ctx, { json }) => json({ id: 1, name: "", email: "" }),
|
|
375
|
+
}),
|
|
376
|
+
defineRoute({
|
|
377
|
+
method: "PUT",
|
|
378
|
+
path: "/users/:id",
|
|
379
|
+
inputSchema: z.object({ name: z.string() }),
|
|
380
|
+
outputSchema: z.object({ id: z.number(), name: z.string() }),
|
|
381
|
+
handler: async ({ pathParams }, { json }) => json({ id: Number(pathParams["id"]), name: "" }),
|
|
382
|
+
}),
|
|
383
|
+
defineRoute({
|
|
384
|
+
method: "DELETE",
|
|
385
|
+
path: "/users/:id",
|
|
386
|
+
inputSchema: z.object({}),
|
|
387
|
+
outputSchema: z.object({ success: z.boolean() }),
|
|
388
|
+
handler: async (_ctx, { json }) => json({ success: true }),
|
|
389
|
+
}),
|
|
390
|
+
] as const;
|
|
391
|
+
|
|
392
|
+
const clientConfig: FragnoPublicClientConfig = {
|
|
393
|
+
baseUrl: "http://localhost:3000",
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
beforeEach(() => {
|
|
397
|
+
vi.clearAllMocks();
|
|
398
|
+
(global.fetch as ReturnType<typeof vi.fn>).mockReset();
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
afterEach(() => {
|
|
402
|
+
vi.restoreAllMocks();
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
test("should create a vanilla mutator for POST route", async () => {
|
|
406
|
+
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
|
407
|
+
headers: new Headers(),
|
|
408
|
+
ok: true,
|
|
409
|
+
json: async () => ({ id: 1, name: "John", email: "john@example.com" }),
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
const client = createClientBuilder(testFragmentDefinition, clientConfig, testRoutes);
|
|
413
|
+
const clientObj = {
|
|
414
|
+
createUser: client.createMutator("POST", "/users"),
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
const { createUser } = useFragno(clientObj);
|
|
418
|
+
const mutator = createUser();
|
|
419
|
+
|
|
420
|
+
const result = await mutator.mutate({
|
|
421
|
+
body: { name: "John", email: "john@example.com" },
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
expect(result).toEqual({ id: 1, name: "John", email: "john@example.com" });
|
|
425
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
426
|
+
expect.stringContaining("/users"),
|
|
427
|
+
expect.objectContaining({
|
|
428
|
+
method: "POST",
|
|
429
|
+
body: JSON.stringify({ name: "John", email: "john@example.com" }),
|
|
430
|
+
}),
|
|
431
|
+
);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
test("should track loading state during mutation", async () => {
|
|
435
|
+
let resolvePromise: (value: unknown) => void;
|
|
436
|
+
const fetchPromise = new Promise((resolve) => {
|
|
437
|
+
resolvePromise = resolve;
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
(global.fetch as ReturnType<typeof vi.fn>).mockReturnValueOnce(fetchPromise);
|
|
441
|
+
|
|
442
|
+
const client = createClientBuilder(testFragmentDefinition, clientConfig, testRoutes);
|
|
443
|
+
const clientObj = {
|
|
444
|
+
createUser: client.createMutator("POST", "/users"),
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
const { createUser } = useFragno(clientObj);
|
|
448
|
+
const mutator = createUser();
|
|
449
|
+
|
|
450
|
+
const stateChanges: Array<{ loading?: boolean; data?: unknown }> = [];
|
|
451
|
+
mutator.subscribe((state) => {
|
|
452
|
+
stateChanges.push({ loading: state.loading, data: state.data });
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
// Initial state
|
|
456
|
+
expect(mutator.get()).toEqual({ loading: false, error: undefined, data: undefined });
|
|
457
|
+
|
|
458
|
+
// Start mutation
|
|
459
|
+
const mutatePromise = mutator.mutate({
|
|
460
|
+
body: { name: "John", email: "john@example.com" },
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
// Check loading state
|
|
464
|
+
await vi.waitFor(() => {
|
|
465
|
+
expect(mutator.get().loading).toBe(true);
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
// Resolve the fetch
|
|
469
|
+
resolvePromise!({
|
|
470
|
+
headers: new Headers(),
|
|
471
|
+
ok: true,
|
|
472
|
+
json: async () => ({ id: 1, name: "John", email: "john@example.com" }),
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
const result = await mutatePromise;
|
|
476
|
+
|
|
477
|
+
// Check final state
|
|
478
|
+
expect(mutator.get()).toEqual({
|
|
479
|
+
loading: false,
|
|
480
|
+
error: undefined,
|
|
481
|
+
data: { id: 1, name: "John", email: "john@example.com" },
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
expect(result).toEqual({ id: 1, name: "John", email: "john@example.com" });
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
test("should create a vanilla mutator for PUT route with path params", async () => {
|
|
488
|
+
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
|
489
|
+
headers: new Headers(),
|
|
490
|
+
ok: true,
|
|
491
|
+
json: async () => ({ id: 123, name: "Jane" }),
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
const client = createClientBuilder(testFragmentDefinition, clientConfig, testRoutes);
|
|
495
|
+
const clientObj = {
|
|
496
|
+
updateUser: client.createMutator("PUT", "/users/:id"),
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
const { updateUser } = useFragno(clientObj);
|
|
500
|
+
const mutator = updateUser();
|
|
501
|
+
|
|
502
|
+
const result = await mutator.mutate({
|
|
503
|
+
body: { name: "Jane" },
|
|
504
|
+
path: { id: "123" },
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
expect(result).toEqual({ id: 123, name: "Jane" });
|
|
508
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
509
|
+
expect.stringContaining("/users/123"),
|
|
510
|
+
expect.objectContaining({
|
|
511
|
+
method: "PUT",
|
|
512
|
+
body: JSON.stringify({ name: "Jane" }),
|
|
513
|
+
}),
|
|
514
|
+
);
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
test("should create a vanilla mutator for DELETE route", async () => {
|
|
518
|
+
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
|
519
|
+
headers: new Headers(),
|
|
520
|
+
ok: true,
|
|
521
|
+
json: async () => ({ success: true }),
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
const client = createClientBuilder(testFragmentDefinition, clientConfig, testRoutes);
|
|
525
|
+
const clientObj = {
|
|
526
|
+
deleteUser: client.createMutator("DELETE", "/users/:id"),
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
const { deleteUser } = useFragno(clientObj);
|
|
530
|
+
const mutator = deleteUser();
|
|
531
|
+
|
|
532
|
+
const result = await mutator.mutate({
|
|
533
|
+
body: {},
|
|
534
|
+
path: { id: "123" },
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
expect(result).toEqual({ success: true });
|
|
538
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
539
|
+
expect.stringContaining("/users/123"),
|
|
540
|
+
expect.objectContaining({
|
|
541
|
+
method: "DELETE",
|
|
542
|
+
}),
|
|
543
|
+
);
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
test("should support subscribe method for mutator state changes", async () => {
|
|
547
|
+
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
|
548
|
+
headers: new Headers(),
|
|
549
|
+
ok: true,
|
|
550
|
+
json: async () => ({ id: 1, name: "John", email: "john@example.com" }),
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
const client = createClientBuilder(testFragmentDefinition, clientConfig, testRoutes);
|
|
554
|
+
const clientObj = {
|
|
555
|
+
createUser: client.createMutator("POST", "/users"),
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
const { createUser } = useFragno(clientObj);
|
|
559
|
+
const mutator = createUser();
|
|
560
|
+
|
|
561
|
+
const stateCallback = vi.fn();
|
|
562
|
+
mutator.subscribe(stateCallback);
|
|
563
|
+
|
|
564
|
+
await mutator.mutate({
|
|
565
|
+
body: { name: "John", email: "john@example.com" },
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
// Should be called for loading state changes
|
|
569
|
+
expect(stateCallback).toHaveBeenCalled();
|
|
570
|
+
expect(stateCallback).toHaveBeenCalledWith({
|
|
571
|
+
loading: true,
|
|
572
|
+
error: undefined,
|
|
573
|
+
data: undefined,
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
expect(stateCallback).toHaveBeenCalledWith({
|
|
577
|
+
loading: false,
|
|
578
|
+
error: undefined,
|
|
579
|
+
data: { id: 1, name: "John", email: "john@example.com" },
|
|
580
|
+
});
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
test("should support async iteration over mutator state changes", async () => {
|
|
584
|
+
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
|
585
|
+
headers: new Headers(),
|
|
586
|
+
ok: true,
|
|
587
|
+
json: async () => ({ id: 1, name: "John", email: "john@example.com" }),
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
const client = createClientBuilder(testFragmentDefinition, clientConfig, testRoutes);
|
|
591
|
+
const clientObj = {
|
|
592
|
+
createUser: client.createMutator("POST", "/users"),
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
const { createUser } = useFragno(clientObj);
|
|
596
|
+
const mutator = createUser();
|
|
597
|
+
|
|
598
|
+
// Start the mutation
|
|
599
|
+
const mutatePromise = mutator.mutate({
|
|
600
|
+
body: { name: "John", email: "john@example.com" },
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
// Use waitForAsyncIterator to wait for the completed state
|
|
604
|
+
const finalState = await waitForAsyncIterator(
|
|
605
|
+
mutator,
|
|
606
|
+
(state) => state.loading === false && state.data !== undefined,
|
|
607
|
+
{ timeout: 3000 },
|
|
608
|
+
);
|
|
609
|
+
|
|
610
|
+
// Verify final state
|
|
611
|
+
expect(finalState).toEqual({
|
|
612
|
+
loading: false,
|
|
613
|
+
error: undefined,
|
|
614
|
+
data: { id: 1, name: "John", email: "john@example.com" },
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
// Ensure mutation completes
|
|
618
|
+
const result = await mutatePromise;
|
|
619
|
+
expect(result).toEqual({ id: 1, name: "John", email: "john@example.com" });
|
|
620
|
+
});
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
describe("useFragno", () => {
|
|
624
|
+
const testFragmentDefinition = defineFragment("test-fragment-useFragno");
|
|
625
|
+
const testRoutes = [
|
|
626
|
+
defineRoute({
|
|
627
|
+
method: "GET",
|
|
628
|
+
path: "/data",
|
|
629
|
+
outputSchema: z.string(),
|
|
630
|
+
handler: async (_ctx, { json }) => json("test data"),
|
|
631
|
+
}),
|
|
632
|
+
defineRoute({
|
|
633
|
+
method: "POST",
|
|
634
|
+
path: "/action",
|
|
635
|
+
inputSchema: z.object({ value: z.string() }),
|
|
636
|
+
outputSchema: z.object({ result: z.string() }),
|
|
637
|
+
handler: async (_ctx, { json }) => json({ result: "test value" }),
|
|
638
|
+
}),
|
|
639
|
+
] as const;
|
|
640
|
+
|
|
641
|
+
const clientConfig: FragnoPublicClientConfig = {
|
|
642
|
+
baseUrl: "http://localhost:3000",
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
beforeEach(() => {
|
|
646
|
+
vi.clearAllMocks();
|
|
647
|
+
(global.fetch as ReturnType<typeof vi.fn>).mockReset();
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
afterEach(() => {
|
|
651
|
+
vi.restoreAllMocks();
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
test("should transform a mixed object of hooks and mutators", async () => {
|
|
655
|
+
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
|
656
|
+
headers: new Headers(),
|
|
657
|
+
ok: true,
|
|
658
|
+
json: async () => "test data",
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
const client = createClientBuilder(testFragmentDefinition, clientConfig, testRoutes);
|
|
662
|
+
const clientObj = {
|
|
663
|
+
data: client.createHook("/data"),
|
|
664
|
+
postAction: client.createMutator("POST", "/action"),
|
|
665
|
+
};
|
|
666
|
+
|
|
667
|
+
const { data, postAction } = useFragno(clientObj);
|
|
668
|
+
|
|
669
|
+
// Test the hook
|
|
670
|
+
const dataStore = data();
|
|
671
|
+
|
|
672
|
+
// Subscribe to trigger the fetch - this will keep the store active
|
|
673
|
+
const unsubscribe = dataStore.subscribe(() => {});
|
|
674
|
+
|
|
675
|
+
await vi.waitFor(() => {
|
|
676
|
+
const state = dataStore.get();
|
|
677
|
+
expect(state.loading).toBe(false);
|
|
678
|
+
expect(state.error).toBeUndefined();
|
|
679
|
+
expect(state.data).toBeDefined();
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
expect(dataStore.get().data).toBe("test data");
|
|
683
|
+
|
|
684
|
+
// Test the mutator
|
|
685
|
+
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
|
686
|
+
headers: new Headers(),
|
|
687
|
+
ok: true,
|
|
688
|
+
json: async () => ({ result: "test value" }),
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
const mutator = postAction();
|
|
692
|
+
const mutatorResult = await mutator.mutate({
|
|
693
|
+
body: { value: "test value" },
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
expect(mutatorResult).toEqual({ result: "test value" });
|
|
697
|
+
|
|
698
|
+
unsubscribe();
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
test("should pass through non-hook values unchanged", () => {
|
|
702
|
+
const client = createClientBuilder(testFragmentDefinition, clientConfig, testRoutes);
|
|
703
|
+
const clientObj = {
|
|
704
|
+
useData: client.createHook("/data"),
|
|
705
|
+
someString: "hello world",
|
|
706
|
+
someNumber: 42,
|
|
707
|
+
someObject: { foo: "bar", nested: { value: true } },
|
|
708
|
+
someArray: [1, 2, 3],
|
|
709
|
+
someFunction: () => "test",
|
|
710
|
+
someNull: null,
|
|
711
|
+
someUndefined: undefined,
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
const result = useFragno(clientObj);
|
|
715
|
+
|
|
716
|
+
// Check that non-hook values are passed through unchanged
|
|
717
|
+
expect(result.someString).toBe("hello world");
|
|
718
|
+
expect(result.someNumber).toBe(42);
|
|
719
|
+
expect(result.someObject).toEqual({ foo: "bar", nested: { value: true } });
|
|
720
|
+
expect(result.someArray).toEqual([1, 2, 3]);
|
|
721
|
+
expect(result.someFunction()).toBe("test");
|
|
722
|
+
expect(result.someNull).toBeNull();
|
|
723
|
+
expect(result.someUndefined).toBeUndefined();
|
|
724
|
+
|
|
725
|
+
// Verify that the hook is still transformed
|
|
726
|
+
expect(typeof result.useData).toBe("function");
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
test("multiple GET hooks for the same path should share the same store", async () => {
|
|
730
|
+
(global.fetch as ReturnType<typeof vi.fn>)
|
|
731
|
+
.mockResolvedValueOnce({
|
|
732
|
+
headers: new Headers(),
|
|
733
|
+
ok: true,
|
|
734
|
+
json: async () => "data1",
|
|
735
|
+
})
|
|
736
|
+
.mockResolvedValueOnce({
|
|
737
|
+
headers: new Headers(),
|
|
738
|
+
ok: true,
|
|
739
|
+
json: async () => "data2",
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
const client = createClientBuilder(testFragmentDefinition, clientConfig, testRoutes);
|
|
743
|
+
const clientObj = {
|
|
744
|
+
data1: client.createHook("/data"),
|
|
745
|
+
data2: client.createHook("/data"),
|
|
746
|
+
};
|
|
747
|
+
|
|
748
|
+
const { data1, data2 } = useFragno(clientObj);
|
|
749
|
+
|
|
750
|
+
const store1 = data1();
|
|
751
|
+
const store2 = data2();
|
|
752
|
+
|
|
753
|
+
// Subscribe to trigger the fetches - this will keep the stores active
|
|
754
|
+
const unsubscribe1 = store1.subscribe(() => {});
|
|
755
|
+
const unsubscribe2 = store2.subscribe(() => {});
|
|
756
|
+
|
|
757
|
+
await vi.waitFor(() => {
|
|
758
|
+
expect(store1.get().loading).toBe(false);
|
|
759
|
+
expect(store2.get().loading).toBe(false);
|
|
760
|
+
expect(store1.get().data).toBeDefined();
|
|
761
|
+
expect(store2.get().data).toBeDefined();
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
expect(fetch).toHaveBeenCalledTimes(1);
|
|
765
|
+
|
|
766
|
+
expect(store1.get().data).toBe("data1");
|
|
767
|
+
expect(store2.get().data).toBe("data1");
|
|
768
|
+
|
|
769
|
+
unsubscribe1();
|
|
770
|
+
unsubscribe2();
|
|
771
|
+
});
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
describe("error handling", () => {
|
|
775
|
+
const testFragmentDefinition = defineFragment("test-fragment-errors");
|
|
776
|
+
const testRoutes = [
|
|
777
|
+
defineRoute({
|
|
778
|
+
method: "GET",
|
|
779
|
+
path: "/users",
|
|
780
|
+
outputSchema: z.array(z.object({ id: z.number(), name: z.string() })),
|
|
781
|
+
handler: async (_ctx, { json }) => json([{ id: 1, name: "John" }]),
|
|
782
|
+
}),
|
|
783
|
+
defineRoute({
|
|
784
|
+
method: "POST",
|
|
785
|
+
path: "/users",
|
|
786
|
+
inputSchema: z.object({ name: z.string(), email: z.string() }),
|
|
787
|
+
outputSchema: z.object({ id: z.number(), name: z.string(), email: z.string() }),
|
|
788
|
+
handler: async (_ctx, { json }) => json({ id: 1, name: "", email: "" }),
|
|
789
|
+
}),
|
|
790
|
+
] as const;
|
|
791
|
+
|
|
792
|
+
const clientConfig: FragnoPublicClientConfig = {
|
|
793
|
+
baseUrl: "http://localhost:3000",
|
|
794
|
+
};
|
|
795
|
+
|
|
796
|
+
beforeEach(() => {
|
|
797
|
+
vi.clearAllMocks();
|
|
798
|
+
(global.fetch as ReturnType<typeof vi.fn>).mockReset();
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
afterEach(() => {
|
|
802
|
+
vi.restoreAllMocks();
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
test("should handle GET hook errors gracefully", async () => {
|
|
806
|
+
(global.fetch as ReturnType<typeof vi.fn>).mockRejectedValueOnce(new Error("Network error"));
|
|
807
|
+
|
|
808
|
+
const builder = createClientBuilder(testFragmentDefinition, clientConfig, testRoutes);
|
|
809
|
+
const clientObj = {
|
|
810
|
+
users: builder.createHook("/users", {
|
|
811
|
+
onErrorRetry: null,
|
|
812
|
+
}),
|
|
813
|
+
};
|
|
814
|
+
|
|
815
|
+
const { users } = useFragno(clientObj);
|
|
816
|
+
const userStore = users();
|
|
817
|
+
|
|
818
|
+
const asyncIterator = userStore[Symbol.asyncIterator]();
|
|
819
|
+
|
|
820
|
+
{
|
|
821
|
+
const { value } = await asyncIterator.next();
|
|
822
|
+
expect(value).toBeDefined();
|
|
823
|
+
const { data, error, loading } = value!;
|
|
824
|
+
expect(data).toBeUndefined();
|
|
825
|
+
expect(error).toBeUndefined();
|
|
826
|
+
expect(loading).toBe(true);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
{
|
|
830
|
+
const { value } = await asyncIterator.next();
|
|
831
|
+
expect(value).toBeDefined();
|
|
832
|
+
const { data, error, loading } = value!;
|
|
833
|
+
expect(data).toBeUndefined();
|
|
834
|
+
expect(error).toBeInstanceOf(FragnoClientFetchNetworkError);
|
|
835
|
+
expect(loading).toBe(false);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
await asyncIterator.return();
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
test("should handle mutator errors gracefully", async () => {
|
|
842
|
+
(global.fetch as ReturnType<typeof vi.fn>).mockRejectedValueOnce(new Error("Server error"));
|
|
843
|
+
|
|
844
|
+
const client = createClientBuilder(testFragmentDefinition, clientConfig, testRoutes);
|
|
845
|
+
const clientObj = {
|
|
846
|
+
createUser: client.createMutator("POST", "/users"),
|
|
847
|
+
};
|
|
848
|
+
|
|
849
|
+
const { createUser } = useFragno(clientObj);
|
|
850
|
+
const mutator = createUser();
|
|
851
|
+
|
|
852
|
+
mutator.mutate({
|
|
853
|
+
body: { name: "John", email: "john@example.com" },
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
const finalState = await waitForAsyncIterator(
|
|
857
|
+
mutator,
|
|
858
|
+
(state) => state.error !== undefined && state.loading === false,
|
|
859
|
+
);
|
|
860
|
+
|
|
861
|
+
expect(finalState).toEqual({
|
|
862
|
+
loading: false,
|
|
863
|
+
error: expect.any(FragnoClientFetchNetworkError),
|
|
864
|
+
data: undefined,
|
|
865
|
+
});
|
|
866
|
+
});
|
|
867
|
+
});
|