@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,947 @@
|
|
|
1
|
+
import { test, expect, describe, vi, beforeEach, afterEach, expectTypeOf } from "vitest";
|
|
2
|
+
import { renderHook, act, waitFor } from "@testing-library/react";
|
|
3
|
+
import { atom, computed, type ReadableAtom } from "nanostores";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { createClientBuilder } from "./client";
|
|
6
|
+
import { useFragno, useStore, type FragnoReactStore } from "./react";
|
|
7
|
+
import { defineRoute } from "../api/route";
|
|
8
|
+
import { defineFragment } from "../api/fragment";
|
|
9
|
+
import type { FragnoPublicClientConfig } from "../mod";
|
|
10
|
+
import { FragnoClientFetchNetworkError, type FragnoClientError } from "./client-error";
|
|
11
|
+
import { RequestOutputContext } from "../api/request-output-context";
|
|
12
|
+
import type { FetcherStore } from "@nanostores/query";
|
|
13
|
+
|
|
14
|
+
// Mock fetch globally
|
|
15
|
+
global.fetch = vi.fn();
|
|
16
|
+
|
|
17
|
+
describe("createReactHook", () => {
|
|
18
|
+
const testFragmentDefinition = defineFragment("test-fragment");
|
|
19
|
+
const testRoutes = [
|
|
20
|
+
defineRoute({
|
|
21
|
+
method: "GET",
|
|
22
|
+
path: "/users",
|
|
23
|
+
outputSchema: z.array(z.object({ id: z.number(), name: z.string() })),
|
|
24
|
+
handler: async (_ctx, { json }) => json([{ id: 1, name: "John" }]),
|
|
25
|
+
}),
|
|
26
|
+
defineRoute({
|
|
27
|
+
method: "GET",
|
|
28
|
+
path: "/users/:id",
|
|
29
|
+
outputSchema: z.object({ id: z.number(), name: z.string() }),
|
|
30
|
+
handler: async ({ pathParams }, { json }) =>
|
|
31
|
+
json({ id: Number(pathParams["id"]), name: "John" }),
|
|
32
|
+
}),
|
|
33
|
+
defineRoute({
|
|
34
|
+
method: "GET",
|
|
35
|
+
path: "/search",
|
|
36
|
+
outputSchema: z.array(z.string()),
|
|
37
|
+
handler: async (_ctx, { json }) => json(["result1", "result2"]),
|
|
38
|
+
}),
|
|
39
|
+
] as const;
|
|
40
|
+
|
|
41
|
+
const clientConfig: FragnoPublicClientConfig = {
|
|
42
|
+
baseUrl: "http://localhost:3000",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
vi.clearAllMocks();
|
|
47
|
+
(global.fetch as ReturnType<typeof vi.fn>).mockReset();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
afterEach(() => {
|
|
51
|
+
vi.restoreAllMocks();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("should create a hook for a simple GET route", async () => {
|
|
55
|
+
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
|
56
|
+
headers: new Headers(),
|
|
57
|
+
ok: true,
|
|
58
|
+
json: async () => [{ id: 1, name: "John" }],
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const client = createClientBuilder(testFragmentDefinition, clientConfig, testRoutes);
|
|
62
|
+
const clientObj = {
|
|
63
|
+
users: client.createHook("/users"),
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const { users } = useFragno(clientObj);
|
|
67
|
+
const { result } = renderHook(() => users());
|
|
68
|
+
|
|
69
|
+
await waitFor(() => {
|
|
70
|
+
expect(result.current.loading).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
expect(result.current.data).toEqual([{ id: 1, name: "John" }]);
|
|
74
|
+
expect(result.current.error).toBeUndefined();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("should create a hook with path parameters", async () => {
|
|
78
|
+
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
|
79
|
+
headers: new Headers(),
|
|
80
|
+
ok: true,
|
|
81
|
+
json: async () => ({ id: 123, name: "John" }),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const client = createClientBuilder(testFragmentDefinition, clientConfig, testRoutes);
|
|
85
|
+
const clientObj = {
|
|
86
|
+
user: client.createHook("/users/:id"),
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const { user } = useFragno(clientObj);
|
|
90
|
+
const { result } = renderHook(() => user({ path: { id: "123" } }));
|
|
91
|
+
|
|
92
|
+
await waitFor(() => {
|
|
93
|
+
expect(result.current.loading).toBe(false);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
expect(result.current.data).toEqual({ id: 123, name: "John" });
|
|
97
|
+
expect(fetch).toHaveBeenCalledWith("http://localhost:3000/api/test-fragment/users/123");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("should create a hook with query parameters", async () => {
|
|
101
|
+
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
|
102
|
+
headers: new Headers(),
|
|
103
|
+
ok: true,
|
|
104
|
+
json: async () => ["result1", "result2"],
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const client = createClientBuilder(testFragmentDefinition, clientConfig, testRoutes);
|
|
108
|
+
const clientObj = {
|
|
109
|
+
search: client.createHook("/search"),
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const { search } = useFragno(clientObj);
|
|
113
|
+
const { result } = renderHook(() => search({ query: { q: "test" } }));
|
|
114
|
+
|
|
115
|
+
await waitFor(() => {
|
|
116
|
+
expect(result.current.loading).toBe(false);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
expect(result.current.data).toEqual(["result1", "result2"]);
|
|
120
|
+
expect(fetch).toHaveBeenCalledWith("http://localhost:3000/api/test-fragment/search?q=test");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("should support reactive path parameters with atoms", async () => {
|
|
124
|
+
const idAtom = atom("1");
|
|
125
|
+
|
|
126
|
+
(global.fetch as ReturnType<typeof vi.fn>)
|
|
127
|
+
.mockResolvedValueOnce({
|
|
128
|
+
headers: new Headers(),
|
|
129
|
+
ok: true,
|
|
130
|
+
json: async () => ({ id: 1, name: "John" }),
|
|
131
|
+
})
|
|
132
|
+
.mockResolvedValueOnce({
|
|
133
|
+
headers: new Headers(),
|
|
134
|
+
ok: true,
|
|
135
|
+
json: async () => ({ id: 2, name: "Jane" }),
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const client = createClientBuilder(testFragmentDefinition, clientConfig, testRoutes);
|
|
139
|
+
const clientObj = {
|
|
140
|
+
user: client.createHook("/users/:id"),
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const { user } = useFragno(clientObj);
|
|
144
|
+
const { result } = renderHook(() => user({ path: { id: idAtom } }));
|
|
145
|
+
|
|
146
|
+
await waitFor(() => {
|
|
147
|
+
expect(result.current.loading).toBe(false);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
expect(result.current.data).toEqual({ id: 1, name: "John" });
|
|
151
|
+
|
|
152
|
+
// Update the atom value
|
|
153
|
+
act(() => {
|
|
154
|
+
idAtom.set("2");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
await waitFor(() => {
|
|
158
|
+
expect(result.current.data).toEqual({ id: 2, name: "Jane" });
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
expect(fetch).toHaveBeenCalledTimes(2);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("should handle errors gracefully", async () => {
|
|
165
|
+
(global.fetch as ReturnType<typeof vi.fn>).mockRejectedValueOnce(new Error("Network error"));
|
|
166
|
+
|
|
167
|
+
const client = createClientBuilder(testFragmentDefinition, clientConfig, testRoutes);
|
|
168
|
+
const clientObj = {
|
|
169
|
+
useUsers: client.createHook("/users"),
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const { useUsers } = useFragno(clientObj);
|
|
173
|
+
const { result } = renderHook(() => useUsers());
|
|
174
|
+
|
|
175
|
+
await waitFor(() => {
|
|
176
|
+
expect(result.current.loading).toBe(false);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
expect(result.current.error).toBeInstanceOf(FragnoClientFetchNetworkError);
|
|
180
|
+
expect(result.current.data).toBeUndefined();
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe("createReactMutator", () => {
|
|
185
|
+
const testFragmentDefinition = defineFragment("test-fragment");
|
|
186
|
+
const testRoutes = [
|
|
187
|
+
defineRoute({
|
|
188
|
+
method: "POST",
|
|
189
|
+
path: "/users",
|
|
190
|
+
inputSchema: z.object({ name: z.string(), email: z.string() }),
|
|
191
|
+
outputSchema: z.object({ id: z.number(), name: z.string(), email: z.string() }),
|
|
192
|
+
handler: async (_ctx, { json }) => json({ id: 1, name: "", email: "" }),
|
|
193
|
+
}),
|
|
194
|
+
defineRoute({
|
|
195
|
+
method: "PUT",
|
|
196
|
+
path: "/users/:id",
|
|
197
|
+
inputSchema: z.object({ name: z.string() }),
|
|
198
|
+
outputSchema: z.object({ id: z.number(), name: z.string() }),
|
|
199
|
+
handler: async ({ pathParams }, { json }) => json({ id: Number(pathParams["id"]), name: "" }),
|
|
200
|
+
}),
|
|
201
|
+
] as const;
|
|
202
|
+
|
|
203
|
+
const clientConfig: FragnoPublicClientConfig = {
|
|
204
|
+
baseUrl: "http://localhost:3000",
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
beforeEach(() => {
|
|
208
|
+
vi.clearAllMocks();
|
|
209
|
+
(global.fetch as ReturnType<typeof vi.fn>).mockReset();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("should be able to use mutator for POST route - direct result", async () => {
|
|
213
|
+
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
|
214
|
+
headers: new Headers(),
|
|
215
|
+
ok: true,
|
|
216
|
+
json: async () => ({ id: 1, name: "John", email: "john@example.com" }),
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const _route1 = testRoutes[0];
|
|
220
|
+
type _Route1 = typeof _route1;
|
|
221
|
+
|
|
222
|
+
const client = createClientBuilder(testFragmentDefinition, clientConfig, testRoutes);
|
|
223
|
+
const clientObj = {
|
|
224
|
+
useCreateUserMutator: client.createMutator("POST", "/users"),
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const { useCreateUserMutator } = useFragno(clientObj);
|
|
228
|
+
const { result: renderedHook } = renderHook(() => useCreateUserMutator());
|
|
229
|
+
const { mutate: createUser } = renderedHook.current;
|
|
230
|
+
|
|
231
|
+
const result = await createUser({
|
|
232
|
+
body: { name: "John", email: "john@example.com" },
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
expect(result).toEqual({ id: 1, name: "John", email: "john@example.com" });
|
|
236
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
237
|
+
expect.stringContaining("/users"),
|
|
238
|
+
expect.objectContaining({
|
|
239
|
+
method: "POST",
|
|
240
|
+
body: JSON.stringify({ name: "John", email: "john@example.com" }),
|
|
241
|
+
}),
|
|
242
|
+
);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("should be able to use mutator for POST route - result in store", async () => {
|
|
246
|
+
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
|
247
|
+
headers: new Headers(),
|
|
248
|
+
ok: true,
|
|
249
|
+
json: async () => ({ id: 1, name: "John", email: "john@example.com" }),
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const client = createClientBuilder(testFragmentDefinition, clientConfig, testRoutes);
|
|
253
|
+
const clientObj = {
|
|
254
|
+
useCreateUserMutator: client.createMutator("POST", "/users"),
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const { useCreateUserMutator } = useFragno(clientObj);
|
|
258
|
+
const { result: renderedHook } = renderHook(() => useCreateUserMutator());
|
|
259
|
+
const { mutate: createUser } = renderedHook.current;
|
|
260
|
+
|
|
261
|
+
await createUser({
|
|
262
|
+
body: { name: "John", email: "john@example.com" },
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
await waitFor(() => {
|
|
266
|
+
expect(renderedHook.current.loading).toBe(false);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
expect(renderedHook.current.data).toEqual({ id: 1, name: "John", email: "john@example.com" });
|
|
270
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
271
|
+
expect.stringContaining("/users"),
|
|
272
|
+
expect.objectContaining({
|
|
273
|
+
method: "POST",
|
|
274
|
+
body: JSON.stringify({ name: "John", email: "john@example.com" }),
|
|
275
|
+
}),
|
|
276
|
+
);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test("should create a mutator for PUT route with path params", async () => {
|
|
280
|
+
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
|
281
|
+
headers: new Headers(),
|
|
282
|
+
ok: true,
|
|
283
|
+
json: async () => ({ id: 123, name: "Jane" }),
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
const client = createClientBuilder(testFragmentDefinition, clientConfig, testRoutes);
|
|
287
|
+
const clientObj = {
|
|
288
|
+
useUpdateUserMutator: client.createMutator("PUT", "/users/:id"),
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const { useUpdateUserMutator } = useFragno(clientObj);
|
|
292
|
+
const { result: renderedHook } = renderHook(() => useUpdateUserMutator());
|
|
293
|
+
const { mutate: updateUser } = renderedHook.current;
|
|
294
|
+
|
|
295
|
+
const result = await updateUser({
|
|
296
|
+
body: { name: "Jane" },
|
|
297
|
+
path: { id: "123" },
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
expect(result).toEqual({ id: 123, name: "Jane" });
|
|
301
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
302
|
+
expect.stringContaining("/users/123"),
|
|
303
|
+
expect.objectContaining({
|
|
304
|
+
method: "PUT",
|
|
305
|
+
body: JSON.stringify({ name: "Jane" }),
|
|
306
|
+
}),
|
|
307
|
+
);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test("should create a mutator for DELETE route (with inputSchema and outputSchema)", async () => {
|
|
311
|
+
const testLocalFragmentDefinition = defineFragment("test-fragment");
|
|
312
|
+
const testLocalRoutes = [
|
|
313
|
+
defineRoute({
|
|
314
|
+
method: "DELETE",
|
|
315
|
+
path: "/users/:id",
|
|
316
|
+
inputSchema: z.object({}),
|
|
317
|
+
outputSchema: z.object({ success: z.boolean() }),
|
|
318
|
+
handler: async (_ctx, { json }) => json({ success: true }),
|
|
319
|
+
}),
|
|
320
|
+
] as const;
|
|
321
|
+
|
|
322
|
+
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
|
323
|
+
headers: new Headers(),
|
|
324
|
+
ok: true,
|
|
325
|
+
json: async () => ({ success: true }),
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
const client = createClientBuilder(testLocalFragmentDefinition, clientConfig, testLocalRoutes);
|
|
329
|
+
const clientObj = {
|
|
330
|
+
useDeleteUserMutator: client.createMutator("DELETE", "/users/:id"),
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
const { useDeleteUserMutator } = useFragno(clientObj);
|
|
334
|
+
const { result: renderedHook } = renderHook(() => useDeleteUserMutator());
|
|
335
|
+
const hook = renderedHook.current;
|
|
336
|
+
|
|
337
|
+
const result = await hook.mutate({
|
|
338
|
+
body: {},
|
|
339
|
+
path: { id: "123" },
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
expect(result).toEqual({ success: true });
|
|
343
|
+
|
|
344
|
+
await waitFor(() => {
|
|
345
|
+
expect(hook.loading).toBe(false);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
expect(hook).toEqual({
|
|
349
|
+
loading: false,
|
|
350
|
+
// TODO: Error and data should be in here, right?
|
|
351
|
+
// error: undefined,
|
|
352
|
+
// data: { success: true },
|
|
353
|
+
mutate: expect.any(Function),
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
357
|
+
expect.stringContaining("/users/123"),
|
|
358
|
+
expect.objectContaining({
|
|
359
|
+
method: "DELETE",
|
|
360
|
+
}),
|
|
361
|
+
);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test("should create a mutator for DELETE route (withOUT inputSchema and outputSchema)", async () => {
|
|
365
|
+
const testLocalFragmentDefinition = defineFragment("test-fragment");
|
|
366
|
+
const testLocalRoutes = [
|
|
367
|
+
defineRoute({
|
|
368
|
+
method: "DELETE",
|
|
369
|
+
path: "/users/:id",
|
|
370
|
+
handler: async (_ctx, { empty }) => empty(),
|
|
371
|
+
}),
|
|
372
|
+
] as const;
|
|
373
|
+
|
|
374
|
+
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
|
375
|
+
headers: new Headers(),
|
|
376
|
+
ok: true,
|
|
377
|
+
status: 204,
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
const client = createClientBuilder(testLocalFragmentDefinition, clientConfig, testLocalRoutes);
|
|
381
|
+
const clientObj = {
|
|
382
|
+
useDeleteUserMutator: client.createMutator("DELETE", "/users/:id"),
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
const { useDeleteUserMutator } = useFragno(clientObj);
|
|
386
|
+
const { result: renderedHook } = renderHook(() => useDeleteUserMutator());
|
|
387
|
+
const hook = renderedHook.current;
|
|
388
|
+
|
|
389
|
+
const result = await hook.mutate({
|
|
390
|
+
path: { id: "123" },
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
expect(result).toBeUndefined();
|
|
394
|
+
|
|
395
|
+
await waitFor(() => {
|
|
396
|
+
expect(hook.loading).toBe(false);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
expect(hook).toEqual({
|
|
400
|
+
loading: false,
|
|
401
|
+
// TODO: Error and data should be in here, right?
|
|
402
|
+
// error: undefined,
|
|
403
|
+
// data: { success: true },
|
|
404
|
+
mutate: expect.any(Function),
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
408
|
+
expect.stringContaining("/users/123"),
|
|
409
|
+
expect.objectContaining({
|
|
410
|
+
method: "DELETE",
|
|
411
|
+
}),
|
|
412
|
+
);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
test("should handle mutation errors", async () => {
|
|
416
|
+
(global.fetch as ReturnType<typeof vi.fn>).mockRejectedValueOnce(new Error("Server error"));
|
|
417
|
+
|
|
418
|
+
const client = createClientBuilder(testFragmentDefinition, clientConfig, testRoutes);
|
|
419
|
+
const clientObj = {
|
|
420
|
+
useCreateUserMutator: client.createMutator("POST", "/users"),
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
const { useCreateUserMutator } = useFragno(clientObj);
|
|
424
|
+
const { result: renderedHook } = renderHook(() => useCreateUserMutator());
|
|
425
|
+
const { mutate: createUser } = renderedHook.current;
|
|
426
|
+
|
|
427
|
+
await expect(
|
|
428
|
+
createUser({
|
|
429
|
+
body: { name: "John", email: "john@example.com" },
|
|
430
|
+
}),
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
await waitFor(() => {
|
|
434
|
+
expect(renderedHook.current.loading).toBe(false);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
expect(renderedHook.current.error).toBeInstanceOf(FragnoClientFetchNetworkError);
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
describe("useFragno", () => {
|
|
442
|
+
const testFragmentDefinition = defineFragment("test-fragment");
|
|
443
|
+
const testRoutes = [
|
|
444
|
+
defineRoute({
|
|
445
|
+
method: "GET",
|
|
446
|
+
path: "/data",
|
|
447
|
+
outputSchema: z.string(),
|
|
448
|
+
handler: async (_ctx, { json }) => json("test data"),
|
|
449
|
+
}),
|
|
450
|
+
defineRoute({
|
|
451
|
+
method: "POST",
|
|
452
|
+
path: "/action",
|
|
453
|
+
inputSchema: z.object({ value: z.string() }),
|
|
454
|
+
outputSchema: z.object({ result: z.string() }),
|
|
455
|
+
handler: async (_ctx, { json }) => json({ result: "test value" }),
|
|
456
|
+
}),
|
|
457
|
+
] as const;
|
|
458
|
+
|
|
459
|
+
const clientConfig: FragnoPublicClientConfig = {
|
|
460
|
+
baseUrl: "http://localhost:3000",
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
test("should transform a mixed object of hooks and mutators", async () => {
|
|
464
|
+
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
|
465
|
+
headers: new Headers(),
|
|
466
|
+
ok: true,
|
|
467
|
+
json: async () => "test data",
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
const client = createClientBuilder(testFragmentDefinition, clientConfig, testRoutes);
|
|
471
|
+
const clientObj = {
|
|
472
|
+
useData: client.createHook("/data"),
|
|
473
|
+
usePostAction: client.createMutator("POST", "/action"),
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
const { useData, usePostAction } = useFragno(clientObj);
|
|
477
|
+
|
|
478
|
+
const { result: renderedHook } = renderHook(() => usePostAction());
|
|
479
|
+
const { mutate: postAction } = renderedHook.current;
|
|
480
|
+
|
|
481
|
+
// Test the hook
|
|
482
|
+
const { result: hookResult } = renderHook(() => useData());
|
|
483
|
+
|
|
484
|
+
await waitFor(() => {
|
|
485
|
+
expect(hookResult.current.loading).toBe(false);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
expect(hookResult.current.data).toBe("test data");
|
|
489
|
+
|
|
490
|
+
// Test the mutator
|
|
491
|
+
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
|
492
|
+
headers: new Headers(),
|
|
493
|
+
ok: true,
|
|
494
|
+
json: async () => ({ result: "test value" }),
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
const mutatorResult = await postAction({
|
|
498
|
+
body: { value: "test value" },
|
|
499
|
+
});
|
|
500
|
+
expect(mutatorResult).toEqual({ result: "test value" });
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
test("should pass through non-hook values unchanged", () => {
|
|
504
|
+
const client = createClientBuilder(testFragmentDefinition, clientConfig, testRoutes);
|
|
505
|
+
const clientObj = {
|
|
506
|
+
useData: client.createHook("/data"),
|
|
507
|
+
someString: "hello world",
|
|
508
|
+
someNumber: 42,
|
|
509
|
+
someObject: { foo: "bar", nested: { value: true } },
|
|
510
|
+
someArray: [1, 2, 3],
|
|
511
|
+
someFunction: () => "test",
|
|
512
|
+
someNull: null,
|
|
513
|
+
someUndefined: undefined,
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
const result = useFragno(clientObj);
|
|
517
|
+
|
|
518
|
+
// Check that non-hook values are passed through unchanged
|
|
519
|
+
expect(result.someString).toBe("hello world");
|
|
520
|
+
expect(result.someNumber).toBe(42);
|
|
521
|
+
expect(result.someObject).toEqual({ foo: "bar", nested: { value: true } });
|
|
522
|
+
expect(result.someArray).toEqual([1, 2, 3]);
|
|
523
|
+
expect(result.someFunction()).toBe("test");
|
|
524
|
+
expect(result.someNull).toBeNull();
|
|
525
|
+
expect(result.someUndefined).toBeUndefined();
|
|
526
|
+
|
|
527
|
+
// Verify that the hook is still transformed
|
|
528
|
+
expect(typeof result.useData).toBe("function");
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
test("should preserve reference equality for non-hook objects", () => {
|
|
532
|
+
const client = createClientBuilder(testFragmentDefinition, clientConfig, testRoutes);
|
|
533
|
+
const sharedObject = { shared: true };
|
|
534
|
+
const sharedArray = [1, 2, 3];
|
|
535
|
+
const sharedFunction = () => "shared";
|
|
536
|
+
|
|
537
|
+
const clientObj = {
|
|
538
|
+
useData: client.createHook("/data"),
|
|
539
|
+
obj: sharedObject,
|
|
540
|
+
arr: sharedArray,
|
|
541
|
+
fn: sharedFunction,
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
const result = useFragno(clientObj);
|
|
545
|
+
|
|
546
|
+
// Check that references are preserved
|
|
547
|
+
expect(result.obj).toBe(sharedObject);
|
|
548
|
+
expect(result.arr).toBe(sharedArray);
|
|
549
|
+
expect(result.fn).toBe(sharedFunction);
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
test("should handle mixed object with hooks, mutators, and other values", () => {
|
|
553
|
+
const client = createClientBuilder(testFragmentDefinition, clientConfig, testRoutes);
|
|
554
|
+
const clientObj = {
|
|
555
|
+
// Hooks and mutators
|
|
556
|
+
useData: client.createHook("/data"),
|
|
557
|
+
usePostAction: client.createMutator("POST", "/action"),
|
|
558
|
+
// Regular values
|
|
559
|
+
config: { apiKey: "test-key", timeout: 5000 },
|
|
560
|
+
utils: {
|
|
561
|
+
formatDate: (date: Date) => date.toISOString(),
|
|
562
|
+
parseId: (id: string) => parseInt(id, 10),
|
|
563
|
+
},
|
|
564
|
+
constants: {
|
|
565
|
+
MAX_RETRIES: 3,
|
|
566
|
+
DEFAULT_PAGE_SIZE: 20,
|
|
567
|
+
},
|
|
568
|
+
metadata: {
|
|
569
|
+
version: "1.0.0",
|
|
570
|
+
environment: "test",
|
|
571
|
+
},
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
const result = useFragno(clientObj);
|
|
575
|
+
|
|
576
|
+
// Check hooks are transformed
|
|
577
|
+
expect(typeof result.useData).toBe("function");
|
|
578
|
+
expect(typeof result.usePostAction).toBe("function");
|
|
579
|
+
|
|
580
|
+
// Check other values are passed through
|
|
581
|
+
expect(result.config).toEqual({ apiKey: "test-key", timeout: 5000 });
|
|
582
|
+
expect(result.utils.formatDate(new Date("2024-01-01"))).toBe("2024-01-01T00:00:00.000Z");
|
|
583
|
+
expect(result.utils.parseId("123")).toBe(123);
|
|
584
|
+
expect(result.constants.MAX_RETRIES).toBe(3);
|
|
585
|
+
expect(result.constants.DEFAULT_PAGE_SIZE).toBe(20);
|
|
586
|
+
expect(result.metadata).toEqual({ version: "1.0.0", environment: "test" });
|
|
587
|
+
|
|
588
|
+
expectTypeOf<(typeof result)["config"]>().toEqualTypeOf<{ apiKey: string; timeout: number }>();
|
|
589
|
+
expectTypeOf<(typeof result)["utils"]>().toEqualTypeOf<{
|
|
590
|
+
formatDate: (date: Date) => string;
|
|
591
|
+
parseId: (id: string) => number;
|
|
592
|
+
}>();
|
|
593
|
+
expectTypeOf<(typeof result)["constants"]>().toEqualTypeOf<{
|
|
594
|
+
MAX_RETRIES: number;
|
|
595
|
+
DEFAULT_PAGE_SIZE: number;
|
|
596
|
+
}>();
|
|
597
|
+
expectTypeOf<(typeof result)["metadata"]>().toEqualTypeOf<{
|
|
598
|
+
version: string;
|
|
599
|
+
environment: string;
|
|
600
|
+
}>();
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
test("should handle empty object", () => {
|
|
604
|
+
const clientObj = {};
|
|
605
|
+
const result = useFragno(clientObj);
|
|
606
|
+
expect(result).toEqual({});
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
test("should handle object with only non-hook values", () => {
|
|
610
|
+
const clientObj = {
|
|
611
|
+
a: 1,
|
|
612
|
+
b: "two",
|
|
613
|
+
c: { three: 3 },
|
|
614
|
+
d: [4, 5, 6],
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
const result = useFragno(clientObj);
|
|
618
|
+
|
|
619
|
+
expect(result).toEqual(clientObj);
|
|
620
|
+
expect(result.a).toBe(1);
|
|
621
|
+
expect(result.b).toBe("two");
|
|
622
|
+
expect(result.c).toEqual({ three: 3 });
|
|
623
|
+
expect(result.d).toEqual([4, 5, 6]);
|
|
624
|
+
});
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
describe("useStore", () => {
|
|
628
|
+
test("should subscribe to store changes", async () => {
|
|
629
|
+
const store = atom({ count: 0 });
|
|
630
|
+
|
|
631
|
+
const { result } = renderHook(() => useStore(store));
|
|
632
|
+
|
|
633
|
+
expect(result.current).toEqual({ count: 0 });
|
|
634
|
+
|
|
635
|
+
act(() => {
|
|
636
|
+
store.set({ count: 1 });
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
expect(result.current).toEqual({ count: 1 });
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
test("should handle computed stores", async () => {
|
|
643
|
+
const baseStore = atom(5);
|
|
644
|
+
const doubledStore = computed(baseStore, (value) => value * 2);
|
|
645
|
+
|
|
646
|
+
const { result } = renderHook(() => useStore(doubledStore));
|
|
647
|
+
|
|
648
|
+
expect(result.current).toBe(10);
|
|
649
|
+
|
|
650
|
+
act(() => {
|
|
651
|
+
baseStore.set(7);
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
expect(result.current).toBe(14);
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
test("should unsubscribe on unmount", () => {
|
|
658
|
+
const store = atom({ value: 0 });
|
|
659
|
+
const unsubscribeSpy = vi.fn();
|
|
660
|
+
|
|
661
|
+
// Mock the store.listen method to track unsubscribe
|
|
662
|
+
const originalListen = store.listen;
|
|
663
|
+
store.listen = vi.fn((callback) => {
|
|
664
|
+
const unsubscribe = originalListen.call(store, callback);
|
|
665
|
+
return () => {
|
|
666
|
+
unsubscribeSpy();
|
|
667
|
+
unsubscribe();
|
|
668
|
+
};
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
const { unmount } = renderHook(() => useStore(store));
|
|
672
|
+
|
|
673
|
+
expect(store.listen).toHaveBeenCalled();
|
|
674
|
+
|
|
675
|
+
unmount();
|
|
676
|
+
|
|
677
|
+
expect(unsubscribeSpy).toHaveBeenCalled();
|
|
678
|
+
});
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
describe("useFragno - createStore", () => {
|
|
682
|
+
const clientConfig: FragnoPublicClientConfig = {
|
|
683
|
+
baseUrl: "http://localhost:3000",
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
beforeEach(() => {
|
|
687
|
+
vi.clearAllMocks();
|
|
688
|
+
(global.fetch as ReturnType<typeof vi.fn>).mockReset();
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
afterEach(() => {
|
|
692
|
+
vi.restoreAllMocks();
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
test("FragnoReactStore type test - ReadableAtom fields", () => {
|
|
696
|
+
// Test that ReadableAtom fields are properly unwrapped to their value types
|
|
697
|
+
const stringAtom: ReadableAtom<string> = atom("hello");
|
|
698
|
+
const numberAtom: ReadableAtom<number> = atom(42);
|
|
699
|
+
const booleanAtom: ReadableAtom<boolean> = atom(true);
|
|
700
|
+
const objectAtom: ReadableAtom<{ count: number }> = atom({ count: 0 });
|
|
701
|
+
const arrayAtom: ReadableAtom<string[]> = atom(["a", "b", "c"]);
|
|
702
|
+
|
|
703
|
+
const cb = createClientBuilder(defineFragment("test-fragment"), clientConfig, []);
|
|
704
|
+
const client = {
|
|
705
|
+
useStore: cb.createStore({
|
|
706
|
+
message: stringAtom,
|
|
707
|
+
count: numberAtom,
|
|
708
|
+
isActive: booleanAtom,
|
|
709
|
+
data: objectAtom,
|
|
710
|
+
items: arrayAtom,
|
|
711
|
+
}),
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
const { useStore } = useFragno(client);
|
|
715
|
+
|
|
716
|
+
// Type assertions to ensure the types are correctly inferred
|
|
717
|
+
expectTypeOf(useStore).toExtend<
|
|
718
|
+
() => {
|
|
719
|
+
message: string;
|
|
720
|
+
count: number;
|
|
721
|
+
isActive: boolean;
|
|
722
|
+
data: { count: number };
|
|
723
|
+
items: string[];
|
|
724
|
+
}
|
|
725
|
+
>();
|
|
726
|
+
|
|
727
|
+
// Runtime test
|
|
728
|
+
const { result } = renderHook(() => useStore());
|
|
729
|
+
expect(result.current.message).toBe("hello");
|
|
730
|
+
expect(result.current.count).toBe(42);
|
|
731
|
+
expect(result.current.isActive).toBe(true);
|
|
732
|
+
expect(result.current.data).toEqual({ count: 0 });
|
|
733
|
+
expect(result.current.items).toEqual(["a", "b", "c"]);
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
test("FragnoReactStore type test - computed stores", () => {
|
|
737
|
+
// Test that computed stores (which are also ReadableAtom) are properly unwrapped
|
|
738
|
+
const baseNumber = atom(10);
|
|
739
|
+
const doubled = computed(baseNumber, (n) => n * 2);
|
|
740
|
+
const tripled = computed(baseNumber, (n) => n * 3);
|
|
741
|
+
const combined = computed([doubled, tripled], (d, t) => ({ doubled: d, tripled: t }));
|
|
742
|
+
|
|
743
|
+
const cb = createClientBuilder(defineFragment("test-fragment"), clientConfig, []);
|
|
744
|
+
const client = {
|
|
745
|
+
useComputedValues: cb.createStore({
|
|
746
|
+
base: baseNumber,
|
|
747
|
+
doubled: doubled,
|
|
748
|
+
tripled: tripled,
|
|
749
|
+
combined: combined,
|
|
750
|
+
}),
|
|
751
|
+
};
|
|
752
|
+
|
|
753
|
+
const { useComputedValues } = useFragno(client);
|
|
754
|
+
|
|
755
|
+
// Type assertions
|
|
756
|
+
expectTypeOf(useComputedValues).toExtend<
|
|
757
|
+
() => {
|
|
758
|
+
base: number;
|
|
759
|
+
doubled: number;
|
|
760
|
+
tripled: number;
|
|
761
|
+
combined: { doubled: number; tripled: number };
|
|
762
|
+
}
|
|
763
|
+
>();
|
|
764
|
+
|
|
765
|
+
// Runtime test
|
|
766
|
+
const { result } = renderHook(() => useComputedValues());
|
|
767
|
+
expect(result.current.base).toBe(10);
|
|
768
|
+
expect(result.current.doubled).toBe(20);
|
|
769
|
+
expect(result.current.tripled).toBe(30);
|
|
770
|
+
expect(result.current.combined).toEqual({ doubled: 20, tripled: 30 });
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
test("FragnoReactStore type test - mixed store and non-store fields", () => {
|
|
774
|
+
// Test that non-store fields are passed through unchanged
|
|
775
|
+
const messageAtom: ReadableAtom<string> = atom("test");
|
|
776
|
+
const regularFunction = (x: number) => x * 2;
|
|
777
|
+
const regularObject = { foo: "bar", baz: 123 };
|
|
778
|
+
|
|
779
|
+
const cb = createClientBuilder(defineFragment("test-fragment"), clientConfig, []);
|
|
780
|
+
const client = {
|
|
781
|
+
useMixed: cb.createStore({
|
|
782
|
+
message: messageAtom,
|
|
783
|
+
multiply: regularFunction,
|
|
784
|
+
config: regularObject,
|
|
785
|
+
constant: 42,
|
|
786
|
+
}),
|
|
787
|
+
};
|
|
788
|
+
|
|
789
|
+
const { useMixed } = useFragno(client);
|
|
790
|
+
|
|
791
|
+
// Type assertions
|
|
792
|
+
expectTypeOf(useMixed).toExtend<
|
|
793
|
+
() => {
|
|
794
|
+
message: string;
|
|
795
|
+
multiply: (x: number) => number;
|
|
796
|
+
config: { foo: string; baz: number };
|
|
797
|
+
constant: number;
|
|
798
|
+
}
|
|
799
|
+
>();
|
|
800
|
+
|
|
801
|
+
// Runtime test
|
|
802
|
+
const { result } = renderHook(() => useMixed());
|
|
803
|
+
expect(result.current.message).toBe("test");
|
|
804
|
+
expect(result.current.multiply(5)).toBe(10);
|
|
805
|
+
expect(result.current.config).toEqual({ foo: "bar", baz: 123 });
|
|
806
|
+
expect(result.current.constant).toBe(42);
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
test("FragnoReactStore type test - single store vs object with stores", () => {
|
|
810
|
+
// Test that a single store is unwrapped directly
|
|
811
|
+
const singleAtom: ReadableAtom<string> = atom("single");
|
|
812
|
+
const cb = createClientBuilder(defineFragment("test-fragment"), clientConfig, []);
|
|
813
|
+
|
|
814
|
+
// Single store case
|
|
815
|
+
const clientSingle = {
|
|
816
|
+
useSingle: cb.createStore(singleAtom),
|
|
817
|
+
};
|
|
818
|
+
const { useSingle } = useFragno(clientSingle);
|
|
819
|
+
expectTypeOf(useSingle).toExtend<() => string>();
|
|
820
|
+
|
|
821
|
+
// Object with stores case
|
|
822
|
+
const clientObject = {
|
|
823
|
+
useObject: cb.createStore({
|
|
824
|
+
value: singleAtom,
|
|
825
|
+
}),
|
|
826
|
+
};
|
|
827
|
+
const { useObject } = useFragno(clientObject);
|
|
828
|
+
expectTypeOf(useObject).toExtend<() => { value: string }>();
|
|
829
|
+
|
|
830
|
+
// Runtime test
|
|
831
|
+
const { result: singleResult } = renderHook(() => useSingle());
|
|
832
|
+
expect(singleResult.current).toBe("single");
|
|
833
|
+
|
|
834
|
+
const { result: objectResult } = renderHook(() => useObject());
|
|
835
|
+
expect(objectResult.current).toEqual({ value: "single" });
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
test("FragnoReactStore type test - complex nested atoms", () => {
|
|
839
|
+
// Test complex nested structures with atoms
|
|
840
|
+
type User = { id: number; name: string; email: string };
|
|
841
|
+
type Settings = { theme: "light" | "dark"; notifications: boolean };
|
|
842
|
+
|
|
843
|
+
const userAtom: ReadableAtom<User> = atom({ id: 1, name: "John", email: "john@example.com" });
|
|
844
|
+
const settingsAtom: ReadableAtom<Settings> = atom({ theme: "light", notifications: true });
|
|
845
|
+
const loadingAtom: ReadableAtom<boolean> = atom(false);
|
|
846
|
+
const errorAtom: ReadableAtom<string | null> = atom(null);
|
|
847
|
+
|
|
848
|
+
const cb = createClientBuilder(defineFragment("test-fragment"), clientConfig, []);
|
|
849
|
+
const client = {
|
|
850
|
+
useAppState: cb.createStore({
|
|
851
|
+
user: userAtom,
|
|
852
|
+
settings: settingsAtom,
|
|
853
|
+
loading: loadingAtom,
|
|
854
|
+
error: errorAtom,
|
|
855
|
+
}),
|
|
856
|
+
};
|
|
857
|
+
|
|
858
|
+
const { useAppState } = useFragno(client);
|
|
859
|
+
|
|
860
|
+
// Type assertions for complex nested structure
|
|
861
|
+
expectTypeOf(useAppState).toExtend<
|
|
862
|
+
() => {
|
|
863
|
+
user: User;
|
|
864
|
+
settings: Settings;
|
|
865
|
+
loading: boolean;
|
|
866
|
+
error: string | null;
|
|
867
|
+
}
|
|
868
|
+
>();
|
|
869
|
+
|
|
870
|
+
// Runtime test
|
|
871
|
+
const { result } = renderHook(() => useAppState());
|
|
872
|
+
expect(result.current.user).toEqual({ id: 1, name: "John", email: "john@example.com" });
|
|
873
|
+
expect(result.current.settings).toEqual({ theme: "light", notifications: true });
|
|
874
|
+
expect(result.current.loading).toBe(false);
|
|
875
|
+
expect(result.current.error).toBeNull();
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
test("Derived from streaming route", async () => {
|
|
879
|
+
const streamFragmentDefinition = defineFragment("stream-fragment");
|
|
880
|
+
const streamRoutes = [
|
|
881
|
+
defineRoute({
|
|
882
|
+
method: "GET",
|
|
883
|
+
path: "/users-stream",
|
|
884
|
+
outputSchema: z.array(z.object({ id: z.number(), name: z.string() })),
|
|
885
|
+
handler: async () => {
|
|
886
|
+
throw new Error("Not implemented");
|
|
887
|
+
},
|
|
888
|
+
}),
|
|
889
|
+
] as const;
|
|
890
|
+
const cb = createClientBuilder(streamFragmentDefinition, clientConfig, streamRoutes);
|
|
891
|
+
const usersStream = cb.createHook("/users-stream");
|
|
892
|
+
|
|
893
|
+
// Create a single shared store instance
|
|
894
|
+
const sharedStore = usersStream.store({});
|
|
895
|
+
|
|
896
|
+
const names = computed(sharedStore, ({ data }) => {
|
|
897
|
+
return (data ?? []).map((user) => user.name).join(", ");
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
const client = {
|
|
901
|
+
useUsersStream: cb.createStore(sharedStore),
|
|
902
|
+
useNames: cb.createStore(names),
|
|
903
|
+
};
|
|
904
|
+
|
|
905
|
+
vi.mocked(global.fetch).mockImplementation(async () => {
|
|
906
|
+
const ctx = new RequestOutputContext(streamRoutes[0].outputSchema);
|
|
907
|
+
return ctx.jsonStream(async (stream) => {
|
|
908
|
+
await stream.write({ id: 1, name: "John" });
|
|
909
|
+
await stream.sleep(0);
|
|
910
|
+
await stream.write({ id: 2, name: "Jane" });
|
|
911
|
+
await stream.sleep(0);
|
|
912
|
+
await stream.write({ id: 3, name: "Jim" });
|
|
913
|
+
});
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
const { useNames, useUsersStream } = useFragno(client);
|
|
917
|
+
|
|
918
|
+
expectTypeOf(useUsersStream).toEqualTypeOf<
|
|
919
|
+
FragnoReactStore<
|
|
920
|
+
FetcherStore<
|
|
921
|
+
{
|
|
922
|
+
id: number;
|
|
923
|
+
name: string;
|
|
924
|
+
}[],
|
|
925
|
+
FragnoClientError<string>
|
|
926
|
+
>
|
|
927
|
+
>
|
|
928
|
+
>();
|
|
929
|
+
|
|
930
|
+
expectTypeOf(useNames).toEqualTypeOf<FragnoReactStore<ReadableAtom<string>>>();
|
|
931
|
+
|
|
932
|
+
const { result } = renderHook(() => ({
|
|
933
|
+
usersStream: useUsersStream(),
|
|
934
|
+
names: useNames(),
|
|
935
|
+
}));
|
|
936
|
+
|
|
937
|
+
await waitFor(() => {
|
|
938
|
+
expect(result.current.names).toEqual("John, Jane, Jim");
|
|
939
|
+
expect(result.current.usersStream.loading).toBe(false);
|
|
940
|
+
expect(result.current.usersStream.data).toEqual([
|
|
941
|
+
{ id: 1, name: "John" },
|
|
942
|
+
{ id: 2, name: "Jane" },
|
|
943
|
+
{ id: 3, name: "Jim" },
|
|
944
|
+
]);
|
|
945
|
+
});
|
|
946
|
+
});
|
|
947
|
+
});
|