@mdxui/terminal 2.0.0

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.
Files changed (191) hide show
  1. package/README.md +571 -0
  2. package/dist/ansi-css-Sk5mWtdK.d.ts +119 -0
  3. package/dist/ansi-css-V6JIHGsM.d.ts +119 -0
  4. package/dist/ansi-css-_3eSEU9d.d.ts +119 -0
  5. package/dist/chunk-3EFDH7PK.js +5235 -0
  6. package/dist/chunk-3RG5ZIWI.js +10 -0
  7. package/dist/chunk-3X5IR6WE.js +884 -0
  8. package/dist/chunk-4FV5ZDCE.js +5236 -0
  9. package/dist/chunk-4OVMSF2J.js +243 -0
  10. package/dist/chunk-63FEETIS.js +4048 -0
  11. package/dist/chunk-B43KP7XJ.js +884 -0
  12. package/dist/chunk-BMTJXWUV.js +655 -0
  13. package/dist/chunk-C3SVH4N7.js +882 -0
  14. package/dist/chunk-EVWR7Y47.js +874 -0
  15. package/dist/chunk-F6A5VWUC.js +1285 -0
  16. package/dist/chunk-FD7KW7GE.js +882 -0
  17. package/dist/chunk-GBQ6UD6I.js +655 -0
  18. package/dist/chunk-GMDD3M6U.js +5227 -0
  19. package/dist/chunk-JBHRXOXM.js +1058 -0
  20. package/dist/chunk-JFOO3EYO.js +1182 -0
  21. package/dist/chunk-JQ5H3WXL.js +1291 -0
  22. package/dist/chunk-JQD5NASE.js +234 -0
  23. package/dist/chunk-KRHJP5R7.js +592 -0
  24. package/dist/chunk-KWF6WVJE.js +962 -0
  25. package/dist/chunk-LHYQVN3H.js +1038 -0
  26. package/dist/chunk-M3TLQLGC.js +1032 -0
  27. package/dist/chunk-MVW4Q5OP.js +240 -0
  28. package/dist/chunk-NXCZSWLU.js +1294 -0
  29. package/dist/chunk-O25TNRO6.js +607 -0
  30. package/dist/chunk-PNECDA2I.js +884 -0
  31. package/dist/chunk-QIHWRLJR.js +962 -0
  32. package/dist/chunk-QW5YMQ7K.js +882 -0
  33. package/dist/chunk-R5U7XKVJ.js +16 -0
  34. package/dist/chunk-RP2MVQLR.js +962 -0
  35. package/dist/chunk-TP6RXGXA.js +1087 -0
  36. package/dist/chunk-TQQSTITZ.js +655 -0
  37. package/dist/chunk-X24GWXQV.js +1281 -0
  38. package/dist/components/index.d.ts +802 -0
  39. package/dist/components/index.js +149 -0
  40. package/dist/data/index.d.ts +2554 -0
  41. package/dist/data/index.js +51 -0
  42. package/dist/forms/index.d.ts +1596 -0
  43. package/dist/forms/index.js +464 -0
  44. package/dist/index-CQRFZntR.d.ts +867 -0
  45. package/dist/index.d.ts +579 -0
  46. package/dist/index.js +786 -0
  47. package/dist/interactive-D0JkWosD.d.ts +217 -0
  48. package/dist/keyboard/index.d.ts +2 -0
  49. package/dist/keyboard/index.js +43 -0
  50. package/dist/renderers/index.d.ts +546 -0
  51. package/dist/renderers/index.js +2157 -0
  52. package/dist/storybook/index.d.ts +396 -0
  53. package/dist/storybook/index.js +641 -0
  54. package/dist/theme/index.d.ts +1339 -0
  55. package/dist/theme/index.js +123 -0
  56. package/dist/types-Bxu5PAgA.d.ts +710 -0
  57. package/dist/types-CIlop5Ji.d.ts +701 -0
  58. package/dist/types-Ca8p_p5X.d.ts +710 -0
  59. package/package.json +90 -0
  60. package/src/__tests__/components/data/card.test.ts +458 -0
  61. package/src/__tests__/components/data/list.test.ts +473 -0
  62. package/src/__tests__/components/data/metrics.test.ts +541 -0
  63. package/src/__tests__/components/data/table.test.ts +448 -0
  64. package/src/__tests__/components/input/field.test.ts +555 -0
  65. package/src/__tests__/components/input/form.test.ts +870 -0
  66. package/src/__tests__/components/input/search.test.ts +1238 -0
  67. package/src/__tests__/components/input/select.test.ts +658 -0
  68. package/src/__tests__/components/navigation/breadcrumb.test.ts +923 -0
  69. package/src/__tests__/components/navigation/command-palette.test.ts +1095 -0
  70. package/src/__tests__/components/navigation/sidebar.test.ts +1018 -0
  71. package/src/__tests__/components/navigation/tabs.test.ts +995 -0
  72. package/src/__tests__/components.test.tsx +1197 -0
  73. package/src/__tests__/core/compiler.test.ts +986 -0
  74. package/src/__tests__/core/parser.test.ts +785 -0
  75. package/src/__tests__/core/tier-switcher.test.ts +1103 -0
  76. package/src/__tests__/core/types.test.ts +1398 -0
  77. package/src/__tests__/data/collections.test.ts +1337 -0
  78. package/src/__tests__/data/db.test.ts +1265 -0
  79. package/src/__tests__/data/reactive.test.ts +1010 -0
  80. package/src/__tests__/data/sync.test.ts +1614 -0
  81. package/src/__tests__/errors.test.ts +660 -0
  82. package/src/__tests__/forms/integration.test.ts +444 -0
  83. package/src/__tests__/integration.test.ts +905 -0
  84. package/src/__tests__/keyboard.test.ts +1791 -0
  85. package/src/__tests__/renderer.test.ts +489 -0
  86. package/src/__tests__/renderers/ansi-css.test.ts +948 -0
  87. package/src/__tests__/renderers/ansi.test.ts +1366 -0
  88. package/src/__tests__/renderers/ascii.test.ts +1360 -0
  89. package/src/__tests__/renderers/interactive.test.ts +2353 -0
  90. package/src/__tests__/renderers/markdown.test.ts +1483 -0
  91. package/src/__tests__/renderers/text.test.ts +1369 -0
  92. package/src/__tests__/renderers/unicode.test.ts +1307 -0
  93. package/src/__tests__/theme.test.ts +639 -0
  94. package/src/__tests__/utils/assertions.ts +685 -0
  95. package/src/__tests__/utils/index.ts +115 -0
  96. package/src/__tests__/utils/test-renderer.ts +381 -0
  97. package/src/__tests__/utils/utils.test.ts +560 -0
  98. package/src/components/containers/card.ts +56 -0
  99. package/src/components/containers/dialog.ts +53 -0
  100. package/src/components/containers/index.ts +9 -0
  101. package/src/components/containers/panel.ts +59 -0
  102. package/src/components/feedback/badge.ts +40 -0
  103. package/src/components/feedback/index.ts +8 -0
  104. package/src/components/feedback/spinner.ts +23 -0
  105. package/src/components/helpers.ts +81 -0
  106. package/src/components/index.ts +153 -0
  107. package/src/components/layout/breadcrumb.ts +31 -0
  108. package/src/components/layout/index.ts +10 -0
  109. package/src/components/layout/list.ts +29 -0
  110. package/src/components/layout/sidebar.ts +79 -0
  111. package/src/components/layout/table.ts +62 -0
  112. package/src/components/primitives/box.ts +95 -0
  113. package/src/components/primitives/button.ts +54 -0
  114. package/src/components/primitives/index.ts +11 -0
  115. package/src/components/primitives/input.ts +88 -0
  116. package/src/components/primitives/select.ts +97 -0
  117. package/src/components/primitives/text.ts +60 -0
  118. package/src/components/render.ts +155 -0
  119. package/src/components/templates/app.ts +43 -0
  120. package/src/components/templates/index.ts +8 -0
  121. package/src/components/templates/site.ts +54 -0
  122. package/src/components/types.ts +777 -0
  123. package/src/core/compiler.ts +718 -0
  124. package/src/core/parser.ts +127 -0
  125. package/src/core/tier-switcher.ts +607 -0
  126. package/src/core/types.ts +672 -0
  127. package/src/data/collection.ts +316 -0
  128. package/src/data/collections.ts +50 -0
  129. package/src/data/context.tsx +174 -0
  130. package/src/data/db.ts +127 -0
  131. package/src/data/hooks.ts +532 -0
  132. package/src/data/index.ts +138 -0
  133. package/src/data/reactive.ts +1225 -0
  134. package/src/data/saas-collections.ts +375 -0
  135. package/src/data/sync.ts +1213 -0
  136. package/src/data/types.ts +660 -0
  137. package/src/forms/converters.ts +512 -0
  138. package/src/forms/index.ts +133 -0
  139. package/src/forms/schemas.ts +403 -0
  140. package/src/forms/types.ts +476 -0
  141. package/src/index.ts +542 -0
  142. package/src/keyboard/focus.ts +748 -0
  143. package/src/keyboard/index.ts +96 -0
  144. package/src/keyboard/integration.ts +371 -0
  145. package/src/keyboard/manager.ts +377 -0
  146. package/src/keyboard/presets.ts +90 -0
  147. package/src/renderers/ansi-css.ts +576 -0
  148. package/src/renderers/ansi.ts +802 -0
  149. package/src/renderers/ascii.ts +680 -0
  150. package/src/renderers/breadcrumb.ts +480 -0
  151. package/src/renderers/command-palette.ts +802 -0
  152. package/src/renderers/components/field.ts +210 -0
  153. package/src/renderers/components/form.ts +327 -0
  154. package/src/renderers/components/index.ts +21 -0
  155. package/src/renderers/components/search.ts +449 -0
  156. package/src/renderers/components/select.ts +222 -0
  157. package/src/renderers/index.ts +101 -0
  158. package/src/renderers/interactive/component-handlers.ts +622 -0
  159. package/src/renderers/interactive/cursor-manager.ts +147 -0
  160. package/src/renderers/interactive/focus-manager.ts +279 -0
  161. package/src/renderers/interactive/index.ts +661 -0
  162. package/src/renderers/interactive/input-handler.ts +164 -0
  163. package/src/renderers/interactive/keyboard-handler.ts +212 -0
  164. package/src/renderers/interactive/mouse-handler.ts +167 -0
  165. package/src/renderers/interactive/state-manager.ts +109 -0
  166. package/src/renderers/interactive/types.ts +338 -0
  167. package/src/renderers/interactive-string.ts +299 -0
  168. package/src/renderers/interactive.ts +59 -0
  169. package/src/renderers/markdown.ts +950 -0
  170. package/src/renderers/sidebar.ts +549 -0
  171. package/src/renderers/tabs.ts +682 -0
  172. package/src/renderers/text.ts +791 -0
  173. package/src/renderers/unicode.ts +917 -0
  174. package/src/renderers/utils.ts +942 -0
  175. package/src/router/adapters.ts +383 -0
  176. package/src/router/types.ts +140 -0
  177. package/src/router/utils.ts +452 -0
  178. package/src/schemas.ts +205 -0
  179. package/src/storybook/index.ts +91 -0
  180. package/src/storybook/interactive-decorator.tsx +659 -0
  181. package/src/storybook/keyboard-simulator.ts +501 -0
  182. package/src/theme/ansi-codes.ts +80 -0
  183. package/src/theme/box-drawing.ts +132 -0
  184. package/src/theme/color-convert.ts +254 -0
  185. package/src/theme/color-support.ts +321 -0
  186. package/src/theme/index.ts +134 -0
  187. package/src/theme/strip-ansi.ts +50 -0
  188. package/src/theme/tailwind-map.ts +469 -0
  189. package/src/theme/text-styles.ts +206 -0
  190. package/src/theme/theme-system.ts +568 -0
  191. package/src/types.ts +103 -0
