jazz-tools 0.19.8 → 0.19.11
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 +56 -50
- package/CHANGELOG.md +30 -3
- package/dist/{chunk-2S3Z2CN6.js → chunk-HX5S6W5E.js} +372 -103
- package/dist/chunk-HX5S6W5E.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/inspector/account-switcher.d.ts +4 -0
- package/dist/inspector/account-switcher.d.ts.map +1 -0
- package/dist/inspector/chunk-C6BJPHBQ.js +4096 -0
- package/dist/inspector/chunk-C6BJPHBQ.js.map +1 -0
- package/dist/inspector/contexts/node.d.ts +19 -0
- package/dist/inspector/contexts/node.d.ts.map +1 -0
- package/dist/inspector/{custom-element-P76EIWEV.js → custom-element-GJVBPZES.js} +1011 -884
- package/dist/inspector/custom-element-GJVBPZES.js.map +1 -0
- package/dist/inspector/{viewer/new-app.d.ts → in-app.d.ts} +3 -3
- package/dist/inspector/in-app.d.ts.map +1 -0
- package/dist/inspector/index.d.ts +0 -11
- package/dist/inspector/index.d.ts.map +1 -1
- package/dist/inspector/index.js +56 -3910
- package/dist/inspector/index.js.map +1 -1
- package/dist/inspector/pages/home.d.ts +2 -0
- package/dist/inspector/pages/home.d.ts.map +1 -0
- package/dist/inspector/register-custom-element.js +1 -1
- package/dist/inspector/router/context.d.ts +12 -0
- package/dist/inspector/router/context.d.ts.map +1 -0
- package/dist/inspector/router/hash-router.d.ts +7 -0
- package/dist/inspector/router/hash-router.d.ts.map +1 -0
- package/dist/inspector/router/in-memory-router.d.ts +7 -0
- package/dist/inspector/router/in-memory-router.d.ts.map +1 -0
- package/dist/inspector/router/index.d.ts +5 -0
- package/dist/inspector/router/index.d.ts.map +1 -0
- package/dist/inspector/standalone.d.ts +6 -0
- package/dist/inspector/standalone.d.ts.map +1 -0
- package/dist/inspector/standalone.js +420 -0
- package/dist/inspector/standalone.js.map +1 -0
- package/dist/inspector/tests/router/hash-router.test.d.ts +2 -0
- package/dist/inspector/tests/router/hash-router.test.d.ts.map +1 -0
- package/dist/inspector/tests/router/in-memory-router.test.d.ts +2 -0
- package/dist/inspector/tests/router/in-memory-router.test.d.ts.map +1 -0
- package/dist/inspector/ui/modal.d.ts +1 -0
- package/dist/inspector/ui/modal.d.ts.map +1 -1
- package/dist/inspector/viewer/breadcrumbs.d.ts +1 -7
- package/dist/inspector/viewer/breadcrumbs.d.ts.map +1 -1
- package/dist/inspector/viewer/header.d.ts +7 -0
- package/dist/inspector/viewer/header.d.ts.map +1 -0
- package/dist/inspector/viewer/page-stack.d.ts +4 -13
- package/dist/inspector/viewer/page-stack.d.ts.map +1 -1
- package/dist/inspector/viewer/page.d.ts.map +1 -1
- package/dist/react/hooks.d.ts +1 -1
- package/dist/react/hooks.d.ts.map +1 -1
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +5 -1
- package/dist/react/index.js.map +1 -1
- package/dist/react-core/hooks.d.ts +59 -0
- package/dist/react-core/hooks.d.ts.map +1 -1
- package/dist/react-core/index.js +124 -36
- package/dist/react-core/index.js.map +1 -1
- package/dist/react-core/tests/testUtils.d.ts +1 -0
- package/dist/react-core/tests/testUtils.d.ts.map +1 -1
- package/dist/react-core/tests/useSuspenseAccount.test.d.ts +2 -0
- package/dist/react-core/tests/useSuspenseAccount.test.d.ts.map +1 -0
- package/dist/react-core/tests/useSuspenseCoState.test.d.ts +2 -0
- package/dist/react-core/tests/useSuspenseCoState.test.d.ts.map +1 -0
- package/dist/react-core/use.d.ts +3 -0
- package/dist/react-core/use.d.ts.map +1 -0
- package/dist/react-native/index.js +5 -1
- package/dist/react-native/index.js.map +1 -1
- package/dist/react-native-core/crypto/RNCrypto.d.ts +2 -0
- package/dist/react-native-core/crypto/RNCrypto.d.ts.map +1 -0
- package/dist/react-native-core/crypto/RNCrypto.js +3 -0
- package/dist/react-native-core/crypto/RNCrypto.js.map +1 -0
- package/dist/react-native-core/hooks.d.ts +1 -1
- package/dist/react-native-core/hooks.d.ts.map +1 -1
- package/dist/react-native-core/index.js +5 -1
- package/dist/react-native-core/index.js.map +1 -1
- package/dist/react-native-core/platform.d.ts +2 -1
- package/dist/react-native-core/platform.d.ts.map +1 -1
- package/dist/testing.js +1 -1
- package/dist/testing.js.map +1 -1
- package/dist/tools/coValues/account.d.ts +7 -1
- package/dist/tools/coValues/account.d.ts.map +1 -1
- package/dist/tools/coValues/interfaces.d.ts +1 -1
- package/dist/tools/coValues/interfaces.d.ts.map +1 -1
- package/dist/tools/implementation/ContextManager.d.ts +3 -0
- package/dist/tools/implementation/ContextManager.d.ts.map +1 -1
- package/dist/tools/implementation/zodSchema/schemaTypes/AccountSchema.d.ts +8 -1
- package/dist/tools/implementation/zodSchema/schemaTypes/AccountSchema.d.ts.map +1 -1
- package/dist/tools/implementation/zodSchema/zodCo.d.ts.map +1 -1
- package/dist/tools/subscribe/CoValueCoreSubscription.d.ts +8 -22
- package/dist/tools/subscribe/CoValueCoreSubscription.d.ts.map +1 -1
- package/dist/tools/subscribe/SubscriptionCache.d.ts +51 -0
- package/dist/tools/subscribe/SubscriptionCache.d.ts.map +1 -0
- package/dist/tools/subscribe/SubscriptionScope.d.ts +17 -1
- package/dist/tools/subscribe/SubscriptionScope.d.ts.map +1 -1
- package/dist/tools/subscribe/utils.d.ts +9 -1
- package/dist/tools/subscribe/utils.d.ts.map +1 -1
- package/dist/tools/testing.d.ts +2 -2
- package/dist/tools/testing.d.ts.map +1 -1
- package/dist/tools/tests/SubscriptionCache.test.d.ts +2 -0
- package/dist/tools/tests/SubscriptionCache.test.d.ts.map +1 -0
- package/package.json +18 -6
- package/src/inspector/account-switcher.tsx +440 -0
- package/src/inspector/contexts/node.tsx +129 -0
- package/src/inspector/custom-element.tsx +2 -2
- package/src/inspector/in-app.tsx +61 -0
- package/src/inspector/index.tsx +2 -22
- package/src/inspector/pages/home.tsx +77 -0
- package/src/inspector/router/context.ts +21 -0
- package/src/inspector/router/hash-router.tsx +128 -0
- package/src/inspector/{viewer/use-page-path.ts → router/in-memory-router.tsx} +31 -29
- package/src/inspector/router/index.ts +4 -0
- package/src/inspector/standalone.tsx +60 -0
- package/src/inspector/tests/router/hash-router.test.tsx +847 -0
- package/src/inspector/tests/router/in-memory-router.test.tsx +724 -0
- package/src/inspector/ui/modal.tsx +5 -2
- package/src/inspector/viewer/breadcrumbs.tsx +5 -11
- package/src/inspector/viewer/header.tsx +67 -0
- package/src/inspector/viewer/page-stack.tsx +18 -26
- package/src/inspector/viewer/page.tsx +0 -1
- package/src/react/hooks.tsx +2 -0
- package/src/react/index.ts +1 -14
- package/src/react-core/hooks.ts +167 -18
- package/src/react-core/tests/createCoValueSubscriptionContext.test.tsx +18 -8
- package/src/react-core/tests/testUtils.tsx +67 -5
- package/src/react-core/tests/useCoState.test.ts +3 -7
- package/src/react-core/tests/useSubscriptionSelector.test.ts +3 -7
- package/src/react-core/tests/useSuspenseAccount.test.tsx +343 -0
- package/src/react-core/tests/useSuspenseCoState.test.tsx +1182 -0
- package/src/react-core/use.ts +46 -0
- package/src/react-native-core/crypto/RNCrypto.ts +1 -0
- package/src/react-native-core/hooks.tsx +2 -0
- package/src/react-native-core/platform.ts +2 -1
- package/src/tools/coValues/account.ts +13 -2
- package/src/tools/coValues/interfaces.ts +2 -3
- package/src/tools/implementation/ContextManager.ts +13 -0
- package/src/tools/implementation/zodSchema/schemaTypes/AccountSchema.ts +8 -1
- package/src/tools/subscribe/CoValueCoreSubscription.ts +71 -100
- package/src/tools/subscribe/SubscriptionCache.ts +272 -0
- package/src/tools/subscribe/SubscriptionScope.ts +113 -7
- package/src/tools/subscribe/utils.ts +77 -0
- package/src/tools/testing.ts +0 -3
- package/src/tools/tests/CoValueCoreSubscription.test.ts +46 -12
- package/src/tools/tests/ContextManager.test.ts +85 -0
- package/src/tools/tests/SubscriptionCache.test.ts +237 -0
- package/src/tools/tests/account.test.ts +11 -4
- package/src/tools/tests/coMap.test.ts +5 -7
- package/src/tools/tests/schema.resolved.test.ts +3 -3
- package/tsup.config.ts +2 -0
- package/dist/chunk-2S3Z2CN6.js.map +0 -1
- package/dist/inspector/custom-element-P76EIWEV.js.map +0 -1
- package/dist/inspector/viewer/new-app.d.ts.map +0 -1
- package/dist/inspector/viewer/use-page-path.d.ts +0 -10
- package/dist/inspector/viewer/use-page-path.d.ts.map +0 -1
- package/src/inspector/viewer/new-app.tsx +0 -156
|
@@ -0,0 +1,1182 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import { cojsonInternals } from "cojson";
|
|
4
|
+
import { Group, Loaded, co, z } from "jazz-tools";
|
|
5
|
+
import { assertLoaded, disableJazzTestSync } from "jazz-tools/testing";
|
|
6
|
+
import { beforeEach, describe, expect, expectTypeOf, it } from "vitest";
|
|
7
|
+
import React, { Suspense, useRef } from "react";
|
|
8
|
+
import { useSuspenseCoState } from "../hooks.js";
|
|
9
|
+
import { createJazzTestAccount, setupJazzTestSync } from "../testing.js";
|
|
10
|
+
import {
|
|
11
|
+
act,
|
|
12
|
+
createAsyncStorage,
|
|
13
|
+
render,
|
|
14
|
+
renderHook,
|
|
15
|
+
waitFor,
|
|
16
|
+
} from "./testUtils.js";
|
|
17
|
+
import { ErrorBoundary } from "react-error-boundary";
|
|
18
|
+
|
|
19
|
+
// Hook to track render count
|
|
20
|
+
const useRenderCount = <T,>(hook: () => T) => {
|
|
21
|
+
const renderCountRef = useRef(0);
|
|
22
|
+
const result = hook();
|
|
23
|
+
renderCountRef.current = renderCountRef.current + 1;
|
|
24
|
+
return {
|
|
25
|
+
renderCount: renderCountRef.current,
|
|
26
|
+
result,
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Silence unhandled rejection errors coming from Suspense
|
|
31
|
+
process.on("unhandledRejection", () => {});
|
|
32
|
+
|
|
33
|
+
beforeEach(async () => {
|
|
34
|
+
cojsonInternals.setCoValueLoadingRetryDelay(20);
|
|
35
|
+
|
|
36
|
+
await setupJazzTestSync({
|
|
37
|
+
asyncPeers: true,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
await createJazzTestAccount({
|
|
41
|
+
isCurrentActiveAccount: true,
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("useSuspenseCoState", () => {
|
|
46
|
+
it("should return loaded value without suspending when data is available", async () => {
|
|
47
|
+
const TestMap = co.map({
|
|
48
|
+
value: z.string(),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const account = await createJazzTestAccount({
|
|
52
|
+
isCurrentActiveAccount: true,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const map = TestMap.create({
|
|
56
|
+
value: "123",
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
let suspenseTriggered = false;
|
|
60
|
+
|
|
61
|
+
const SuspenseFallback = () => {
|
|
62
|
+
suspenseTriggered = true;
|
|
63
|
+
return <div>Loading...</div>;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
67
|
+
<Suspense fallback={<SuspenseFallback />}>{children}</Suspense>
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const { result } = renderHook(
|
|
71
|
+
() => useSuspenseCoState(TestMap, map.$jazz.id),
|
|
72
|
+
{
|
|
73
|
+
account,
|
|
74
|
+
wrapper,
|
|
75
|
+
},
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// Wait for any async operations to complete
|
|
79
|
+
await waitFor(() => {
|
|
80
|
+
expect(result.current).toBeDefined();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Verify Suspense was not triggered since data was immediately available
|
|
84
|
+
expect(suspenseTriggered).toBe(false);
|
|
85
|
+
|
|
86
|
+
// Verify the hook returns loaded data
|
|
87
|
+
assertLoaded(result.current);
|
|
88
|
+
expect(result.current.value).toBe("123");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("should have Loaded<T> return type", async () => {
|
|
92
|
+
const TestMap = co.map({
|
|
93
|
+
value: z.string(),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const account = await createJazzTestAccount({
|
|
97
|
+
isCurrentActiveAccount: true,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const map = TestMap.create({
|
|
101
|
+
value: "123",
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
105
|
+
<Suspense fallback={<div>Loading...</div>}>{children}</Suspense>
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const { result } = renderHook(
|
|
109
|
+
() => useSuspenseCoState(TestMap, map.$jazz.id),
|
|
110
|
+
{
|
|
111
|
+
account,
|
|
112
|
+
wrapper,
|
|
113
|
+
},
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
await waitFor(() => {
|
|
117
|
+
expect(result.current).toBeDefined();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Verify the return type is Loaded<typeof TestMap>
|
|
121
|
+
expectTypeOf(result.current).toEqualTypeOf<Loaded<typeof TestMap>>();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("should suspend when data is not immediately available", async () => {
|
|
125
|
+
const TestMap = co.map({
|
|
126
|
+
value: z.string(),
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const map = TestMap.create(
|
|
130
|
+
{
|
|
131
|
+
value: "123",
|
|
132
|
+
},
|
|
133
|
+
Group.create().makePublic("reader"),
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const viewerAccount = await createJazzTestAccount({
|
|
137
|
+
isCurrentActiveAccount: true,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
let suspenseTriggered = false;
|
|
141
|
+
|
|
142
|
+
const SuspenseFallback = () => {
|
|
143
|
+
suspenseTriggered = true;
|
|
144
|
+
return <div>Loading...</div>;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const TestComponent = () => {
|
|
148
|
+
const value = useSuspenseCoState(TestMap, map.$jazz.id);
|
|
149
|
+
return <div>{value.value}</div>;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const { container } = await act(async () => {
|
|
153
|
+
return render(
|
|
154
|
+
<Suspense fallback={<SuspenseFallback />}>
|
|
155
|
+
<TestComponent />
|
|
156
|
+
</Suspense>,
|
|
157
|
+
{
|
|
158
|
+
account: viewerAccount,
|
|
159
|
+
},
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
expect(suspenseTriggered).toBe(true);
|
|
164
|
+
|
|
165
|
+
// Wait for data to load - the subscription should update and resolve
|
|
166
|
+
await waitFor(() => {
|
|
167
|
+
expect(container.textContent).toContain("123");
|
|
168
|
+
expect(container.textContent).not.toContain("Loading...");
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("should throw error when CoValue is unavailable", async () => {
|
|
173
|
+
const TestMap = co.map({
|
|
174
|
+
value: z.string(),
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const map = TestMap.create(
|
|
178
|
+
{
|
|
179
|
+
value: "123",
|
|
180
|
+
},
|
|
181
|
+
Group.create().makePublic("reader"),
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
await setupJazzTestSync();
|
|
185
|
+
|
|
186
|
+
const viewerAccount = await createJazzTestAccount({
|
|
187
|
+
isCurrentActiveAccount: true,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const TestComponent = () => {
|
|
191
|
+
const value = useSuspenseCoState(TestMap, map.$jazz.id);
|
|
192
|
+
return <div>{value.value}</div>;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const { container } = await act(async () => {
|
|
196
|
+
return render(
|
|
197
|
+
<ErrorBoundary fallback={<div>Error!</div>}>
|
|
198
|
+
<Suspense fallback={<div>Loading...</div>}>
|
|
199
|
+
<TestComponent />
|
|
200
|
+
</Suspense>
|
|
201
|
+
</ErrorBoundary>,
|
|
202
|
+
{
|
|
203
|
+
account: viewerAccount,
|
|
204
|
+
},
|
|
205
|
+
);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// Verify error is displayed in error boundary
|
|
209
|
+
await waitFor(
|
|
210
|
+
() => {
|
|
211
|
+
expect(container.textContent).toContain("Error");
|
|
212
|
+
},
|
|
213
|
+
{ timeout: 10_000 },
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("should throw error when CoValue is unavailable due disabled network", async () => {
|
|
218
|
+
disableJazzTestSync();
|
|
219
|
+
|
|
220
|
+
const TestMap = co.map({
|
|
221
|
+
value: z.string(),
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const map = TestMap.create(
|
|
225
|
+
{
|
|
226
|
+
value: "123",
|
|
227
|
+
},
|
|
228
|
+
Group.create().makePublic("reader"),
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
const viewerAccount = await createJazzTestAccount({
|
|
232
|
+
isCurrentActiveAccount: true,
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
viewerAccount.$jazz.localNode.setStorage(await createAsyncStorage());
|
|
236
|
+
|
|
237
|
+
const TestComponent = () => {
|
|
238
|
+
const value = useSuspenseCoState(TestMap, map.$jazz.id);
|
|
239
|
+
return <div>{value.value}</div>;
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const { container } = await act(async () => {
|
|
243
|
+
return render(
|
|
244
|
+
<ErrorBoundary fallback={<div>Error!</div>}>
|
|
245
|
+
<Suspense fallback={<div>Loading...</div>}>
|
|
246
|
+
<TestComponent />
|
|
247
|
+
</Suspense>
|
|
248
|
+
</ErrorBoundary>,
|
|
249
|
+
{
|
|
250
|
+
account: viewerAccount,
|
|
251
|
+
},
|
|
252
|
+
);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// Verify error is displayed in error boundary
|
|
256
|
+
await waitFor(
|
|
257
|
+
() => {
|
|
258
|
+
expect(container.textContent).toContain("Error!");
|
|
259
|
+
},
|
|
260
|
+
{ timeout: 10_000 },
|
|
261
|
+
);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("should throw error when CoValue is unavailable due to missing loading sources", async () => {
|
|
265
|
+
disableJazzTestSync();
|
|
266
|
+
|
|
267
|
+
const TestMap = co.map({
|
|
268
|
+
value: z.string(),
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const map = TestMap.create(
|
|
272
|
+
{
|
|
273
|
+
value: "123",
|
|
274
|
+
},
|
|
275
|
+
Group.create().makePublic("reader"),
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
const viewerAccount = await createJazzTestAccount({
|
|
279
|
+
isCurrentActiveAccount: true,
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
const TestComponent = () => {
|
|
283
|
+
const value = useSuspenseCoState(TestMap, map.$jazz.id);
|
|
284
|
+
return <div>{value.value}</div>;
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const { container } = await act(async () => {
|
|
288
|
+
return render(
|
|
289
|
+
<ErrorBoundary fallback={<div>Error!</div>}>
|
|
290
|
+
<Suspense fallback={<div>Loading...</div>}>
|
|
291
|
+
<TestComponent />
|
|
292
|
+
</Suspense>
|
|
293
|
+
</ErrorBoundary>,
|
|
294
|
+
{
|
|
295
|
+
account: viewerAccount,
|
|
296
|
+
},
|
|
297
|
+
);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// Verify error is displayed in error boundary
|
|
301
|
+
await waitFor(() => {
|
|
302
|
+
expect(container.textContent).toContain("Error!");
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("should throw error with invalid subscription ID", async () => {
|
|
307
|
+
const TestMap = co.map({
|
|
308
|
+
value: z.string(),
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
const TestComponent = () => {
|
|
312
|
+
const value = useSuspenseCoState(TestMap, "invalid-id");
|
|
313
|
+
return <div>{value.value}</div>;
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const { container } = await act(async () => {
|
|
317
|
+
return render(
|
|
318
|
+
<ErrorBoundary fallback={<div>Error!</div>}>
|
|
319
|
+
<Suspense fallback={<div>Loading...</div>}>
|
|
320
|
+
<TestComponent />
|
|
321
|
+
</Suspense>
|
|
322
|
+
</ErrorBoundary>,
|
|
323
|
+
);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// Wait for error to be thrown
|
|
327
|
+
await waitFor(
|
|
328
|
+
() => {
|
|
329
|
+
expect(container.textContent).toContain("Error!");
|
|
330
|
+
},
|
|
331
|
+
{ timeout: 1000 },
|
|
332
|
+
);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("should throw error when CoValue is unauthorized", async () => {
|
|
336
|
+
const TestMap = co.map({
|
|
337
|
+
value: z.string(),
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// Create CoValue owned by another account without sharing
|
|
341
|
+
const map = TestMap.create(
|
|
342
|
+
{
|
|
343
|
+
value: "123",
|
|
344
|
+
},
|
|
345
|
+
Group.create(),
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
await createJazzTestAccount({
|
|
349
|
+
isCurrentActiveAccount: true,
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
const TestComponent = () => {
|
|
353
|
+
const value = useSuspenseCoState(TestMap, map.$jazz.id);
|
|
354
|
+
return <div>{value.value}</div>;
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
const { container } = await act(async () => {
|
|
358
|
+
return render(
|
|
359
|
+
<ErrorBoundary fallback={<div>Error!</div>}>
|
|
360
|
+
<Suspense fallback={<div>Loading...</div>}>
|
|
361
|
+
<TestComponent />
|
|
362
|
+
</Suspense>
|
|
363
|
+
</ErrorBoundary>,
|
|
364
|
+
);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// Wait for error to be thrown (unauthorized access)
|
|
368
|
+
await waitFor(() => {
|
|
369
|
+
expect(container.textContent).toContain("Error!");
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it("should update value when CoValue changes", async () => {
|
|
374
|
+
const TestMap = co.map({
|
|
375
|
+
value: z.string(),
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
const account = await createJazzTestAccount({
|
|
379
|
+
isCurrentActiveAccount: true,
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
const map = TestMap.create({
|
|
383
|
+
value: "123",
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
387
|
+
<Suspense fallback={<div>Loading...</div>}>{children}</Suspense>
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
const { result } = renderHook(
|
|
391
|
+
() => useSuspenseCoState(TestMap, map.$jazz.id),
|
|
392
|
+
{
|
|
393
|
+
account,
|
|
394
|
+
wrapper,
|
|
395
|
+
},
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
// Wait for initial load
|
|
399
|
+
await waitFor(() => {
|
|
400
|
+
expect(result.current).toBeTruthy();
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// Verify initial value is correct
|
|
404
|
+
assertLoaded(result.current);
|
|
405
|
+
expect(result.current.value).toBe("123");
|
|
406
|
+
|
|
407
|
+
// Update the CoValue field
|
|
408
|
+
act(() => {
|
|
409
|
+
map.$jazz.set("value", "456");
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
// Verify the hook returns updated value
|
|
413
|
+
await waitFor(() => {
|
|
414
|
+
expect(result.current.value).toBe("456");
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
// Verify it's still loaded (no suspension occurred)
|
|
418
|
+
assertLoaded(result.current);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it("should maintain loaded state during updates", async () => {
|
|
422
|
+
const TestMap = co.map({
|
|
423
|
+
value: z.string(),
|
|
424
|
+
count: z.number(),
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
const account = await createJazzTestAccount({
|
|
428
|
+
isCurrentActiveAccount: true,
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
const map = TestMap.create({
|
|
432
|
+
value: "initial",
|
|
433
|
+
count: 0,
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
437
|
+
<Suspense fallback={<div>Loading...</div>}>{children}</Suspense>
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
const { result } = renderHook(
|
|
441
|
+
() => useSuspenseCoState(TestMap, map.$jazz.id),
|
|
442
|
+
{
|
|
443
|
+
account,
|
|
444
|
+
wrapper,
|
|
445
|
+
},
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
// Wait for initial load
|
|
449
|
+
await waitFor(() => {
|
|
450
|
+
expect(result.current).toBeDefined();
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
assertLoaded(result.current);
|
|
454
|
+
expect(result.current.value).toBe("initial");
|
|
455
|
+
expect(result.current.count).toBe(0);
|
|
456
|
+
|
|
457
|
+
// Update multiple fields
|
|
458
|
+
act(() => {
|
|
459
|
+
map.$jazz.set("value", "updated");
|
|
460
|
+
map.$jazz.set("count", 42);
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
// Verify all changes are reflected
|
|
464
|
+
await waitFor(() => {
|
|
465
|
+
expect(result.current.value).toBe("updated");
|
|
466
|
+
expect(result.current.count).toBe(42);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
// Verify still loaded (no suspension)
|
|
470
|
+
assertLoaded(result.current);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it("should load nested values with resolve query", async () => {
|
|
474
|
+
const TestNestedMap = co.map({
|
|
475
|
+
value: z.string(),
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
const TestMap = co.map({
|
|
479
|
+
value: z.string(),
|
|
480
|
+
nested: TestNestedMap,
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
const account = await createJazzTestAccount({
|
|
484
|
+
isCurrentActiveAccount: true,
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
const map = TestMap.create({
|
|
488
|
+
value: "123",
|
|
489
|
+
nested: TestNestedMap.create({
|
|
490
|
+
value: "456",
|
|
491
|
+
}),
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
495
|
+
<Suspense fallback={<div>Loading...</div>}>{children}</Suspense>
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
const { result } = renderHook(
|
|
499
|
+
() =>
|
|
500
|
+
useSuspenseCoState(TestMap, map.$jazz.id, {
|
|
501
|
+
resolve: {
|
|
502
|
+
nested: true,
|
|
503
|
+
},
|
|
504
|
+
}),
|
|
505
|
+
{
|
|
506
|
+
account,
|
|
507
|
+
wrapper,
|
|
508
|
+
},
|
|
509
|
+
);
|
|
510
|
+
|
|
511
|
+
// Wait for both parent and nested values to load
|
|
512
|
+
await waitFor(() => {
|
|
513
|
+
expect(result.current).toBeDefined();
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
// Verify both parent and nested values are loaded
|
|
517
|
+
assertLoaded(result.current);
|
|
518
|
+
expect(result.current.value).toBe("123");
|
|
519
|
+
assertLoaded(result.current.nested);
|
|
520
|
+
expect(result.current.nested.value).toBe("456");
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
it("should auto-load nested values on access", async () => {
|
|
524
|
+
const TestNestedMap = co.map({
|
|
525
|
+
value: z.string(),
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
const TestMap = co.map({
|
|
529
|
+
value: z.string(),
|
|
530
|
+
nested: TestNestedMap,
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
const map = TestMap.create(
|
|
534
|
+
{
|
|
535
|
+
value: "123",
|
|
536
|
+
nested: {
|
|
537
|
+
value: "456",
|
|
538
|
+
},
|
|
539
|
+
},
|
|
540
|
+
Group.create().makePublic("reader"),
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
const account = await createJazzTestAccount({
|
|
544
|
+
isCurrentActiveAccount: true,
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
// Preload the CoValue to avoid that the initial load triggers a suspension
|
|
548
|
+
await TestMap.load(map.$jazz.id);
|
|
549
|
+
|
|
550
|
+
let suspenseTriggered = false;
|
|
551
|
+
|
|
552
|
+
const SuspenseFallback = () => {
|
|
553
|
+
suspenseTriggered = true;
|
|
554
|
+
return <div>Loading...</div>;
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
558
|
+
<Suspense fallback={<SuspenseFallback />}>{children}</Suspense>
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
const { result } = renderHook(
|
|
562
|
+
() => useSuspenseCoState(TestMap, map.$jazz.id),
|
|
563
|
+
{
|
|
564
|
+
account,
|
|
565
|
+
wrapper,
|
|
566
|
+
},
|
|
567
|
+
);
|
|
568
|
+
|
|
569
|
+
// Wait for parent value to load
|
|
570
|
+
await waitFor(() => {
|
|
571
|
+
expect(result.current).toBeDefined();
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
// Verify parent value is loaded
|
|
575
|
+
assertLoaded(result.current);
|
|
576
|
+
expect(result.current.value).toBe("123");
|
|
577
|
+
|
|
578
|
+
// Access nested value - it should load automatically
|
|
579
|
+
await waitFor(() => {
|
|
580
|
+
assertLoaded(result.current.nested);
|
|
581
|
+
expect(result.current.nested.value).toBe("456");
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
// Verify Suspense was not triggered during the autoload
|
|
585
|
+
expect(suspenseTriggered).toBe(false);
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
it("should load deeply nested structures", async () => {
|
|
589
|
+
const Message = co.map({
|
|
590
|
+
content: co.plainText(),
|
|
591
|
+
});
|
|
592
|
+
const Messages = co.list(Message);
|
|
593
|
+
const Thread = co.map({
|
|
594
|
+
messages: Messages,
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
const account = await createJazzTestAccount({
|
|
598
|
+
isCurrentActiveAccount: true,
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
const thread = Thread.create({
|
|
602
|
+
messages: Messages.create([
|
|
603
|
+
Message.create({
|
|
604
|
+
content: "Hello man!",
|
|
605
|
+
}),
|
|
606
|
+
Message.create({
|
|
607
|
+
content: "The temperature is high today",
|
|
608
|
+
}),
|
|
609
|
+
Message.create({
|
|
610
|
+
content: "Shall we go to the beach?",
|
|
611
|
+
}),
|
|
612
|
+
]),
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
616
|
+
<Suspense fallback={<div>Loading...</div>}>{children}</Suspense>
|
|
617
|
+
);
|
|
618
|
+
|
|
619
|
+
const { result } = renderHook(
|
|
620
|
+
() =>
|
|
621
|
+
useSuspenseCoState(Thread, thread.$jazz.id, {
|
|
622
|
+
resolve: {
|
|
623
|
+
messages: {
|
|
624
|
+
$each: {
|
|
625
|
+
content: true,
|
|
626
|
+
},
|
|
627
|
+
},
|
|
628
|
+
},
|
|
629
|
+
}),
|
|
630
|
+
{
|
|
631
|
+
account,
|
|
632
|
+
wrapper,
|
|
633
|
+
},
|
|
634
|
+
);
|
|
635
|
+
|
|
636
|
+
// Wait for all nested levels to load
|
|
637
|
+
await waitFor(() => {
|
|
638
|
+
expect(result.current).toBeDefined();
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
// Verify all nested levels are loaded
|
|
642
|
+
assertLoaded(result.current);
|
|
643
|
+
expect(result.current.messages.length).toBe(3);
|
|
644
|
+
|
|
645
|
+
// Verify each message and its content are loaded
|
|
646
|
+
const message0 = result.current.messages[0];
|
|
647
|
+
expect(message0).toBeDefined();
|
|
648
|
+
assertLoaded(message0!);
|
|
649
|
+
expect(message0!.content.toString()).toBe("Hello man!");
|
|
650
|
+
|
|
651
|
+
const message1 = result.current.messages[1];
|
|
652
|
+
expect(message1).toBeDefined();
|
|
653
|
+
assertLoaded(message1!);
|
|
654
|
+
expect(message1!.content.toString()).toBe("The temperature is high today");
|
|
655
|
+
|
|
656
|
+
const message2 = result.current.messages[2];
|
|
657
|
+
expect(message2).toBeDefined();
|
|
658
|
+
assertLoaded(message2!);
|
|
659
|
+
expect(message2!.content.toString()).toBe("Shall we go to the beach?");
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
it("should work with selector function", async () => {
|
|
663
|
+
const TestMap = co.map({
|
|
664
|
+
value: z.string(),
|
|
665
|
+
count: z.number(),
|
|
666
|
+
metadata: z.string(),
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
const account = await createJazzTestAccount({
|
|
670
|
+
isCurrentActiveAccount: true,
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
const map = TestMap.create({
|
|
674
|
+
value: "test",
|
|
675
|
+
count: 42,
|
|
676
|
+
metadata: "extra",
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
680
|
+
<Suspense fallback={<div>Loading...</div>}>{children}</Suspense>
|
|
681
|
+
);
|
|
682
|
+
|
|
683
|
+
// Selector that transforms data - returns only value and count as an object
|
|
684
|
+
const { result } = renderHook(
|
|
685
|
+
() =>
|
|
686
|
+
useSuspenseCoState(TestMap, map.$jazz.id, {
|
|
687
|
+
select: (value) => ({
|
|
688
|
+
value: value.value,
|
|
689
|
+
count: value.count,
|
|
690
|
+
}),
|
|
691
|
+
}),
|
|
692
|
+
{
|
|
693
|
+
account,
|
|
694
|
+
wrapper,
|
|
695
|
+
},
|
|
696
|
+
);
|
|
697
|
+
|
|
698
|
+
// Wait for data to load
|
|
699
|
+
await waitFor(() => {
|
|
700
|
+
expect(result.current).toBeDefined();
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
// Verify returned value is the transformed result
|
|
704
|
+
expect(result.current).toEqual({
|
|
705
|
+
value: "test",
|
|
706
|
+
count: 42,
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
// Verify metadata is not included (selector filtered it out)
|
|
710
|
+
expect(result.current).not.toHaveProperty("metadata");
|
|
711
|
+
|
|
712
|
+
// Verify return type matches selector output type
|
|
713
|
+
expectTypeOf(result.current).toEqualTypeOf<{
|
|
714
|
+
value: string;
|
|
715
|
+
count: number;
|
|
716
|
+
}>();
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
it("should maintain type safety with selector", async () => {
|
|
720
|
+
const TestMap = co.map({
|
|
721
|
+
value: z.string(),
|
|
722
|
+
count: z.number(),
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
const account = await createJazzTestAccount({
|
|
726
|
+
isCurrentActiveAccount: true,
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
const map = TestMap.create({
|
|
730
|
+
value: "hello",
|
|
731
|
+
count: 10,
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
735
|
+
<Suspense fallback={<div>Loading...</div>}>{children}</Suspense>
|
|
736
|
+
);
|
|
737
|
+
|
|
738
|
+
// Selector that returns a string
|
|
739
|
+
const { result } = renderHook(
|
|
740
|
+
() =>
|
|
741
|
+
useSuspenseCoState(TestMap, map.$jazz.id, {
|
|
742
|
+
select: (value) => `${value.value}: ${value.count}`,
|
|
743
|
+
}),
|
|
744
|
+
{
|
|
745
|
+
account,
|
|
746
|
+
wrapper,
|
|
747
|
+
},
|
|
748
|
+
);
|
|
749
|
+
|
|
750
|
+
// Wait for data to load
|
|
751
|
+
await waitFor(() => {
|
|
752
|
+
expect(result.current).toBeDefined();
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
// Verify returned value is the transformed result
|
|
756
|
+
expect(result.current).toBe("hello: 10");
|
|
757
|
+
|
|
758
|
+
// Verify return type matches selector output type (string)
|
|
759
|
+
expectTypeOf(result.current).toEqualTypeOf<string>();
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
it("should update selector result when CoValue changes", async () => {
|
|
763
|
+
const TestMap = co.map({
|
|
764
|
+
value: z.string(),
|
|
765
|
+
count: z.number(),
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
const account = await createJazzTestAccount({
|
|
769
|
+
isCurrentActiveAccount: true,
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
const map = TestMap.create({
|
|
773
|
+
value: "initial",
|
|
774
|
+
count: 0,
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
778
|
+
<Suspense fallback={<div>Loading...</div>}>{children}</Suspense>
|
|
779
|
+
);
|
|
780
|
+
|
|
781
|
+
// Selector that combines value and count
|
|
782
|
+
const { result } = renderHook(
|
|
783
|
+
() =>
|
|
784
|
+
useSuspenseCoState(TestMap, map.$jazz.id, {
|
|
785
|
+
select: (value) => `${value.value}-${value.count}`,
|
|
786
|
+
}),
|
|
787
|
+
{
|
|
788
|
+
account,
|
|
789
|
+
wrapper,
|
|
790
|
+
},
|
|
791
|
+
);
|
|
792
|
+
|
|
793
|
+
// Wait for initial load
|
|
794
|
+
await waitFor(() => {
|
|
795
|
+
expect(result.current).toBeDefined();
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
expect(result.current).toBe("initial-0");
|
|
799
|
+
|
|
800
|
+
// Update the CoValue
|
|
801
|
+
act(() => {
|
|
802
|
+
map.$jazz.set("value", "updated");
|
|
803
|
+
map.$jazz.set("count", 100);
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
// Verify selector result updates
|
|
807
|
+
await waitFor(() => {
|
|
808
|
+
expect(result.current).toBe("updated-100");
|
|
809
|
+
});
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
it("should respect custom equality function", async () => {
|
|
813
|
+
const TestMap = co.map({
|
|
814
|
+
count: z.number(),
|
|
815
|
+
metadata: z.string(),
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
const account = await createJazzTestAccount({
|
|
819
|
+
isCurrentActiveAccount: true,
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
const map = TestMap.create({
|
|
823
|
+
count: 0,
|
|
824
|
+
metadata: "initial",
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
828
|
+
<Suspense fallback={<div>Loading...</div>}>{children}</Suspense>
|
|
829
|
+
);
|
|
830
|
+
|
|
831
|
+
// Custom equality function that compares count only
|
|
832
|
+
const { result } = renderHook(
|
|
833
|
+
() =>
|
|
834
|
+
useRenderCount(() =>
|
|
835
|
+
useSuspenseCoState(TestMap, map.$jazz.id, {
|
|
836
|
+
select: (value) => ({
|
|
837
|
+
count: value.count,
|
|
838
|
+
metadata: value.metadata,
|
|
839
|
+
}),
|
|
840
|
+
equalityFn: (a, b) => a.count === b.count,
|
|
841
|
+
}),
|
|
842
|
+
),
|
|
843
|
+
{
|
|
844
|
+
account,
|
|
845
|
+
wrapper,
|
|
846
|
+
},
|
|
847
|
+
);
|
|
848
|
+
|
|
849
|
+
// Wait for initial load
|
|
850
|
+
await waitFor(() => {
|
|
851
|
+
expect(result.current.result).toBeDefined();
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
// Verify initial render
|
|
855
|
+
expect(result.current.renderCount).toBe(1);
|
|
856
|
+
expect(result.current.result.count).toBe(0);
|
|
857
|
+
expect(result.current.result.metadata).toBe("initial");
|
|
858
|
+
|
|
859
|
+
const initialRenderCount = result.current.renderCount;
|
|
860
|
+
|
|
861
|
+
// Update metadata field (equality should return true - count unchanged)
|
|
862
|
+
act(() => {
|
|
863
|
+
map.$jazz.set("metadata", "updated");
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
// Wait a bit to ensure no re-render occurred
|
|
867
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
868
|
+
|
|
869
|
+
// Verify no re-render occurred (equality function returned true)
|
|
870
|
+
expect(result.current.renderCount).toBe(initialRenderCount);
|
|
871
|
+
// Note: The result might still show old metadata since no re-render occurred
|
|
872
|
+
// But the underlying data has changed
|
|
873
|
+
|
|
874
|
+
// Update count field (equality should return false - count changed)
|
|
875
|
+
act(() => {
|
|
876
|
+
map.$jazz.set("count", 42);
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
// Verify re-render occurred
|
|
880
|
+
await waitFor(() => {
|
|
881
|
+
expect(result.current.renderCount).toBe(initialRenderCount + 1);
|
|
882
|
+
expect(result.current.result.count).toBe(42);
|
|
883
|
+
});
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
it("should prevent re-renders when equality returns true", async () => {
|
|
887
|
+
const TestMap = co.map({
|
|
888
|
+
value: z.string(),
|
|
889
|
+
count: z.number(),
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
const account = await createJazzTestAccount({
|
|
893
|
+
isCurrentActiveAccount: true,
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
const map = TestMap.create({
|
|
897
|
+
value: "test",
|
|
898
|
+
count: 10,
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
902
|
+
<Suspense fallback={<div>Loading...</div>}>{children}</Suspense>
|
|
903
|
+
);
|
|
904
|
+
|
|
905
|
+
// Equality function that always returns true (prevents all re-renders)
|
|
906
|
+
const { result } = renderHook(
|
|
907
|
+
() =>
|
|
908
|
+
useRenderCount(() =>
|
|
909
|
+
useSuspenseCoState(TestMap, map.$jazz.id, {
|
|
910
|
+
select: (value) => ({
|
|
911
|
+
value: value.value,
|
|
912
|
+
count: value.count,
|
|
913
|
+
}),
|
|
914
|
+
equalityFn: () => true,
|
|
915
|
+
}),
|
|
916
|
+
),
|
|
917
|
+
{
|
|
918
|
+
account,
|
|
919
|
+
wrapper,
|
|
920
|
+
},
|
|
921
|
+
);
|
|
922
|
+
|
|
923
|
+
// Wait for initial load
|
|
924
|
+
await waitFor(() => {
|
|
925
|
+
expect(result.current.result).toBeDefined();
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
const initialRenderCount = result.current.renderCount;
|
|
929
|
+
const initialValue = result.current.result.value;
|
|
930
|
+
const initialCount = result.current.result.count;
|
|
931
|
+
|
|
932
|
+
// Update both fields multiple times
|
|
933
|
+
for (let i = 1; i <= 10; i++) {
|
|
934
|
+
act(() => {
|
|
935
|
+
map.$jazz.set("value", `updated-${i}`);
|
|
936
|
+
map.$jazz.set("count", 10 + i);
|
|
937
|
+
});
|
|
938
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// Verify no re-renders occurred (equality always returns true)
|
|
942
|
+
expect(result.current.renderCount).toBe(initialRenderCount);
|
|
943
|
+
// Result should still show initial values since no re-render occurred
|
|
944
|
+
expect(result.current.result.value).toBe(initialValue);
|
|
945
|
+
expect(result.current.result.count).toBe(initialCount);
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
it("should trigger re-render when equality returns false", async () => {
|
|
949
|
+
const TestMap = co.map({
|
|
950
|
+
value: z.string(),
|
|
951
|
+
count: z.number(),
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
const account = await createJazzTestAccount({
|
|
955
|
+
isCurrentActiveAccount: true,
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
const map = TestMap.create({
|
|
959
|
+
value: "initial",
|
|
960
|
+
count: 0,
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
964
|
+
<Suspense fallback={<div>Loading...</div>}>{children}</Suspense>
|
|
965
|
+
);
|
|
966
|
+
|
|
967
|
+
// Equality function that compares both value and count
|
|
968
|
+
const { result } = renderHook(
|
|
969
|
+
() =>
|
|
970
|
+
useRenderCount(() =>
|
|
971
|
+
useSuspenseCoState(TestMap, map.$jazz.id, {
|
|
972
|
+
select: (value) => ({
|
|
973
|
+
value: value.value,
|
|
974
|
+
count: value.count,
|
|
975
|
+
}),
|
|
976
|
+
equalityFn: (a, b) => a.value === b.value && a.count === b.count,
|
|
977
|
+
}),
|
|
978
|
+
),
|
|
979
|
+
{
|
|
980
|
+
account,
|
|
981
|
+
wrapper,
|
|
982
|
+
},
|
|
983
|
+
);
|
|
984
|
+
|
|
985
|
+
// Wait for initial load
|
|
986
|
+
await waitFor(() => {
|
|
987
|
+
expect(result.current.result).toBeDefined();
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
const initialRenderCount = result.current.renderCount;
|
|
991
|
+
|
|
992
|
+
// Update count field (equality should return false)
|
|
993
|
+
act(() => {
|
|
994
|
+
map.$jazz.set("count", 100);
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
// Verify re-render occurred
|
|
998
|
+
await waitFor(() => {
|
|
999
|
+
expect(result.current.renderCount).toBe(initialRenderCount + 1);
|
|
1000
|
+
expect(result.current.result.count).toBe(100);
|
|
1001
|
+
});
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
it("should work with branches - create, edit, and merge", async () => {
|
|
1005
|
+
const Person = co.map({
|
|
1006
|
+
name: z.string(),
|
|
1007
|
+
age: z.number(),
|
|
1008
|
+
email: z.string(),
|
|
1009
|
+
});
|
|
1010
|
+
|
|
1011
|
+
const group = Group.create();
|
|
1012
|
+
group.addMember("everyone", "writer");
|
|
1013
|
+
|
|
1014
|
+
const originalPerson = Person.create(
|
|
1015
|
+
{
|
|
1016
|
+
name: "John Doe",
|
|
1017
|
+
age: 30,
|
|
1018
|
+
email: "john@example.com",
|
|
1019
|
+
},
|
|
1020
|
+
group,
|
|
1021
|
+
);
|
|
1022
|
+
|
|
1023
|
+
const account = await createJazzTestAccount({
|
|
1024
|
+
isCurrentActiveAccount: true,
|
|
1025
|
+
});
|
|
1026
|
+
|
|
1027
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
1028
|
+
<Suspense fallback={<div>Loading...</div>}>{children}</Suspense>
|
|
1029
|
+
);
|
|
1030
|
+
|
|
1031
|
+
// Render useSuspenseCoState twice: once for branch, once for main
|
|
1032
|
+
const { result } = await act(async () => {
|
|
1033
|
+
return renderHook(
|
|
1034
|
+
() => {
|
|
1035
|
+
const branch = useSuspenseCoState(Person, originalPerson.$jazz.id, {
|
|
1036
|
+
unstable_branch: { name: "feature-branch" },
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
const main = useSuspenseCoState(Person, originalPerson.$jazz.id);
|
|
1040
|
+
|
|
1041
|
+
return { branch, main };
|
|
1042
|
+
},
|
|
1043
|
+
{
|
|
1044
|
+
account,
|
|
1045
|
+
wrapper,
|
|
1046
|
+
},
|
|
1047
|
+
);
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
// Wait for both to load
|
|
1051
|
+
await waitFor(() => {
|
|
1052
|
+
expect(result.current).not.toBeNull();
|
|
1053
|
+
expect(result.current.branch).toBeDefined();
|
|
1054
|
+
expect(result.current.main).toBeDefined();
|
|
1055
|
+
});
|
|
1056
|
+
|
|
1057
|
+
// Verify both return loaded data
|
|
1058
|
+
assertLoaded(result.current.branch);
|
|
1059
|
+
assertLoaded(result.current.main);
|
|
1060
|
+
|
|
1061
|
+
expect(result.current.branch.name).toBe("John Doe");
|
|
1062
|
+
expect(result.current.branch.age).toBe(30);
|
|
1063
|
+
expect(result.current.branch.email).toBe("john@example.com");
|
|
1064
|
+
|
|
1065
|
+
expect(result.current.main.name).toBe("John Doe");
|
|
1066
|
+
expect(result.current.main.age).toBe(30);
|
|
1067
|
+
expect(result.current.main.email).toBe("john@example.com");
|
|
1068
|
+
|
|
1069
|
+
// Use act() to modify branch CoValue
|
|
1070
|
+
act(() => {
|
|
1071
|
+
result.current.branch.$jazz.applyDiff({
|
|
1072
|
+
name: "John Smith",
|
|
1073
|
+
age: 31,
|
|
1074
|
+
email: "john.smith@example.com",
|
|
1075
|
+
});
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
// Wait for updates
|
|
1079
|
+
await waitFor(() => {
|
|
1080
|
+
expect(result.current.branch.name).toBe("John Smith");
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
// Verify branch has changes
|
|
1084
|
+
expect(result.current.branch.name).toBe("John Smith");
|
|
1085
|
+
expect(result.current.branch.age).toBe(31);
|
|
1086
|
+
expect(result.current.branch.email).toBe("john.smith@example.com");
|
|
1087
|
+
|
|
1088
|
+
// Verify main is unchanged
|
|
1089
|
+
expect(result.current.main.name).toBe("John Doe");
|
|
1090
|
+
expect(result.current.main.age).toBe(30);
|
|
1091
|
+
expect(result.current.main.email).toBe("john@example.com");
|
|
1092
|
+
|
|
1093
|
+
// Merge branch
|
|
1094
|
+
await act(async () => {
|
|
1095
|
+
await result.current.branch.$jazz.unstable_merge();
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
// Wait for merge to propagate
|
|
1099
|
+
await waitFor(() => {
|
|
1100
|
+
expect(result.current.main.name).toBe("John Smith");
|
|
1101
|
+
});
|
|
1102
|
+
|
|
1103
|
+
// Verify main now has the changes
|
|
1104
|
+
expect(result.current.main.name).toBe("John Smith");
|
|
1105
|
+
expect(result.current.main.age).toBe(31);
|
|
1106
|
+
expect(result.current.main.email).toBe("john.smith@example.com");
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
it("should preload value when provided", async () => {
|
|
1110
|
+
disableJazzTestSync();
|
|
1111
|
+
|
|
1112
|
+
const Person = co.map({
|
|
1113
|
+
name: z.string(),
|
|
1114
|
+
age: z.number(),
|
|
1115
|
+
email: z.string(),
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
const group = Group.create();
|
|
1119
|
+
group.addMember("everyone", "writer");
|
|
1120
|
+
|
|
1121
|
+
const originalPerson = Person.create(
|
|
1122
|
+
{
|
|
1123
|
+
name: "John Doe",
|
|
1124
|
+
age: 30,
|
|
1125
|
+
email: "john@example.com",
|
|
1126
|
+
},
|
|
1127
|
+
group,
|
|
1128
|
+
);
|
|
1129
|
+
|
|
1130
|
+
// Create a test account (different from creator)
|
|
1131
|
+
const bob = await createJazzTestAccount({
|
|
1132
|
+
isCurrentActiveAccount: true,
|
|
1133
|
+
});
|
|
1134
|
+
|
|
1135
|
+
// Export the CoValue
|
|
1136
|
+
const exportedPerson = originalPerson.$jazz.export();
|
|
1137
|
+
|
|
1138
|
+
// Track render count
|
|
1139
|
+
let renderCount = 0;
|
|
1140
|
+
let suspenseTriggered = false;
|
|
1141
|
+
|
|
1142
|
+
const SuspenseFallback = () => {
|
|
1143
|
+
suspenseTriggered = true;
|
|
1144
|
+
return <div>Loading...</div>;
|
|
1145
|
+
};
|
|
1146
|
+
|
|
1147
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
1148
|
+
<Suspense fallback={<SuspenseFallback />}>{children}</Suspense>
|
|
1149
|
+
);
|
|
1150
|
+
|
|
1151
|
+
// Render useSuspenseCoState with preloaded data
|
|
1152
|
+
const { result } = renderHook(
|
|
1153
|
+
() => {
|
|
1154
|
+
renderCount++;
|
|
1155
|
+
return useSuspenseCoState(Person, originalPerson.$jazz.id, {
|
|
1156
|
+
preloaded: exportedPerson,
|
|
1157
|
+
});
|
|
1158
|
+
},
|
|
1159
|
+
{
|
|
1160
|
+
account: bob,
|
|
1161
|
+
wrapper,
|
|
1162
|
+
},
|
|
1163
|
+
);
|
|
1164
|
+
|
|
1165
|
+
// Wait for any async operations
|
|
1166
|
+
await waitFor(() => {
|
|
1167
|
+
expect(result.current).toBeDefined();
|
|
1168
|
+
});
|
|
1169
|
+
|
|
1170
|
+
// Verify only one render occurred
|
|
1171
|
+
expect(renderCount).toBe(1);
|
|
1172
|
+
|
|
1173
|
+
// Verify Suspense was not triggered (preloaded data enables immediate rendering)
|
|
1174
|
+
expect(suspenseTriggered).toBe(false);
|
|
1175
|
+
|
|
1176
|
+
// Verify data is immediately accessible
|
|
1177
|
+
assertLoaded(result.current);
|
|
1178
|
+
expect(result.current.name).toBe("John Doe");
|
|
1179
|
+
expect(result.current.age).toBe(30);
|
|
1180
|
+
expect(result.current.email).toBe("john@example.com");
|
|
1181
|
+
});
|
|
1182
|
+
});
|