@@ -0,0 +1,1010 @@
1
+ /**
2
+ * @mdxui/terminal Reactive Data Hooks Tests
3
+ *
4
+ * Tests for the reactive data hooks (useReactiveData, useReactiveTable,
5
+ * useReactiveList, useReactiveMetrics, useReactiveCard) that integrate
6
+ * data components with TanStack DB collections.
7
+ */
8
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
9
+ import { renderHook, act, waitFor } from '@testing-library/react'
10
+ import * as React from 'react'
11
+ import { z } from 'zod'
12
+ import {
13
+ createDB,
14
+ createCollection,
15
+ DBProvider,
16
+ useReactiveData,
17
+ useReactiveTable,
18
+ useReactiveList,
19
+ useReactiveMetrics,
20
+ useReactiveCard,
21
+ } from '../../data'
22
+ import type { DB } from '../../data'
23
+
24
+ // ============================================================================
25
+ // Test Setup
26
+ // ============================================================================
27
+
28
+ const UserSchema = z.object({
29
+ id: z.string(),
30
+ name: z.string(),
31
+ email: z.string(),
32
+ role: z.enum(['admin', 'user', 'guest']),
33
+ age: z.number().optional(),
34
+ status: z.enum(['active', 'inactive']).default('active'),
35
+ })
36
+
37
+ type User = z.infer<typeof UserSchema>
38
+
39
+ const OrderSchema = z.object({
40
+ id: z.string(),
41
+ userId: z.string(),
42
+ total: z.number(),
43
+ status: z.enum(['pending', 'completed', 'cancelled']),
44
+ createdAt: z.string(),
45
+ })
46
+
47
+ type Order = z.infer<typeof OrderSchema>
48
+
49
+ function createTestDB(): DB {
50
+ const usersCollection = createCollection<User>({
51
+ name: 'users',
52
+ schema: UserSchema,
53
+ primaryKey: 'id',
54
+ })
55
+
56
+ const ordersCollection = createCollection<Order>({
57
+ name: 'orders',
58
+ schema: OrderSchema,
59
+ primaryKey: 'id',
60
+ })
61
+
62
+ return createDB({
63
+ collections: [usersCollection, ordersCollection],
64
+ })
65
+ }
66
+
67
+ function createWrapper(db: DB) {
68
+ return function Wrapper({ children }: { children: React.ReactNode }) {
69
+ return React.createElement(DBProvider, { db }, children)
70
+ }
71
+ }
72
+
73
+ // ============================================================================
74
+ // useReactiveData Tests
75
+ // ============================================================================
76
+
77
+ describe('useReactiveData', () => {
78
+ let db: DB
79
+
80
+ beforeEach(() => {
81
+ db = createTestDB()
82
+ })
83
+
84
+ afterEach(async () => {
85
+ await db.clear()
86
+ db.close()
87
+ })
88
+
89
+ describe('basic queries', () => {
90
+ it('returns empty array for empty collection', async () => {
91
+ const { result } = renderHook(
92
+ () => useReactiveData<User>({ collection: 'users' }),
93
+ { wrapper: createWrapper(db) }
94
+ )
95
+
96
+ await waitFor(() => {
97
+ expect(result.current.isLoading).toBe(false)
98
+ })
99
+
100
+ expect(result.current.data).toEqual([])
101
+ expect(result.current.error).toBeUndefined()
102
+ })
103
+
104
+ it('returns data from collection', async () => {
105
+ await db.collections.users.insert({
106
+ id: '1',
107
+ name: 'Alice',
108
+ email: 'alice@example.com',
109
+ role: 'admin',
110
+ status: 'active',
111
+ })
112
+
113
+ const { result } = renderHook(
114
+ () => useReactiveData<User>({ collection: 'users' }),
115
+ { wrapper: createWrapper(db) }
116
+ )
117
+
118
+ await waitFor(() => {
119
+ expect(result.current.isLoading).toBe(false)
120
+ })
121
+
122
+ expect(result.current.data).toHaveLength(1)
123
+ expect(result.current.data[0].name).toBe('Alice')
124
+ })
125
+
126
+ it('filters data with where clause', async () => {
127
+ await db.collections.users.insert({
128
+ id: '1',
129
+ name: 'Alice',
130
+ email: 'alice@example.com',
131
+ role: 'admin',
132
+ status: 'active',
133
+ })
134
+ await db.collections.users.insert({
135
+ id: '2',
136
+ name: 'Bob',
137
+ email: 'bob@example.com',
138
+ role: 'user',
139
+ status: 'active',
140
+ })
141
+
142
+ const { result } = renderHook(
143
+ () => useReactiveData<User>({
144
+ collection: 'users',
145
+ where: { role: 'admin' },
146
+ }),
147
+ { wrapper: createWrapper(db) }
148
+ )
149
+
150
+ await waitFor(() => {
151
+ expect(result.current.isLoading).toBe(false)
152
+ })
153
+
154
+ expect(result.current.data).toHaveLength(1)
155
+ expect(result.current.data[0].name).toBe('Alice')
156
+ })
157
+
158
+ it('returns error for non-existent collection', async () => {
159
+ const { result } = renderHook(
160
+ () => useReactiveData<User>({ collection: 'nonexistent' }),
161
+ { wrapper: createWrapper(db) }
162
+ )
163
+
164
+ await waitFor(() => {
165
+ expect(result.current.isLoading).toBe(false)
166
+ })
167
+
168
+ expect(result.current.error).toBeDefined()
169
+ expect(result.current.error?.message).toContain('not found')
170
+ })
171
+ })
172
+
173
+ describe('reactive updates', () => {
174
+ it('updates data when collection changes', async () => {
175
+ const { result } = renderHook(
176
+ () => useReactiveData<User>({ collection: 'users' }),
177
+ { wrapper: createWrapper(db) }
178
+ )
179
+
180
+ await waitFor(() => {
181
+ expect(result.current.isLoading).toBe(false)
182
+ })
183
+
184
+ expect(result.current.data).toHaveLength(0)
185
+
186
+ // Insert data
187
+ await act(async () => {
188
+ await db.collections.users.insert({
189
+ id: '1',
190
+ name: 'Alice',
191
+ email: 'alice@example.com',
192
+ role: 'admin',
193
+ status: 'active',
194
+ })
195
+ })
196
+
197
+ await waitFor(() => {
198
+ expect(result.current.data).toHaveLength(1)
199
+ })
200
+
201
+ expect(result.current.data[0].name).toBe('Alice')
202
+ })
203
+
204
+ it('updates data when document is updated', async () => {
205
+ await db.collections.users.insert({
206
+ id: '1',
207
+ name: 'Alice',
208
+ email: 'alice@example.com',
209
+ role: 'admin',
210
+ status: 'active',
211
+ })
212
+
213
+ const { result } = renderHook(
214
+ () => useReactiveData<User>({ collection: 'users' }),
215
+ { wrapper: createWrapper(db) }
216
+ )
217
+
218
+ await waitFor(() => {
219
+ expect(result.current.data).toHaveLength(1)
220
+ })
221
+
222
+ // Update data
223
+ await act(async () => {
224
+ await db.collections.users.update({ id: '1' }, { name: 'Alicia' })
225
+ })
226
+
227
+ await waitFor(() => {
228
+ expect(result.current.data[0].name).toBe('Alicia')
229
+ })
230
+ })
231
+
232
+ it('updates data when document is deleted', async () => {
233
+ await db.collections.users.insert({
234
+ id: '1',
235
+ name: 'Alice',
236
+ email: 'alice@example.com',
237
+ role: 'admin',
238
+ status: 'active',
239
+ })
240
+
241
+ const { result } = renderHook(
242
+ () => useReactiveData<User>({ collection: 'users' }),
243
+ { wrapper: createWrapper(db) }
244
+ )
245
+
246
+ await waitFor(() => {
247
+ expect(result.current.data).toHaveLength(1)
248
+ })
249
+
250
+ // Delete data
251
+ await act(async () => {
252
+ await db.collections.users.delete({ id: '1' })
253
+ })
254
+
255
+ await waitFor(() => {
256
+ expect(result.current.data).toHaveLength(0)
257
+ })
258
+ })
259
+ })
260
+
261
+ describe('mutations', () => {
262
+ it('inserts data via mutate.insert', async () => {
263
+ const { result } = renderHook(
264
+ () => useReactiveData<User>({ collection: 'users' }),
265
+ { wrapper: createWrapper(db) }
266
+ )
267
+
268
+ await waitFor(() => {
269
+ expect(result.current.isLoading).toBe(false)
270
+ })
271
+
272
+ await act(async () => {
273
+ await result.current.mutate.insert({
274
+ id: '1',
275
+ name: 'Alice',
276
+ email: 'alice@example.com',
277
+ role: 'admin',
278
+ status: 'active',
279
+ })
280
+ })
281
+
282
+ expect(result.current.data).toHaveLength(1)
283
+ expect(result.current.data[0].name).toBe('Alice')
284
+ })
285
+
286
+ it('updates data via mutate.update', async () => {
287
+ await db.collections.users.insert({
288
+ id: '1',
289
+ name: 'Alice',
290
+ email: 'alice@example.com',
291
+ role: 'admin',
292
+ status: 'active',
293
+ })
294
+
295
+ const { result } = renderHook(
296
+ () => useReactiveData<User>({ collection: 'users' }),
297
+ { wrapper: createWrapper(db) }
298
+ )
299
+
300
+ await waitFor(() => {
301
+ expect(result.current.data).toHaveLength(1)
302
+ })
303
+
304
+ await act(async () => {
305
+ await result.current.mutate.update({ id: '1' }, { name: 'Alicia' })
306
+ })
307
+
308
+ await waitFor(() => {
309
+ expect(result.current.data[0].name).toBe('Alicia')
310
+ })
311
+ })
312
+
313
+ it('deletes data via mutate.delete', async () => {
314
+ await db.collections.users.insert({
315
+ id: '1',
316
+ name: 'Alice',
317
+ email: 'alice@example.com',
318
+ role: 'admin',
319
+ status: 'active',
320
+ })
321
+
322
+ const { result } = renderHook(
323
+ () => useReactiveData<User>({ collection: 'users' }),
324
+ { wrapper: createWrapper(db) }
325
+ )
326
+
327
+ await waitFor(() => {
328
+ expect(result.current.data).toHaveLength(1)
329
+ })
330
+
331
+ await act(async () => {
332
+ await result.current.mutate.delete({ id: '1' })
333
+ })
334
+
335
+ await waitFor(() => {
336
+ expect(result.current.data).toHaveLength(0)
337
+ })
338
+ })
339
+
340
+ it('performs optimistic insert', async () => {
341
+ const { result } = renderHook(
342
+ () => useReactiveData<User>({
343
+ collection: 'users',
344
+ optimistic: true,
345
+ }),
346
+ { wrapper: createWrapper(db) }
347
+ )
348
+
349
+ await waitFor(() => {
350
+ expect(result.current.isLoading).toBe(false)
351
+ })
352
+
353
+ // Optimistic insert - data shows immediately after mutation starts
354
+ await act(async () => {
355
+ await result.current.mutate.insert({
356
+ id: '1',
357
+ name: 'Alice',
358
+ email: 'alice@example.com',
359
+ role: 'admin',
360
+ status: 'active',
361
+ })
362
+ })
363
+
364
+ // Data should be updated (optimistically or via subscription)
365
+ expect(result.current.data).toHaveLength(1)
366
+ expect(result.current.data[0].name).toBe('Alice')
367
+ })
368
+ })
369
+
370
+ describe('sorting', () => {
371
+ beforeEach(async () => {
372
+ await db.collections.users.insert({
373
+ id: '1',
374
+ name: 'Charlie',
375
+ email: 'charlie@example.com',
376
+ role: 'user',
377
+ status: 'active',
378
+ })
379
+ await db.collections.users.insert({
380
+ id: '2',
381
+ name: 'Alice',
382
+ email: 'alice@example.com',
383
+ role: 'admin',
384
+ status: 'active',
385
+ })
386
+ await db.collections.users.insert({
387
+ id: '3',
388
+ name: 'Bob',
389
+ email: 'bob@example.com',
390
+ role: 'user',
391
+ status: 'active',
392
+ })
393
+ })
394
+
395
+ it('sorts data with initial orderBy', async () => {
396
+ const { result } = renderHook(
397
+ () => useReactiveData<User>({
398
+ collection: 'users',
399
+ orderBy: { name: 'asc' },
400
+ }),
401
+ { wrapper: createWrapper(db) }
402
+ )
403
+
404
+ await waitFor(() => {
405
+ expect(result.current.isLoading).toBe(false)
406
+ })
407
+
408
+ expect(result.current.data[0].name).toBe('Alice')
409
+ expect(result.current.data[1].name).toBe('Bob')
410
+ expect(result.current.data[2].name).toBe('Charlie')
411
+ })
412
+
413
+ it('toggles sort direction with setSort', async () => {
414
+ const { result } = renderHook(
415
+ () => useReactiveData<User>({
416
+ collection: 'users',
417
+ orderBy: { name: 'asc' },
418
+ }),
419
+ { wrapper: createWrapper(db) }
420
+ )
421
+
422
+ await waitFor(() => {
423
+ expect(result.current.isLoading).toBe(false)
424
+ })
425
+
426
+ expect(result.current.sort.field).toBe('name')
427
+ expect(result.current.sort.direction).toBe('asc')
428
+
429
+ // Toggle to descending
430
+ act(() => {
431
+ result.current.setSort('name')
432
+ })
433
+
434
+ expect(result.current.sort.direction).toBe('desc')
435
+
436
+ // Toggle back to ascending
437
+ act(() => {
438
+ result.current.setSort('name')
439
+ })
440
+
441
+ expect(result.current.sort.direction).toBe('asc')
442
+ })
443
+
444
+ it('changes sort field with setSort', async () => {
445
+ const { result } = renderHook(
446
+ () => useReactiveData<User>({
447
+ collection: 'users',
448
+ orderBy: { name: 'asc' },
449
+ }),
450
+ { wrapper: createWrapper(db) }
451
+ )
452
+
453
+ await waitFor(() => {
454
+ expect(result.current.isLoading).toBe(false)
455
+ })
456
+
457
+ act(() => {
458
+ result.current.setSort('email')
459
+ })
460
+
461
+ expect(result.current.sort.field).toBe('email')
462
+ expect(result.current.sort.direction).toBe('asc')
463
+ })
464
+ })
465
+
466
+ describe('selection', () => {
467
+ beforeEach(async () => {
468
+ await db.collections.users.insert({
469
+ id: '1',
470
+ name: 'Alice',
471
+ email: 'alice@example.com',
472
+ role: 'admin',
473
+ status: 'active',
474
+ })
475
+ await db.collections.users.insert({
476
+ id: '2',
477
+ name: 'Bob',
478
+ email: 'bob@example.com',
479
+ role: 'user',
480
+ status: 'active',
481
+ })
482
+ })
483
+
484
+ it('supports single selection', async () => {
485
+ const { result } = renderHook(
486
+ () => useReactiveData<User>({
487
+ collection: 'users',
488
+ selectable: 'single',
489
+ }),
490
+ { wrapper: createWrapper(db) }
491
+ )
492
+
493
+ await waitFor(() => {
494
+ expect(result.current.isLoading).toBe(false)
495
+ })
496
+
497
+ expect(result.current.selection.mode).toBe('single')
498
+ expect(result.current.selection.selected.size).toBe(0)
499
+
500
+ // Select first item
501
+ act(() => {
502
+ result.current.toggleSelect('1')
503
+ })
504
+
505
+ expect(result.current.selection.selected.has('1')).toBe(true)
506
+ expect(result.current.selection.selected.size).toBe(1)
507
+
508
+ // Select second item (should replace)
509
+ act(() => {
510
+ result.current.toggleSelect('2')
511
+ })
512
+
513
+ expect(result.current.selection.selected.has('2')).toBe(true)
514
+ expect(result.current.selection.selected.has('1')).toBe(false)
515
+ expect(result.current.selection.selected.size).toBe(1)
516
+ })
517
+
518
+ it('supports multi selection', async () => {
519
+ const { result } = renderHook(
520
+ () => useReactiveData<User>({
521
+ collection: 'users',
522
+ selectable: 'multi',
523
+ }),
524
+ { wrapper: createWrapper(db) }
525
+ )
526
+
527
+ await waitFor(() => {
528
+ expect(result.current.isLoading).toBe(false)
529
+ })
530
+
531
+ expect(result.current.selection.mode).toBe('multi')
532
+
533
+ // Select first item
534
+ act(() => {
535
+ result.current.toggleSelect('1')
536
+ })
537
+
538
+ expect(result.current.selection.selected.has('1')).toBe(true)
539
+
540
+ // Select second item (should add)
541
+ act(() => {
542
+ result.current.toggleSelect('2')
543
+ })
544
+
545
+ expect(result.current.selection.selected.has('1')).toBe(true)
546
+ expect(result.current.selection.selected.has('2')).toBe(true)
547
+ expect(result.current.selection.selected.size).toBe(2)
548
+ })
549
+
550
+ it('clears selection with clearSelection', async () => {
551
+ const { result } = renderHook(
552
+ () => useReactiveData<User>({
553
+ collection: 'users',
554
+ selectable: 'multi',
555
+ }),
556
+ { wrapper: createWrapper(db) }
557
+ )
558
+
559
+ await waitFor(() => {
560
+ expect(result.current.isLoading).toBe(false)
561
+ })
562
+
563
+ act(() => {
564
+ result.current.toggleSelect('1')
565
+ result.current.toggleSelect('2')
566
+ })
567
+
568
+ expect(result.current.selection.selected.size).toBe(2)
569
+
570
+ act(() => {
571
+ result.current.clearSelection()
572
+ })
573
+
574
+ expect(result.current.selection.selected.size).toBe(0)
575
+ })
576
+
577
+ it('selects all with selectAll', async () => {
578
+ const { result } = renderHook(
579
+ () => useReactiveData<User>({
580
+ collection: 'users',
581
+ selectable: 'multi',
582
+ }),
583
+ { wrapper: createWrapper(db) }
584
+ )
585
+
586
+ await waitFor(() => {
587
+ expect(result.current.isLoading).toBe(false)
588
+ })
589
+
590
+ act(() => {
591
+ result.current.selectAll()
592
+ })
593
+
594
+ expect(result.current.selection.selected.size).toBe(2)
595
+ expect(result.current.selection.selected.has('1')).toBe(true)
596
+ expect(result.current.selection.selected.has('2')).toBe(true)
597
+ })
598
+ })
599
+ })
600
+
601
+ // ============================================================================
602
+ // useReactiveTable Tests
603
+ // ============================================================================
604
+
605
+ describe('useReactiveTable', () => {
606
+ let db: DB
607
+
608
+ beforeEach(() => {
609
+ db = createTestDB()
610
+ })
611
+
612
+ afterEach(async () => {
613
+ await db.clear()
614
+ db.close()
615
+ })
616
+
617
+ it('works as a specialized useReactiveData for tables', async () => {
618
+ await db.collections.users.insert({
619
+ id: '1',
620
+ name: 'Alice',
621
+ email: 'alice@example.com',
622
+ role: 'admin',
623
+ status: 'active',
624
+ })
625
+
626
+ const { result } = renderHook(
627
+ () => useReactiveTable<User>({
628
+ collection: 'users',
629
+ columns: [
630
+ { key: 'name', header: 'Name', sortable: true },
631
+ { key: 'email', header: 'Email' },
632
+ ],
633
+ }),
634
+ { wrapper: createWrapper(db) }
635
+ )
636
+
637
+ await waitFor(() => {
638
+ expect(result.current.isLoading).toBe(false)
639
+ })
640
+
641
+ expect(result.current.data).toHaveLength(1)
642
+ expect(result.current.data[0].name).toBe('Alice')
643
+ })
644
+ })
645
+
646
+ // ============================================================================
647
+ // useReactiveList Tests
648
+ // ============================================================================
649
+
650
+ describe('useReactiveList', () => {
651
+ let db: DB
652
+
653
+ beforeEach(() => {
654
+ db = createTestDB()
655
+ })
656
+
657
+ afterEach(async () => {
658
+ await db.clear()
659
+ db.close()
660
+ })
661
+
662
+ it('works as a specialized useReactiveData for lists', async () => {
663
+ await db.collections.users.insert({
664
+ id: '1',
665
+ name: 'Alice',
666
+ email: 'alice@example.com',
667
+ role: 'admin',
668
+ status: 'active',
669
+ })
670
+
671
+ const { result } = renderHook(
672
+ () => useReactiveList<User>({
673
+ collection: 'users',
674
+ labelField: 'name',
675
+ }),
676
+ { wrapper: createWrapper(db) }
677
+ )
678
+
679
+ await waitFor(() => {
680
+ expect(result.current.isLoading).toBe(false)
681
+ })
682
+
683
+ expect(result.current.data).toHaveLength(1)
684
+ expect(result.current.data[0].name).toBe('Alice')
685
+ })
686
+ })
687
+
688
+ // ============================================================================
689
+ // useReactiveMetrics Tests
690
+ // ============================================================================
691
+
692
+ describe('useReactiveMetrics', () => {
693
+ let db: DB
694
+
695
+ beforeEach(() => {
696
+ db = createTestDB()
697
+ })
698
+
699
+ afterEach(async () => {
700
+ await db.clear()
701
+ db.close()
702
+ })
703
+
704
+ it('computes count metric', async () => {
705
+ await db.collections.orders.insert({
706
+ id: '1',
707
+ userId: 'u1',
708
+ total: 100,
709
+ status: 'completed',
710
+ createdAt: new Date().toISOString(),
711
+ })
712
+ await db.collections.orders.insert({
713
+ id: '2',
714
+ userId: 'u2',
715
+ total: 200,
716
+ status: 'completed',
717
+ createdAt: new Date().toISOString(),
718
+ })
719
+
720
+ const { result } = renderHook(
721
+ () => useReactiveMetrics<Order>({
722
+ collection: 'orders',
723
+ metrics: [
724
+ {
725
+ key: 'total-orders',
726
+ label: 'Total Orders',
727
+ field: 'id',
728
+ aggregate: 'count',
729
+ },
730
+ ],
731
+ }),
732
+ { wrapper: createWrapper(db) }
733
+ )
734
+
735
+ await waitFor(() => {
736
+ expect(result.current.isLoading).toBe(false)
737
+ })
738
+
739
+ expect(result.current.metrics).toHaveLength(1)
740
+ expect(result.current.metrics[0].label).toBe('Total Orders')
741
+ expect(result.current.metrics[0].value).toBe(2)
742
+ })
743
+
744
+ it('computes sum metric', async () => {
745
+ await db.collections.orders.insert({
746
+ id: '1',
747
+ userId: 'u1',
748
+ total: 100,
749
+ status: 'completed',
750
+ createdAt: new Date().toISOString(),
751
+ })
752
+ await db.collections.orders.insert({
753
+ id: '2',
754
+ userId: 'u2',
755
+ total: 200,
756
+ status: 'completed',
757
+ createdAt: new Date().toISOString(),
758
+ })
759
+
760
+ const { result } = renderHook(
761
+ () => useReactiveMetrics<Order>({
762
+ collection: 'orders',
763
+ metrics: [
764
+ {
765
+ key: 'revenue',
766
+ label: 'Revenue',
767
+ field: 'total',
768
+ aggregate: 'sum',
769
+ },
770
+ ],
771
+ }),
772
+ { wrapper: createWrapper(db) }
773
+ )
774
+
775
+ await waitFor(() => {
776
+ expect(result.current.isLoading).toBe(false)
777
+ })
778
+
779
+ expect(result.current.metrics[0].label).toBe('Revenue')
780
+ expect(result.current.metrics[0].value).toBe(300)
781
+ })
782
+
783
+ it('computes avg metric', async () => {
784
+ await db.collections.orders.insert({
785
+ id: '1',
786
+ userId: 'u1',
787
+ total: 100,
788
+ status: 'completed',
789
+ createdAt: new Date().toISOString(),
790
+ })
791
+ await db.collections.orders.insert({
792
+ id: '2',
793
+ userId: 'u2',
794
+ total: 200,
795
+ status: 'completed',
796
+ createdAt: new Date().toISOString(),
797
+ })
798
+
799
+ const { result } = renderHook(
800
+ () => useReactiveMetrics<Order>({
801
+ collection: 'orders',
802
+ metrics: [
803
+ {
804
+ key: 'avg-order',
805
+ label: 'Avg Order',
806
+ field: 'total',
807
+ aggregate: 'avg',
808
+ },
809
+ ],
810
+ }),
811
+ { wrapper: createWrapper(db) }
812
+ )
813
+
814
+ await waitFor(() => {
815
+ expect(result.current.isLoading).toBe(false)
816
+ })
817
+
818
+ expect(result.current.metrics[0].label).toBe('Avg Order')
819
+ expect(result.current.metrics[0].value).toBe(150)
820
+ })
821
+
822
+ it('updates reactively when data changes', async () => {
823
+ const { result } = renderHook(
824
+ () => useReactiveMetrics<Order>({
825
+ collection: 'orders',
826
+ metrics: [
827
+ {
828
+ key: 'total-orders',
829
+ label: 'Total Orders',
830
+ field: 'id',
831
+ aggregate: 'count',
832
+ },
833
+ ],
834
+ }),
835
+ { wrapper: createWrapper(db) }
836
+ )
837
+
838
+ await waitFor(() => {
839
+ expect(result.current.isLoading).toBe(false)
840
+ })
841
+
842
+ expect(result.current.metrics[0].value).toBe(0)
843
+
844
+ // Add data
845
+ await act(async () => {
846
+ await db.collections.orders.insert({
847
+ id: '1',
848
+ userId: 'u1',
849
+ total: 100,
850
+ status: 'completed',
851
+ createdAt: new Date().toISOString(),
852
+ })
853
+ })
854
+
855
+ await waitFor(() => {
856
+ expect(result.current.metrics[0].value).toBe(1)
857
+ })
858
+ })
859
+
860
+ it('formats currency values', async () => {
861
+ await db.collections.orders.insert({
862
+ id: '1',
863
+ userId: 'u1',
864
+ total: 1000,
865
+ status: 'completed',
866
+ createdAt: new Date().toISOString(),
867
+ })
868
+
869
+ const { result } = renderHook(
870
+ () => useReactiveMetrics<Order>({
871
+ collection: 'orders',
872
+ metrics: [
873
+ {
874
+ key: 'revenue',
875
+ label: 'Revenue',
876
+ field: 'total',
877
+ aggregate: 'sum',
878
+ format: 'currency',
879
+ },
880
+ ],
881
+ }),
882
+ { wrapper: createWrapper(db) }
883
+ )
884
+
885
+ await waitFor(() => {
886
+ expect(result.current.isLoading).toBe(false)
887
+ })
888
+
889
+ expect(result.current.metrics[0].value).toBe('$1,000')
890
+ })
891
+ })
892
+
893
+ // ============================================================================
894
+ // useReactiveCard Tests
895
+ // ============================================================================
896
+
897
+ describe('useReactiveCard', () => {
898
+ let db: DB
899
+
900
+ beforeEach(() => {
901
+ db = createTestDB()
902
+ })
903
+
904
+ afterEach(async () => {
905
+ await db.clear()
906
+ db.close()
907
+ })
908
+
909
+ it('fetches single record', async () => {
910
+ await db.collections.users.insert({
911
+ id: '1',
912
+ name: 'Alice',
913
+ email: 'alice@example.com',
914
+ role: 'admin',
915
+ status: 'active',
916
+ })
917
+
918
+ const { result } = renderHook(
919
+ () => useReactiveCard<User>({
920
+ collection: 'users',
921
+ where: { id: '1' },
922
+ }),
923
+ { wrapper: createWrapper(db) }
924
+ )
925
+
926
+ await waitFor(() => {
927
+ expect(result.current.isLoading).toBe(false)
928
+ })
929
+
930
+ expect(result.current.data).not.toBeNull()
931
+ expect(result.current.data?.name).toBe('Alice')
932
+ })
933
+
934
+ it('returns null for non-existent record', async () => {
935
+ const { result } = renderHook(
936
+ () => useReactiveCard<User>({
937
+ collection: 'users',
938
+ where: { id: 'nonexistent' },
939
+ }),
940
+ { wrapper: createWrapper(db) }
941
+ )
942
+
943
+ await waitFor(() => {
944
+ expect(result.current.isLoading).toBe(false)
945
+ })
946
+
947
+ expect(result.current.data).toBeNull()
948
+ })
949
+
950
+ it('updates record via update function', async () => {
951
+ await db.collections.users.insert({
952
+ id: '1',
953
+ name: 'Alice',
954
+ email: 'alice@example.com',
955
+ role: 'admin',
956
+ status: 'active',
957
+ })
958
+
959
+ const { result } = renderHook(
960
+ () => useReactiveCard<User>({
961
+ collection: 'users',
962
+ where: { id: '1' },
963
+ }),
964
+ { wrapper: createWrapper(db) }
965
+ )
966
+
967
+ await waitFor(() => {
968
+ expect(result.current.data?.name).toBe('Alice')
969
+ })
970
+
971
+ await act(async () => {
972
+ await result.current.update({ name: 'Alicia' })
973
+ })
974
+
975
+ await waitFor(() => {
976
+ expect(result.current.data?.name).toBe('Alicia')
977
+ })
978
+ })
979
+
980
+ it('updates reactively when record changes externally', async () => {
981
+ await db.collections.users.insert({
982
+ id: '1',
983
+ name: 'Alice',
984
+ email: 'alice@example.com',
985
+ role: 'admin',
986
+ status: 'active',
987
+ })
988
+
989
+ const { result } = renderHook(
990
+ () => useReactiveCard<User>({
991
+ collection: 'users',
992
+ where: { id: '1' },
993
+ }),
994
+ { wrapper: createWrapper(db) }
995
+ )
996
+
997
+ await waitFor(() => {
998
+ expect(result.current.data?.name).toBe('Alice')
999
+ })
1000
+
1001
+ // Update externally
1002
+ await act(async () => {
1003
+ await db.collections.users.update({ id: '1' }, { name: 'Updated' })
1004
+ })
1005
+
1006
+ await waitFor(() => {
1007
+ expect(result.current.data?.name).toBe('Updated')
1008
+ })
1009
+ })
1010
+ })