@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.
- package/README.md +571 -0
- package/dist/ansi-css-Sk5mWtdK.d.ts +119 -0
- package/dist/ansi-css-V6JIHGsM.d.ts +119 -0
- package/dist/ansi-css-_3eSEU9d.d.ts +119 -0
- package/dist/chunk-3EFDH7PK.js +5235 -0
- package/dist/chunk-3RG5ZIWI.js +10 -0
- package/dist/chunk-3X5IR6WE.js +884 -0
- package/dist/chunk-4FV5ZDCE.js +5236 -0
- package/dist/chunk-4OVMSF2J.js +243 -0
- package/dist/chunk-63FEETIS.js +4048 -0
- package/dist/chunk-B43KP7XJ.js +884 -0
- package/dist/chunk-BMTJXWUV.js +655 -0
- package/dist/chunk-C3SVH4N7.js +882 -0
- package/dist/chunk-EVWR7Y47.js +874 -0
- package/dist/chunk-F6A5VWUC.js +1285 -0
- package/dist/chunk-FD7KW7GE.js +882 -0
- package/dist/chunk-GBQ6UD6I.js +655 -0
- package/dist/chunk-GMDD3M6U.js +5227 -0
- package/dist/chunk-JBHRXOXM.js +1058 -0
- package/dist/chunk-JFOO3EYO.js +1182 -0
- package/dist/chunk-JQ5H3WXL.js +1291 -0
- package/dist/chunk-JQD5NASE.js +234 -0
- package/dist/chunk-KRHJP5R7.js +592 -0
- package/dist/chunk-KWF6WVJE.js +962 -0
- package/dist/chunk-LHYQVN3H.js +1038 -0
- package/dist/chunk-M3TLQLGC.js +1032 -0
- package/dist/chunk-MVW4Q5OP.js +240 -0
- package/dist/chunk-NXCZSWLU.js +1294 -0
- package/dist/chunk-O25TNRO6.js +607 -0
- package/dist/chunk-PNECDA2I.js +884 -0
- package/dist/chunk-QIHWRLJR.js +962 -0
- package/dist/chunk-QW5YMQ7K.js +882 -0
- package/dist/chunk-R5U7XKVJ.js +16 -0
- package/dist/chunk-RP2MVQLR.js +962 -0
- package/dist/chunk-TP6RXGXA.js +1087 -0
- package/dist/chunk-TQQSTITZ.js +655 -0
- package/dist/chunk-X24GWXQV.js +1281 -0
- package/dist/components/index.d.ts +802 -0
- package/dist/components/index.js +149 -0
- package/dist/data/index.d.ts +2554 -0
- package/dist/data/index.js +51 -0
- package/dist/forms/index.d.ts +1596 -0
- package/dist/forms/index.js +464 -0
- package/dist/index-CQRFZntR.d.ts +867 -0
- package/dist/index.d.ts +579 -0
- package/dist/index.js +786 -0
- package/dist/interactive-D0JkWosD.d.ts +217 -0
- package/dist/keyboard/index.d.ts +2 -0
- package/dist/keyboard/index.js +43 -0
- package/dist/renderers/index.d.ts +546 -0
- package/dist/renderers/index.js +2157 -0
- package/dist/storybook/index.d.ts +396 -0
- package/dist/storybook/index.js +641 -0
- package/dist/theme/index.d.ts +1339 -0
- package/dist/theme/index.js +123 -0
- package/dist/types-Bxu5PAgA.d.ts +710 -0
- package/dist/types-CIlop5Ji.d.ts +701 -0
- package/dist/types-Ca8p_p5X.d.ts +710 -0
- package/package.json +90 -0
- package/src/__tests__/components/data/card.test.ts +458 -0
- package/src/__tests__/components/data/list.test.ts +473 -0
- package/src/__tests__/components/data/metrics.test.ts +541 -0
- package/src/__tests__/components/data/table.test.ts +448 -0
- package/src/__tests__/components/input/field.test.ts +555 -0
- package/src/__tests__/components/input/form.test.ts +870 -0
- package/src/__tests__/components/input/search.test.ts +1238 -0
- package/src/__tests__/components/input/select.test.ts +658 -0
- package/src/__tests__/components/navigation/breadcrumb.test.ts +923 -0
- package/src/__tests__/components/navigation/command-palette.test.ts +1095 -0
- package/src/__tests__/components/navigation/sidebar.test.ts +1018 -0
- package/src/__tests__/components/navigation/tabs.test.ts +995 -0
- package/src/__tests__/components.test.tsx +1197 -0
- package/src/__tests__/core/compiler.test.ts +986 -0
- package/src/__tests__/core/parser.test.ts +785 -0
- package/src/__tests__/core/tier-switcher.test.ts +1103 -0
- package/src/__tests__/core/types.test.ts +1398 -0
- package/src/__tests__/data/collections.test.ts +1337 -0
- package/src/__tests__/data/db.test.ts +1265 -0
- package/src/__tests__/data/reactive.test.ts +1010 -0
- package/src/__tests__/data/sync.test.ts +1614 -0
- package/src/__tests__/errors.test.ts +660 -0
- package/src/__tests__/forms/integration.test.ts +444 -0
- package/src/__tests__/integration.test.ts +905 -0
- package/src/__tests__/keyboard.test.ts +1791 -0
- package/src/__tests__/renderer.test.ts +489 -0
- package/src/__tests__/renderers/ansi-css.test.ts +948 -0
- package/src/__tests__/renderers/ansi.test.ts +1366 -0
- package/src/__tests__/renderers/ascii.test.ts +1360 -0
- package/src/__tests__/renderers/interactive.test.ts +2353 -0
- package/src/__tests__/renderers/markdown.test.ts +1483 -0
- package/src/__tests__/renderers/text.test.ts +1369 -0
- package/src/__tests__/renderers/unicode.test.ts +1307 -0
- package/src/__tests__/theme.test.ts +639 -0
- package/src/__tests__/utils/assertions.ts +685 -0
- package/src/__tests__/utils/index.ts +115 -0
- package/src/__tests__/utils/test-renderer.ts +381 -0
- package/src/__tests__/utils/utils.test.ts +560 -0
- package/src/components/containers/card.ts +56 -0
- package/src/components/containers/dialog.ts +53 -0
- package/src/components/containers/index.ts +9 -0
- package/src/components/containers/panel.ts +59 -0
- package/src/components/feedback/badge.ts +40 -0
- package/src/components/feedback/index.ts +8 -0
- package/src/components/feedback/spinner.ts +23 -0
- package/src/components/helpers.ts +81 -0
- package/src/components/index.ts +153 -0
- package/src/components/layout/breadcrumb.ts +31 -0
- package/src/components/layout/index.ts +10 -0
- package/src/components/layout/list.ts +29 -0
- package/src/components/layout/sidebar.ts +79 -0
- package/src/components/layout/table.ts +62 -0
- package/src/components/primitives/box.ts +95 -0
- package/src/components/primitives/button.ts +54 -0
- package/src/components/primitives/index.ts +11 -0
- package/src/components/primitives/input.ts +88 -0
- package/src/components/primitives/select.ts +97 -0
- package/src/components/primitives/text.ts +60 -0
- package/src/components/render.ts +155 -0
- package/src/components/templates/app.ts +43 -0
- package/src/components/templates/index.ts +8 -0
- package/src/components/templates/site.ts +54 -0
- package/src/components/types.ts +777 -0
- package/src/core/compiler.ts +718 -0
- package/src/core/parser.ts +127 -0
- package/src/core/tier-switcher.ts +607 -0
- package/src/core/types.ts +672 -0
- package/src/data/collection.ts +316 -0
- package/src/data/collections.ts +50 -0
- package/src/data/context.tsx +174 -0
- package/src/data/db.ts +127 -0
- package/src/data/hooks.ts +532 -0
- package/src/data/index.ts +138 -0
- package/src/data/reactive.ts +1225 -0
- package/src/data/saas-collections.ts +375 -0
- package/src/data/sync.ts +1213 -0
- package/src/data/types.ts +660 -0
- package/src/forms/converters.ts +512 -0
- package/src/forms/index.ts +133 -0
- package/src/forms/schemas.ts +403 -0
- package/src/forms/types.ts +476 -0
- package/src/index.ts +542 -0
- package/src/keyboard/focus.ts +748 -0
- package/src/keyboard/index.ts +96 -0
- package/src/keyboard/integration.ts +371 -0
- package/src/keyboard/manager.ts +377 -0
- package/src/keyboard/presets.ts +90 -0
- package/src/renderers/ansi-css.ts +576 -0
- package/src/renderers/ansi.ts +802 -0
- package/src/renderers/ascii.ts +680 -0
- package/src/renderers/breadcrumb.ts +480 -0
- package/src/renderers/command-palette.ts +802 -0
- package/src/renderers/components/field.ts +210 -0
- package/src/renderers/components/form.ts +327 -0
- package/src/renderers/components/index.ts +21 -0
- package/src/renderers/components/search.ts +449 -0
- package/src/renderers/components/select.ts +222 -0
- package/src/renderers/index.ts +101 -0
- package/src/renderers/interactive/component-handlers.ts +622 -0
- package/src/renderers/interactive/cursor-manager.ts +147 -0
- package/src/renderers/interactive/focus-manager.ts +279 -0
- package/src/renderers/interactive/index.ts +661 -0
- package/src/renderers/interactive/input-handler.ts +164 -0
- package/src/renderers/interactive/keyboard-handler.ts +212 -0
- package/src/renderers/interactive/mouse-handler.ts +167 -0
- package/src/renderers/interactive/state-manager.ts +109 -0
- package/src/renderers/interactive/types.ts +338 -0
- package/src/renderers/interactive-string.ts +299 -0
- package/src/renderers/interactive.ts +59 -0
- package/src/renderers/markdown.ts +950 -0
- package/src/renderers/sidebar.ts +549 -0
- package/src/renderers/tabs.ts +682 -0
- package/src/renderers/text.ts +791 -0
- package/src/renderers/unicode.ts +917 -0
- package/src/renderers/utils.ts +942 -0
- package/src/router/adapters.ts +383 -0
- package/src/router/types.ts +140 -0
- package/src/router/utils.ts +452 -0
- package/src/schemas.ts +205 -0
- package/src/storybook/index.ts +91 -0
- package/src/storybook/interactive-decorator.tsx +659 -0
- package/src/storybook/keyboard-simulator.ts +501 -0
- package/src/theme/ansi-codes.ts +80 -0
- package/src/theme/box-drawing.ts +132 -0
- package/src/theme/color-convert.ts +254 -0
- package/src/theme/color-support.ts +321 -0
- package/src/theme/index.ts +134 -0
- package/src/theme/strip-ansi.ts +50 -0
- package/src/theme/tailwind-map.ts +469 -0
- package/src/theme/text-styles.ts +206 -0
- package/src/theme/theme-system.ts +568 -0
- package/src/types.ts +103 -0
|
@@ -0,0 +1,1265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mdxui/terminal TanStack DB Integration Tests
|
|
3
|
+
*
|
|
4
|
+
* TDD RED Phase: These tests define the contract for TanStack DB integration.
|
|
5
|
+
* All tests should FAIL initially because the implementation doesn't exist yet.
|
|
6
|
+
*
|
|
7
|
+
* The data layer provides:
|
|
8
|
+
* - createDB() - Database instance creation
|
|
9
|
+
* - createCollection() - Typed collection with Zod schema validation
|
|
10
|
+
* - useQuery() - Reactive data queries with filtering and sorting
|
|
11
|
+
* - useMutation() - Mutations with optimistic updates
|
|
12
|
+
*/
|
|
13
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
14
|
+
import { z } from 'zod'
|
|
15
|
+
import { renderHook, act, waitFor } from '@testing-library/react'
|
|
16
|
+
import * as React from 'react'
|
|
17
|
+
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// Test Schemas
|
|
20
|
+
// ============================================================================
|
|
21
|
+
|
|
22
|
+
const UserSchema = z.object({
|
|
23
|
+
id: z.string(),
|
|
24
|
+
name: z.string(),
|
|
25
|
+
email: z.string().email(),
|
|
26
|
+
age: z.number().optional(),
|
|
27
|
+
role: z.enum(['admin', 'user', 'guest']),
|
|
28
|
+
createdAt: z.date(),
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const TodoSchema = z.object({
|
|
32
|
+
id: z.string(),
|
|
33
|
+
title: z.string(),
|
|
34
|
+
completed: z.boolean(),
|
|
35
|
+
priority: z.number().min(1).max(5),
|
|
36
|
+
userId: z.string(),
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
type User = z.infer<typeof UserSchema>
|
|
40
|
+
type Todo = z.infer<typeof TodoSchema>
|
|
41
|
+
|
|
42
|
+
// ============================================================================
|
|
43
|
+
// createDB() Tests
|
|
44
|
+
// ============================================================================
|
|
45
|
+
|
|
46
|
+
describe('@mdxui/terminal data layer - createDB', () => {
|
|
47
|
+
it('creates a database instance', async () => {
|
|
48
|
+
const { createDB } = await import('@mdxui/terminal')
|
|
49
|
+
|
|
50
|
+
const db = createDB()
|
|
51
|
+
|
|
52
|
+
expect(db).toBeDefined()
|
|
53
|
+
expect(typeof db).toBe('object')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('accepts collections array configuration', async () => {
|
|
57
|
+
const { createDB, createCollection } = await import('@mdxui/terminal')
|
|
58
|
+
|
|
59
|
+
const usersCollection = createCollection({
|
|
60
|
+
name: 'users',
|
|
61
|
+
schema: UserSchema,
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const todosCollection = createCollection({
|
|
65
|
+
name: 'todos',
|
|
66
|
+
schema: TodoSchema,
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const db = createDB({
|
|
70
|
+
collections: [usersCollection, todosCollection],
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
expect(db).toBeDefined()
|
|
74
|
+
expect(db.collections).toBeDefined()
|
|
75
|
+
expect(db.collections.users).toBeDefined()
|
|
76
|
+
expect(db.collections.todos).toBeDefined()
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('accepts sync adapter configuration', async () => {
|
|
80
|
+
const { createDB } = await import('@mdxui/terminal')
|
|
81
|
+
|
|
82
|
+
const mockSyncAdapter = {
|
|
83
|
+
push: vi.fn(),
|
|
84
|
+
pull: vi.fn(),
|
|
85
|
+
subscribe: vi.fn(),
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const db = createDB({
|
|
89
|
+
sync: mockSyncAdapter,
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
expect(db).toBeDefined()
|
|
93
|
+
expect(db.sync).toBe(mockSyncAdapter)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('returns database with standard methods', async () => {
|
|
97
|
+
const { createDB } = await import('@mdxui/terminal')
|
|
98
|
+
|
|
99
|
+
const db = createDB()
|
|
100
|
+
|
|
101
|
+
expect(typeof db.close).toBe('function')
|
|
102
|
+
expect(typeof db.clear).toBe('function')
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
// ============================================================================
|
|
107
|
+
// createCollection() Tests
|
|
108
|
+
// ============================================================================
|
|
109
|
+
|
|
110
|
+
describe('@mdxui/terminal data layer - createCollection', () => {
|
|
111
|
+
it('creates a typed collection with name and schema', async () => {
|
|
112
|
+
const { createCollection } = await import('@mdxui/terminal')
|
|
113
|
+
|
|
114
|
+
const collection = createCollection({
|
|
115
|
+
name: 'users',
|
|
116
|
+
schema: UserSchema,
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
expect(collection).toBeDefined()
|
|
120
|
+
expect(collection.name).toBe('users')
|
|
121
|
+
expect(collection.schema).toBe(UserSchema)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('validates data against Zod schema on insert', async () => {
|
|
125
|
+
const { createDB, createCollection } = await import('@mdxui/terminal')
|
|
126
|
+
|
|
127
|
+
const usersCollection = createCollection({
|
|
128
|
+
name: 'users',
|
|
129
|
+
schema: UserSchema,
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
const db = createDB({
|
|
133
|
+
collections: [usersCollection],
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
const validUser: User = {
|
|
137
|
+
id: '1',
|
|
138
|
+
name: 'John Doe',
|
|
139
|
+
email: 'john@example.com',
|
|
140
|
+
role: 'user',
|
|
141
|
+
createdAt: new Date(),
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Valid data should not throw
|
|
145
|
+
await expect(db.collections.users.insert(validUser)).resolves.not.toThrow()
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('rejects invalid data on insert', async () => {
|
|
149
|
+
const { createDB, createCollection } = await import('@mdxui/terminal')
|
|
150
|
+
|
|
151
|
+
const usersCollection = createCollection({
|
|
152
|
+
name: 'users',
|
|
153
|
+
schema: UserSchema,
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
const db = createDB({
|
|
157
|
+
collections: [usersCollection],
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
const invalidUser = {
|
|
161
|
+
id: '1',
|
|
162
|
+
name: 'John Doe',
|
|
163
|
+
email: 'not-an-email', // Invalid email
|
|
164
|
+
role: 'user',
|
|
165
|
+
createdAt: new Date(),
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
await expect(db.collections.users.insert(invalidUser)).rejects.toThrow()
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('returns collection with CRUD methods', async () => {
|
|
172
|
+
const { createCollection } = await import('@mdxui/terminal')
|
|
173
|
+
|
|
174
|
+
const collection = createCollection({
|
|
175
|
+
name: 'users',
|
|
176
|
+
schema: UserSchema,
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
expect(typeof collection.insert).toBe('function')
|
|
180
|
+
expect(typeof collection.update).toBe('function')
|
|
181
|
+
expect(typeof collection.delete).toBe('function')
|
|
182
|
+
expect(typeof collection.findOne).toBe('function')
|
|
183
|
+
expect(typeof collection.findMany).toBe('function')
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('supports primary key configuration', async () => {
|
|
187
|
+
const { createCollection } = await import('@mdxui/terminal')
|
|
188
|
+
|
|
189
|
+
const collection = createCollection({
|
|
190
|
+
name: 'users',
|
|
191
|
+
schema: UserSchema,
|
|
192
|
+
primaryKey: 'id',
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
expect(collection.primaryKey).toBe('id')
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('supports optional indexes configuration', async () => {
|
|
199
|
+
const { createCollection } = await import('@mdxui/terminal')
|
|
200
|
+
|
|
201
|
+
const collection = createCollection({
|
|
202
|
+
name: 'users',
|
|
203
|
+
schema: UserSchema,
|
|
204
|
+
indexes: ['email', 'role'],
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
expect(collection.indexes).toContain('email')
|
|
208
|
+
expect(collection.indexes).toContain('role')
|
|
209
|
+
})
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
// ============================================================================
|
|
213
|
+
// useQuery() Hook Tests
|
|
214
|
+
// ============================================================================
|
|
215
|
+
|
|
216
|
+
describe('@mdxui/terminal data layer - useQuery', () => {
|
|
217
|
+
// Provider wrapper for hooks
|
|
218
|
+
let wrapper: React.FC<{ children: React.ReactNode }>
|
|
219
|
+
|
|
220
|
+
beforeEach(async () => {
|
|
221
|
+
const { createDB, createCollection, DBProvider } = await import('@mdxui/terminal')
|
|
222
|
+
|
|
223
|
+
const usersCollection = createCollection({
|
|
224
|
+
name: 'users',
|
|
225
|
+
schema: UserSchema,
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
const db = createDB({
|
|
229
|
+
collections: [usersCollection],
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
// Seed test data
|
|
233
|
+
await db.collections.users.insert({
|
|
234
|
+
id: '1',
|
|
235
|
+
name: 'Alice',
|
|
236
|
+
email: 'alice@example.com',
|
|
237
|
+
age: 30,
|
|
238
|
+
role: 'admin',
|
|
239
|
+
createdAt: new Date('2024-01-01'),
|
|
240
|
+
})
|
|
241
|
+
await db.collections.users.insert({
|
|
242
|
+
id: '2',
|
|
243
|
+
name: 'Bob',
|
|
244
|
+
email: 'bob@example.com',
|
|
245
|
+
age: 25,
|
|
246
|
+
role: 'user',
|
|
247
|
+
createdAt: new Date('2024-02-01'),
|
|
248
|
+
})
|
|
249
|
+
await db.collections.users.insert({
|
|
250
|
+
id: '3',
|
|
251
|
+
name: 'Charlie',
|
|
252
|
+
email: 'charlie@example.com',
|
|
253
|
+
age: 35,
|
|
254
|
+
role: 'user',
|
|
255
|
+
createdAt: new Date('2024-03-01'),
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
wrapper = ({ children }) =>
|
|
259
|
+
React.createElement(DBProvider, { db }, children)
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
it('returns reactive data from collection', async () => {
|
|
263
|
+
const { useQuery } = await import('@mdxui/terminal')
|
|
264
|
+
|
|
265
|
+
const { result } = renderHook(
|
|
266
|
+
() =>
|
|
267
|
+
useQuery({
|
|
268
|
+
from: 'users',
|
|
269
|
+
}),
|
|
270
|
+
{ wrapper }
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
await waitFor(() => {
|
|
274
|
+
expect(result.current.data).toBeDefined()
|
|
275
|
+
expect(result.current.data).toHaveLength(3)
|
|
276
|
+
})
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
it('supports from collection parameter', async () => {
|
|
280
|
+
const { useQuery } = await import('@mdxui/terminal')
|
|
281
|
+
|
|
282
|
+
const { result } = renderHook(
|
|
283
|
+
() =>
|
|
284
|
+
useQuery({
|
|
285
|
+
from: 'users',
|
|
286
|
+
}),
|
|
287
|
+
{ wrapper }
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
await waitFor(() => {
|
|
291
|
+
expect(result.current.data).toBeDefined()
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
expect(result.current.data?.[0]).toHaveProperty('name')
|
|
295
|
+
expect(result.current.data?.[0]).toHaveProperty('email')
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
it('supports where filtering with equality', async () => {
|
|
299
|
+
const { useQuery } = await import('@mdxui/terminal')
|
|
300
|
+
|
|
301
|
+
const { result } = renderHook(
|
|
302
|
+
() =>
|
|
303
|
+
useQuery({
|
|
304
|
+
from: 'users',
|
|
305
|
+
where: { role: 'admin' },
|
|
306
|
+
}),
|
|
307
|
+
{ wrapper }
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
await waitFor(() => {
|
|
311
|
+
expect(result.current.data).toHaveLength(1)
|
|
312
|
+
expect(result.current.data?.[0].name).toBe('Alice')
|
|
313
|
+
})
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
it('supports where filtering with comparison operators', async () => {
|
|
317
|
+
const { useQuery } = await import('@mdxui/terminal')
|
|
318
|
+
|
|
319
|
+
const { result } = renderHook(
|
|
320
|
+
() =>
|
|
321
|
+
useQuery({
|
|
322
|
+
from: 'users',
|
|
323
|
+
where: { age: { $gt: 28 } },
|
|
324
|
+
}),
|
|
325
|
+
{ wrapper }
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
await waitFor(() => {
|
|
329
|
+
expect(result.current.data).toHaveLength(2)
|
|
330
|
+
expect(result.current.data?.every((u) => (u.age ?? 0) > 28)).toBe(true)
|
|
331
|
+
})
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
it('supports orderBy sorting ascending', async () => {
|
|
335
|
+
const { useQuery } = await import('@mdxui/terminal')
|
|
336
|
+
|
|
337
|
+
const { result } = renderHook(
|
|
338
|
+
() =>
|
|
339
|
+
useQuery({
|
|
340
|
+
from: 'users',
|
|
341
|
+
orderBy: { age: 'asc' },
|
|
342
|
+
}),
|
|
343
|
+
{ wrapper }
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
await waitFor(() => {
|
|
347
|
+
expect(result.current.data).toHaveLength(3)
|
|
348
|
+
expect(result.current.data?.[0].name).toBe('Bob') // age 25
|
|
349
|
+
expect(result.current.data?.[1].name).toBe('Alice') // age 30
|
|
350
|
+
expect(result.current.data?.[2].name).toBe('Charlie') // age 35
|
|
351
|
+
})
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
it('supports orderBy sorting descending', async () => {
|
|
355
|
+
const { useQuery } = await import('@mdxui/terminal')
|
|
356
|
+
|
|
357
|
+
const { result } = renderHook(
|
|
358
|
+
() =>
|
|
359
|
+
useQuery({
|
|
360
|
+
from: 'users',
|
|
361
|
+
orderBy: { age: 'desc' },
|
|
362
|
+
}),
|
|
363
|
+
{ wrapper }
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
await waitFor(() => {
|
|
367
|
+
expect(result.current.data).toHaveLength(3)
|
|
368
|
+
expect(result.current.data?.[0].name).toBe('Charlie') // age 35
|
|
369
|
+
expect(result.current.data?.[2].name).toBe('Bob') // age 25
|
|
370
|
+
})
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
it('supports multiple orderBy fields', async () => {
|
|
374
|
+
const { useQuery } = await import('@mdxui/terminal')
|
|
375
|
+
|
|
376
|
+
const { result } = renderHook(
|
|
377
|
+
() =>
|
|
378
|
+
useQuery({
|
|
379
|
+
from: 'users',
|
|
380
|
+
orderBy: [
|
|
381
|
+
{ role: 'asc' },
|
|
382
|
+
{ age: 'desc' },
|
|
383
|
+
],
|
|
384
|
+
}),
|
|
385
|
+
{ wrapper }
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
await waitFor(() => {
|
|
389
|
+
expect(result.current.data).toHaveLength(3)
|
|
390
|
+
// admin first, then users sorted by age desc
|
|
391
|
+
expect(result.current.data?.[0].role).toBe('admin')
|
|
392
|
+
})
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
it('updates when data changes', async () => {
|
|
396
|
+
const { useQuery, createDB, createCollection, DBProvider } = await import('@mdxui/terminal')
|
|
397
|
+
|
|
398
|
+
const usersCollection = createCollection({
|
|
399
|
+
name: 'users',
|
|
400
|
+
schema: UserSchema,
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
const db = createDB({
|
|
404
|
+
collections: [usersCollection],
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
await db.collections.users.insert({
|
|
408
|
+
id: '1',
|
|
409
|
+
name: 'Initial User',
|
|
410
|
+
email: 'initial@example.com',
|
|
411
|
+
role: 'user',
|
|
412
|
+
createdAt: new Date(),
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
const testWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) =>
|
|
416
|
+
React.createElement(DBProvider, { db }, children)
|
|
417
|
+
|
|
418
|
+
const { result } = renderHook(
|
|
419
|
+
() =>
|
|
420
|
+
useQuery({
|
|
421
|
+
from: 'users',
|
|
422
|
+
}),
|
|
423
|
+
{ wrapper: testWrapper }
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
await waitFor(() => {
|
|
427
|
+
expect(result.current.data).toHaveLength(1)
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
// Add new user
|
|
431
|
+
await act(async () => {
|
|
432
|
+
await db.collections.users.insert({
|
|
433
|
+
id: '2',
|
|
434
|
+
name: 'New User',
|
|
435
|
+
email: 'new@example.com',
|
|
436
|
+
role: 'admin',
|
|
437
|
+
createdAt: new Date(),
|
|
438
|
+
})
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
await waitFor(() => {
|
|
442
|
+
expect(result.current.data).toHaveLength(2)
|
|
443
|
+
})
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
it('returns loading state', async () => {
|
|
447
|
+
const { useQuery } = await import('@mdxui/terminal')
|
|
448
|
+
|
|
449
|
+
const { result } = renderHook(
|
|
450
|
+
() =>
|
|
451
|
+
useQuery({
|
|
452
|
+
from: 'users',
|
|
453
|
+
}),
|
|
454
|
+
{ wrapper }
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
// Initially loading
|
|
458
|
+
expect(result.current.isLoading).toBe(true)
|
|
459
|
+
|
|
460
|
+
await waitFor(() => {
|
|
461
|
+
expect(result.current.isLoading).toBe(false)
|
|
462
|
+
})
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
it('returns error state on query failure', async () => {
|
|
466
|
+
const { useQuery } = await import('@mdxui/terminal')
|
|
467
|
+
|
|
468
|
+
const { result } = renderHook(
|
|
469
|
+
() =>
|
|
470
|
+
useQuery({
|
|
471
|
+
from: 'nonexistent_collection' as any,
|
|
472
|
+
}),
|
|
473
|
+
{ wrapper }
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
await waitFor(() => {
|
|
477
|
+
expect(result.current.error).toBeDefined()
|
|
478
|
+
})
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
it('supports limit parameter', async () => {
|
|
482
|
+
const { useQuery } = await import('@mdxui/terminal')
|
|
483
|
+
|
|
484
|
+
const { result } = renderHook(
|
|
485
|
+
() =>
|
|
486
|
+
useQuery({
|
|
487
|
+
from: 'users',
|
|
488
|
+
limit: 2,
|
|
489
|
+
}),
|
|
490
|
+
{ wrapper }
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
await waitFor(() => {
|
|
494
|
+
expect(result.current.data).toHaveLength(2)
|
|
495
|
+
})
|
|
496
|
+
})
|
|
497
|
+
|
|
498
|
+
it('supports offset parameter', async () => {
|
|
499
|
+
const { useQuery } = await import('@mdxui/terminal')
|
|
500
|
+
|
|
501
|
+
const { result } = renderHook(
|
|
502
|
+
() =>
|
|
503
|
+
useQuery({
|
|
504
|
+
from: 'users',
|
|
505
|
+
orderBy: { age: 'asc' },
|
|
506
|
+
offset: 1,
|
|
507
|
+
}),
|
|
508
|
+
{ wrapper }
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
await waitFor(() => {
|
|
512
|
+
expect(result.current.data?.[0].name).toBe('Alice') // skipped Bob
|
|
513
|
+
})
|
|
514
|
+
})
|
|
515
|
+
})
|
|
516
|
+
|
|
517
|
+
// ============================================================================
|
|
518
|
+
// useMutation() Hook Tests
|
|
519
|
+
// ============================================================================
|
|
520
|
+
|
|
521
|
+
describe('@mdxui/terminal data layer - useMutation', () => {
|
|
522
|
+
let wrapper: React.FC<{ children: React.ReactNode }>
|
|
523
|
+
let db: any
|
|
524
|
+
|
|
525
|
+
beforeEach(async () => {
|
|
526
|
+
const { createDB, createCollection, DBProvider } = await import('@mdxui/terminal')
|
|
527
|
+
|
|
528
|
+
const usersCollection = createCollection({
|
|
529
|
+
name: 'users',
|
|
530
|
+
schema: UserSchema,
|
|
531
|
+
})
|
|
532
|
+
|
|
533
|
+
db = createDB({
|
|
534
|
+
collections: [usersCollection],
|
|
535
|
+
})
|
|
536
|
+
|
|
537
|
+
await db.collections.users.insert({
|
|
538
|
+
id: '1',
|
|
539
|
+
name: 'Alice',
|
|
540
|
+
email: 'alice@example.com',
|
|
541
|
+
role: 'admin',
|
|
542
|
+
createdAt: new Date(),
|
|
543
|
+
})
|
|
544
|
+
|
|
545
|
+
wrapper = ({ children }) =>
|
|
546
|
+
React.createElement(DBProvider, { db }, children)
|
|
547
|
+
})
|
|
548
|
+
|
|
549
|
+
it('returns mutation function', async () => {
|
|
550
|
+
const { useMutation } = await import('@mdxui/terminal')
|
|
551
|
+
|
|
552
|
+
const { result } = renderHook(
|
|
553
|
+
() =>
|
|
554
|
+
useMutation({
|
|
555
|
+
collection: 'users',
|
|
556
|
+
operation: 'insert',
|
|
557
|
+
}),
|
|
558
|
+
{ wrapper }
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
expect(result.current.mutate).toBeDefined()
|
|
562
|
+
expect(typeof result.current.mutate).toBe('function')
|
|
563
|
+
})
|
|
564
|
+
|
|
565
|
+
it('executes insert mutation', async () => {
|
|
566
|
+
const { useMutation, useQuery } = await import('@mdxui/terminal')
|
|
567
|
+
|
|
568
|
+
const { result: mutationResult } = renderHook(
|
|
569
|
+
() =>
|
|
570
|
+
useMutation({
|
|
571
|
+
collection: 'users',
|
|
572
|
+
operation: 'insert',
|
|
573
|
+
}),
|
|
574
|
+
{ wrapper }
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
const { result: queryResult } = renderHook(
|
|
578
|
+
() =>
|
|
579
|
+
useQuery({
|
|
580
|
+
from: 'users',
|
|
581
|
+
}),
|
|
582
|
+
{ wrapper }
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
await waitFor(() => {
|
|
586
|
+
expect(queryResult.current.data).toHaveLength(1)
|
|
587
|
+
})
|
|
588
|
+
|
|
589
|
+
await act(async () => {
|
|
590
|
+
await mutationResult.current.mutate({
|
|
591
|
+
id: '2',
|
|
592
|
+
name: 'Bob',
|
|
593
|
+
email: 'bob@example.com',
|
|
594
|
+
role: 'user',
|
|
595
|
+
createdAt: new Date(),
|
|
596
|
+
})
|
|
597
|
+
})
|
|
598
|
+
|
|
599
|
+
await waitFor(() => {
|
|
600
|
+
expect(queryResult.current.data).toHaveLength(2)
|
|
601
|
+
})
|
|
602
|
+
})
|
|
603
|
+
|
|
604
|
+
it('executes update mutation', async () => {
|
|
605
|
+
const { useMutation, useQuery } = await import('@mdxui/terminal')
|
|
606
|
+
|
|
607
|
+
const { result: mutationResult } = renderHook(
|
|
608
|
+
() =>
|
|
609
|
+
useMutation({
|
|
610
|
+
collection: 'users',
|
|
611
|
+
operation: 'update',
|
|
612
|
+
}),
|
|
613
|
+
{ wrapper }
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
await act(async () => {
|
|
617
|
+
await mutationResult.current.mutate({
|
|
618
|
+
where: { id: '1' },
|
|
619
|
+
data: { name: 'Alice Updated' },
|
|
620
|
+
})
|
|
621
|
+
})
|
|
622
|
+
|
|
623
|
+
const { result: queryResult } = renderHook(
|
|
624
|
+
() =>
|
|
625
|
+
useQuery({
|
|
626
|
+
from: 'users',
|
|
627
|
+
where: { id: '1' },
|
|
628
|
+
}),
|
|
629
|
+
{ wrapper }
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
await waitFor(() => {
|
|
633
|
+
expect(queryResult.current.data?.[0].name).toBe('Alice Updated')
|
|
634
|
+
})
|
|
635
|
+
})
|
|
636
|
+
|
|
637
|
+
it('executes delete mutation', async () => {
|
|
638
|
+
const { useMutation, useQuery } = await import('@mdxui/terminal')
|
|
639
|
+
|
|
640
|
+
const { result: mutationResult } = renderHook(
|
|
641
|
+
() =>
|
|
642
|
+
useMutation({
|
|
643
|
+
collection: 'users',
|
|
644
|
+
operation: 'delete',
|
|
645
|
+
}),
|
|
646
|
+
{ wrapper }
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
await act(async () => {
|
|
650
|
+
await mutationResult.current.mutate({ id: '1' })
|
|
651
|
+
})
|
|
652
|
+
|
|
653
|
+
const { result: queryResult } = renderHook(
|
|
654
|
+
() =>
|
|
655
|
+
useQuery({
|
|
656
|
+
from: 'users',
|
|
657
|
+
}),
|
|
658
|
+
{ wrapper }
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
await waitFor(() => {
|
|
662
|
+
expect(queryResult.current.data).toHaveLength(0)
|
|
663
|
+
})
|
|
664
|
+
})
|
|
665
|
+
|
|
666
|
+
it('supports optimistic updates', async () => {
|
|
667
|
+
const { useMutation, useQuery } = await import('@mdxui/terminal')
|
|
668
|
+
|
|
669
|
+
const { result: queryResult } = renderHook(
|
|
670
|
+
() =>
|
|
671
|
+
useQuery({
|
|
672
|
+
from: 'users',
|
|
673
|
+
}),
|
|
674
|
+
{ wrapper }
|
|
675
|
+
)
|
|
676
|
+
|
|
677
|
+
await waitFor(() => {
|
|
678
|
+
expect(queryResult.current.data).toHaveLength(1)
|
|
679
|
+
})
|
|
680
|
+
|
|
681
|
+
const { result: mutationResult } = renderHook(
|
|
682
|
+
() =>
|
|
683
|
+
useMutation({
|
|
684
|
+
collection: 'users',
|
|
685
|
+
operation: 'insert',
|
|
686
|
+
optimistic: true,
|
|
687
|
+
}),
|
|
688
|
+
{ wrapper }
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
// With optimistic update, data should update immediately
|
|
692
|
+
act(() => {
|
|
693
|
+
mutationResult.current.mutate({
|
|
694
|
+
id: '2',
|
|
695
|
+
name: 'Bob',
|
|
696
|
+
email: 'bob@example.com',
|
|
697
|
+
role: 'user',
|
|
698
|
+
createdAt: new Date(),
|
|
699
|
+
})
|
|
700
|
+
})
|
|
701
|
+
|
|
702
|
+
// Should immediately reflect the optimistic update
|
|
703
|
+
expect(queryResult.current.data).toHaveLength(2)
|
|
704
|
+
})
|
|
705
|
+
|
|
706
|
+
it('rolls back optimistic update on error', async () => {
|
|
707
|
+
const { useMutation, useQuery, createDB, createCollection, DBProvider } =
|
|
708
|
+
await import('@mdxui/terminal')
|
|
709
|
+
|
|
710
|
+
const usersCollection = createCollection({
|
|
711
|
+
name: 'users',
|
|
712
|
+
schema: UserSchema,
|
|
713
|
+
})
|
|
714
|
+
|
|
715
|
+
// Create DB with a sync adapter that fails
|
|
716
|
+
const failingSyncAdapter = {
|
|
717
|
+
push: vi.fn().mockRejectedValue(new Error('Sync failed')),
|
|
718
|
+
pull: vi.fn(),
|
|
719
|
+
subscribe: vi.fn(),
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const failingDb = createDB({
|
|
723
|
+
collections: [usersCollection],
|
|
724
|
+
sync: failingSyncAdapter,
|
|
725
|
+
})
|
|
726
|
+
|
|
727
|
+
await failingDb.collections.users.insert({
|
|
728
|
+
id: '1',
|
|
729
|
+
name: 'Alice',
|
|
730
|
+
email: 'alice@example.com',
|
|
731
|
+
role: 'admin',
|
|
732
|
+
createdAt: new Date(),
|
|
733
|
+
})
|
|
734
|
+
|
|
735
|
+
const failingWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) =>
|
|
736
|
+
React.createElement(DBProvider, { db: failingDb }, children)
|
|
737
|
+
|
|
738
|
+
const { result: queryResult } = renderHook(
|
|
739
|
+
() =>
|
|
740
|
+
useQuery({
|
|
741
|
+
from: 'users',
|
|
742
|
+
}),
|
|
743
|
+
{ wrapper: failingWrapper }
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
await waitFor(() => {
|
|
747
|
+
expect(queryResult.current.data).toHaveLength(1)
|
|
748
|
+
})
|
|
749
|
+
|
|
750
|
+
const { result: mutationResult } = renderHook(
|
|
751
|
+
() =>
|
|
752
|
+
useMutation({
|
|
753
|
+
collection: 'users',
|
|
754
|
+
operation: 'insert',
|
|
755
|
+
optimistic: true,
|
|
756
|
+
}),
|
|
757
|
+
{ wrapper: failingWrapper }
|
|
758
|
+
)
|
|
759
|
+
|
|
760
|
+
await act(async () => {
|
|
761
|
+
try {
|
|
762
|
+
await mutationResult.current.mutate({
|
|
763
|
+
id: '2',
|
|
764
|
+
name: 'Bob',
|
|
765
|
+
email: 'bob@example.com',
|
|
766
|
+
role: 'user',
|
|
767
|
+
createdAt: new Date(),
|
|
768
|
+
})
|
|
769
|
+
} catch {
|
|
770
|
+
// Expected to fail
|
|
771
|
+
}
|
|
772
|
+
})
|
|
773
|
+
|
|
774
|
+
// Should roll back to original state
|
|
775
|
+
await waitFor(() => {
|
|
776
|
+
expect(queryResult.current.data).toHaveLength(1)
|
|
777
|
+
})
|
|
778
|
+
})
|
|
779
|
+
|
|
780
|
+
it('returns isPending state during mutation', async () => {
|
|
781
|
+
const { useMutation } = await import('@mdxui/terminal')
|
|
782
|
+
|
|
783
|
+
const { result } = renderHook(
|
|
784
|
+
() =>
|
|
785
|
+
useMutation({
|
|
786
|
+
collection: 'users',
|
|
787
|
+
operation: 'insert',
|
|
788
|
+
}),
|
|
789
|
+
{ wrapper }
|
|
790
|
+
)
|
|
791
|
+
|
|
792
|
+
expect(result.current.isPending).toBe(false)
|
|
793
|
+
|
|
794
|
+
act(() => {
|
|
795
|
+
result.current.mutate({
|
|
796
|
+
id: '2',
|
|
797
|
+
name: 'Bob',
|
|
798
|
+
email: 'bob@example.com',
|
|
799
|
+
role: 'user',
|
|
800
|
+
createdAt: new Date(),
|
|
801
|
+
})
|
|
802
|
+
})
|
|
803
|
+
|
|
804
|
+
expect(result.current.isPending).toBe(true)
|
|
805
|
+
|
|
806
|
+
await waitFor(() => {
|
|
807
|
+
expect(result.current.isPending).toBe(false)
|
|
808
|
+
})
|
|
809
|
+
})
|
|
810
|
+
|
|
811
|
+
it('returns error state on mutation failure', async () => {
|
|
812
|
+
const { useMutation } = await import('@mdxui/terminal')
|
|
813
|
+
|
|
814
|
+
const { result } = renderHook(
|
|
815
|
+
() =>
|
|
816
|
+
useMutation({
|
|
817
|
+
collection: 'users',
|
|
818
|
+
operation: 'insert',
|
|
819
|
+
}),
|
|
820
|
+
{ wrapper }
|
|
821
|
+
)
|
|
822
|
+
|
|
823
|
+
await act(async () => {
|
|
824
|
+
try {
|
|
825
|
+
await result.current.mutate({
|
|
826
|
+
id: '1', // Duplicate ID should fail
|
|
827
|
+
name: 'Duplicate',
|
|
828
|
+
email: 'dup@example.com',
|
|
829
|
+
role: 'user',
|
|
830
|
+
createdAt: new Date(),
|
|
831
|
+
})
|
|
832
|
+
} catch {
|
|
833
|
+
// Expected
|
|
834
|
+
}
|
|
835
|
+
})
|
|
836
|
+
|
|
837
|
+
expect(result.current.error).toBeDefined()
|
|
838
|
+
})
|
|
839
|
+
})
|
|
840
|
+
|
|
841
|
+
// ============================================================================
|
|
842
|
+
// Query Filtering Tests
|
|
843
|
+
// ============================================================================
|
|
844
|
+
|
|
845
|
+
describe('@mdxui/terminal data layer - query filtering', () => {
|
|
846
|
+
let wrapper: React.FC<{ children: React.ReactNode }>
|
|
847
|
+
|
|
848
|
+
beforeEach(async () => {
|
|
849
|
+
const { createDB, createCollection, DBProvider } = await import('@mdxui/terminal')
|
|
850
|
+
|
|
851
|
+
const todosCollection = createCollection({
|
|
852
|
+
name: 'todos',
|
|
853
|
+
schema: TodoSchema,
|
|
854
|
+
})
|
|
855
|
+
|
|
856
|
+
const db = createDB({
|
|
857
|
+
collections: [todosCollection],
|
|
858
|
+
})
|
|
859
|
+
|
|
860
|
+
// Seed test data
|
|
861
|
+
await db.collections.todos.insert({
|
|
862
|
+
id: '1',
|
|
863
|
+
title: 'Buy groceries',
|
|
864
|
+
completed: false,
|
|
865
|
+
priority: 3,
|
|
866
|
+
userId: 'user1',
|
|
867
|
+
})
|
|
868
|
+
await db.collections.todos.insert({
|
|
869
|
+
id: '2',
|
|
870
|
+
title: 'Write tests',
|
|
871
|
+
completed: true,
|
|
872
|
+
priority: 5,
|
|
873
|
+
userId: 'user1',
|
|
874
|
+
})
|
|
875
|
+
await db.collections.todos.insert({
|
|
876
|
+
id: '3',
|
|
877
|
+
title: 'Review PR',
|
|
878
|
+
completed: false,
|
|
879
|
+
priority: 4,
|
|
880
|
+
userId: 'user2',
|
|
881
|
+
})
|
|
882
|
+
await db.collections.todos.insert({
|
|
883
|
+
id: '4',
|
|
884
|
+
title: 'Deploy app',
|
|
885
|
+
completed: true,
|
|
886
|
+
priority: 5,
|
|
887
|
+
userId: 'user2',
|
|
888
|
+
})
|
|
889
|
+
|
|
890
|
+
wrapper = ({ children }) =>
|
|
891
|
+
React.createElement(DBProvider, { db }, children)
|
|
892
|
+
})
|
|
893
|
+
|
|
894
|
+
it('filters with $eq (equality)', async () => {
|
|
895
|
+
const { useQuery } = await import('@mdxui/terminal')
|
|
896
|
+
|
|
897
|
+
const { result } = renderHook(
|
|
898
|
+
() =>
|
|
899
|
+
useQuery({
|
|
900
|
+
from: 'todos',
|
|
901
|
+
where: { completed: { $eq: true } },
|
|
902
|
+
}),
|
|
903
|
+
{ wrapper }
|
|
904
|
+
)
|
|
905
|
+
|
|
906
|
+
await waitFor(() => {
|
|
907
|
+
expect(result.current.data).toHaveLength(2)
|
|
908
|
+
expect(result.current.data?.every((t) => t.completed)).toBe(true)
|
|
909
|
+
})
|
|
910
|
+
})
|
|
911
|
+
|
|
912
|
+
it('filters with $ne (not equal)', async () => {
|
|
913
|
+
const { useQuery } = await import('@mdxui/terminal')
|
|
914
|
+
|
|
915
|
+
const { result } = renderHook(
|
|
916
|
+
() =>
|
|
917
|
+
useQuery({
|
|
918
|
+
from: 'todos',
|
|
919
|
+
where: { userId: { $ne: 'user1' } },
|
|
920
|
+
}),
|
|
921
|
+
{ wrapper }
|
|
922
|
+
)
|
|
923
|
+
|
|
924
|
+
await waitFor(() => {
|
|
925
|
+
expect(result.current.data).toHaveLength(2)
|
|
926
|
+
expect(result.current.data?.every((t) => t.userId !== 'user1')).toBe(true)
|
|
927
|
+
})
|
|
928
|
+
})
|
|
929
|
+
|
|
930
|
+
it('filters with $gt (greater than)', async () => {
|
|
931
|
+
const { useQuery } = await import('@mdxui/terminal')
|
|
932
|
+
|
|
933
|
+
const { result } = renderHook(
|
|
934
|
+
() =>
|
|
935
|
+
useQuery({
|
|
936
|
+
from: 'todos',
|
|
937
|
+
where: { priority: { $gt: 3 } },
|
|
938
|
+
}),
|
|
939
|
+
{ wrapper }
|
|
940
|
+
)
|
|
941
|
+
|
|
942
|
+
await waitFor(() => {
|
|
943
|
+
expect(result.current.data).toHaveLength(3)
|
|
944
|
+
expect(result.current.data?.every((t) => t.priority > 3)).toBe(true)
|
|
945
|
+
})
|
|
946
|
+
})
|
|
947
|
+
|
|
948
|
+
it('filters with $gte (greater than or equal)', async () => {
|
|
949
|
+
const { useQuery } = await import('@mdxui/terminal')
|
|
950
|
+
|
|
951
|
+
const { result } = renderHook(
|
|
952
|
+
() =>
|
|
953
|
+
useQuery({
|
|
954
|
+
from: 'todos',
|
|
955
|
+
where: { priority: { $gte: 4 } },
|
|
956
|
+
}),
|
|
957
|
+
{ wrapper }
|
|
958
|
+
)
|
|
959
|
+
|
|
960
|
+
await waitFor(() => {
|
|
961
|
+
expect(result.current.data).toHaveLength(3)
|
|
962
|
+
expect(result.current.data?.every((t) => t.priority >= 4)).toBe(true)
|
|
963
|
+
})
|
|
964
|
+
})
|
|
965
|
+
|
|
966
|
+
it('filters with $lt (less than)', async () => {
|
|
967
|
+
const { useQuery } = await import('@mdxui/terminal')
|
|
968
|
+
|
|
969
|
+
const { result } = renderHook(
|
|
970
|
+
() =>
|
|
971
|
+
useQuery({
|
|
972
|
+
from: 'todos',
|
|
973
|
+
where: { priority: { $lt: 5 } },
|
|
974
|
+
}),
|
|
975
|
+
{ wrapper }
|
|
976
|
+
)
|
|
977
|
+
|
|
978
|
+
await waitFor(() => {
|
|
979
|
+
expect(result.current.data).toHaveLength(2)
|
|
980
|
+
expect(result.current.data?.every((t) => t.priority < 5)).toBe(true)
|
|
981
|
+
})
|
|
982
|
+
})
|
|
983
|
+
|
|
984
|
+
it('filters with $lte (less than or equal)', async () => {
|
|
985
|
+
const { useQuery } = await import('@mdxui/terminal')
|
|
986
|
+
|
|
987
|
+
const { result } = renderHook(
|
|
988
|
+
() =>
|
|
989
|
+
useQuery({
|
|
990
|
+
from: 'todos',
|
|
991
|
+
where: { priority: { $lte: 4 } },
|
|
992
|
+
}),
|
|
993
|
+
{ wrapper }
|
|
994
|
+
)
|
|
995
|
+
|
|
996
|
+
await waitFor(() => {
|
|
997
|
+
expect(result.current.data).toHaveLength(2)
|
|
998
|
+
expect(result.current.data?.every((t) => t.priority <= 4)).toBe(true)
|
|
999
|
+
})
|
|
1000
|
+
})
|
|
1001
|
+
|
|
1002
|
+
it('filters with $in (in array)', async () => {
|
|
1003
|
+
const { useQuery } = await import('@mdxui/terminal')
|
|
1004
|
+
|
|
1005
|
+
const { result } = renderHook(
|
|
1006
|
+
() =>
|
|
1007
|
+
useQuery({
|
|
1008
|
+
from: 'todos',
|
|
1009
|
+
where: { priority: { $in: [3, 5] } },
|
|
1010
|
+
}),
|
|
1011
|
+
{ wrapper }
|
|
1012
|
+
)
|
|
1013
|
+
|
|
1014
|
+
await waitFor(() => {
|
|
1015
|
+
expect(result.current.data).toHaveLength(3)
|
|
1016
|
+
expect(result.current.data?.every((t) => [3, 5].includes(t.priority))).toBe(
|
|
1017
|
+
true
|
|
1018
|
+
)
|
|
1019
|
+
})
|
|
1020
|
+
})
|
|
1021
|
+
|
|
1022
|
+
it('filters with $nin (not in array)', async () => {
|
|
1023
|
+
const { useQuery } = await import('@mdxui/terminal')
|
|
1024
|
+
|
|
1025
|
+
const { result } = renderHook(
|
|
1026
|
+
() =>
|
|
1027
|
+
useQuery({
|
|
1028
|
+
from: 'todos',
|
|
1029
|
+
where: { priority: { $nin: [3, 5] } },
|
|
1030
|
+
}),
|
|
1031
|
+
{ wrapper }
|
|
1032
|
+
)
|
|
1033
|
+
|
|
1034
|
+
await waitFor(() => {
|
|
1035
|
+
expect(result.current.data).toHaveLength(1)
|
|
1036
|
+
expect(result.current.data?.[0].priority).toBe(4)
|
|
1037
|
+
})
|
|
1038
|
+
})
|
|
1039
|
+
|
|
1040
|
+
it('combines multiple where conditions (AND)', async () => {
|
|
1041
|
+
const { useQuery } = await import('@mdxui/terminal')
|
|
1042
|
+
|
|
1043
|
+
const { result } = renderHook(
|
|
1044
|
+
() =>
|
|
1045
|
+
useQuery({
|
|
1046
|
+
from: 'todos',
|
|
1047
|
+
where: {
|
|
1048
|
+
completed: false,
|
|
1049
|
+
priority: { $gte: 3 },
|
|
1050
|
+
},
|
|
1051
|
+
}),
|
|
1052
|
+
{ wrapper }
|
|
1053
|
+
)
|
|
1054
|
+
|
|
1055
|
+
await waitFor(() => {
|
|
1056
|
+
expect(result.current.data).toHaveLength(2)
|
|
1057
|
+
expect(result.current.data?.every((t) => !t.completed && t.priority >= 3)).toBe(
|
|
1058
|
+
true
|
|
1059
|
+
)
|
|
1060
|
+
})
|
|
1061
|
+
})
|
|
1062
|
+
|
|
1063
|
+
it('filters with $or operator', async () => {
|
|
1064
|
+
const { useQuery } = await import('@mdxui/terminal')
|
|
1065
|
+
|
|
1066
|
+
const { result } = renderHook(
|
|
1067
|
+
() =>
|
|
1068
|
+
useQuery({
|
|
1069
|
+
from: 'todos',
|
|
1070
|
+
where: {
|
|
1071
|
+
$or: [{ priority: 5 }, { userId: 'user1' }],
|
|
1072
|
+
},
|
|
1073
|
+
}),
|
|
1074
|
+
{ wrapper }
|
|
1075
|
+
)
|
|
1076
|
+
|
|
1077
|
+
await waitFor(() => {
|
|
1078
|
+
expect(result.current.data).toHaveLength(3)
|
|
1079
|
+
})
|
|
1080
|
+
})
|
|
1081
|
+
})
|
|
1082
|
+
|
|
1083
|
+
// ============================================================================
|
|
1084
|
+
// Query Sorting Tests
|
|
1085
|
+
// ============================================================================
|
|
1086
|
+
|
|
1087
|
+
describe('@mdxui/terminal data layer - query sorting', () => {
|
|
1088
|
+
let wrapper: React.FC<{ children: React.ReactNode }>
|
|
1089
|
+
|
|
1090
|
+
beforeEach(async () => {
|
|
1091
|
+
const { createDB, createCollection, DBProvider } = await import('@mdxui/terminal')
|
|
1092
|
+
|
|
1093
|
+
const todosCollection = createCollection({
|
|
1094
|
+
name: 'todos',
|
|
1095
|
+
schema: TodoSchema,
|
|
1096
|
+
})
|
|
1097
|
+
|
|
1098
|
+
const db = createDB({
|
|
1099
|
+
collections: [todosCollection],
|
|
1100
|
+
})
|
|
1101
|
+
|
|
1102
|
+
await db.collections.todos.insert({
|
|
1103
|
+
id: '1',
|
|
1104
|
+
title: 'A task',
|
|
1105
|
+
completed: false,
|
|
1106
|
+
priority: 3,
|
|
1107
|
+
userId: 'user1',
|
|
1108
|
+
})
|
|
1109
|
+
await db.collections.todos.insert({
|
|
1110
|
+
id: '2',
|
|
1111
|
+
title: 'B task',
|
|
1112
|
+
completed: true,
|
|
1113
|
+
priority: 3,
|
|
1114
|
+
userId: 'user2',
|
|
1115
|
+
})
|
|
1116
|
+
await db.collections.todos.insert({
|
|
1117
|
+
id: '3',
|
|
1118
|
+
title: 'C task',
|
|
1119
|
+
completed: false,
|
|
1120
|
+
priority: 1,
|
|
1121
|
+
userId: 'user1',
|
|
1122
|
+
})
|
|
1123
|
+
|
|
1124
|
+
wrapper = ({ children }) =>
|
|
1125
|
+
React.createElement(DBProvider, { db }, children)
|
|
1126
|
+
})
|
|
1127
|
+
|
|
1128
|
+
it('sorts by single field ascending', async () => {
|
|
1129
|
+
const { useQuery } = await import('@mdxui/terminal')
|
|
1130
|
+
|
|
1131
|
+
const { result } = renderHook(
|
|
1132
|
+
() =>
|
|
1133
|
+
useQuery({
|
|
1134
|
+
from: 'todos',
|
|
1135
|
+
orderBy: { title: 'asc' },
|
|
1136
|
+
}),
|
|
1137
|
+
{ wrapper }
|
|
1138
|
+
)
|
|
1139
|
+
|
|
1140
|
+
await waitFor(() => {
|
|
1141
|
+
expect(result.current.data?.[0].title).toBe('A task')
|
|
1142
|
+
expect(result.current.data?.[1].title).toBe('B task')
|
|
1143
|
+
expect(result.current.data?.[2].title).toBe('C task')
|
|
1144
|
+
})
|
|
1145
|
+
})
|
|
1146
|
+
|
|
1147
|
+
it('sorts by single field descending', async () => {
|
|
1148
|
+
const { useQuery } = await import('@mdxui/terminal')
|
|
1149
|
+
|
|
1150
|
+
const { result } = renderHook(
|
|
1151
|
+
() =>
|
|
1152
|
+
useQuery({
|
|
1153
|
+
from: 'todos',
|
|
1154
|
+
orderBy: { title: 'desc' },
|
|
1155
|
+
}),
|
|
1156
|
+
{ wrapper }
|
|
1157
|
+
)
|
|
1158
|
+
|
|
1159
|
+
await waitFor(() => {
|
|
1160
|
+
expect(result.current.data?.[0].title).toBe('C task')
|
|
1161
|
+
expect(result.current.data?.[2].title).toBe('A task')
|
|
1162
|
+
})
|
|
1163
|
+
})
|
|
1164
|
+
|
|
1165
|
+
it('sorts by multiple fields', async () => {
|
|
1166
|
+
const { useQuery } = await import('@mdxui/terminal')
|
|
1167
|
+
|
|
1168
|
+
const { result } = renderHook(
|
|
1169
|
+
() =>
|
|
1170
|
+
useQuery({
|
|
1171
|
+
from: 'todos',
|
|
1172
|
+
orderBy: [
|
|
1173
|
+
{ priority: 'asc' },
|
|
1174
|
+
{ title: 'desc' },
|
|
1175
|
+
],
|
|
1176
|
+
}),
|
|
1177
|
+
{ wrapper }
|
|
1178
|
+
)
|
|
1179
|
+
|
|
1180
|
+
await waitFor(() => {
|
|
1181
|
+
// Priority 1 first
|
|
1182
|
+
expect(result.current.data?.[0].priority).toBe(1)
|
|
1183
|
+
// Then priority 3, sorted by title desc (B before A)
|
|
1184
|
+
expect(result.current.data?.[1].title).toBe('B task')
|
|
1185
|
+
expect(result.current.data?.[2].title).toBe('A task')
|
|
1186
|
+
})
|
|
1187
|
+
})
|
|
1188
|
+
|
|
1189
|
+
it('handles null/undefined values in sort', async () => {
|
|
1190
|
+
const { createDB, createCollection, DBProvider, useQuery } = await import('@mdxui/terminal')
|
|
1191
|
+
|
|
1192
|
+
const SchemaWithOptional = z.object({
|
|
1193
|
+
id: z.string(),
|
|
1194
|
+
name: z.string(),
|
|
1195
|
+
score: z.number().optional(),
|
|
1196
|
+
})
|
|
1197
|
+
|
|
1198
|
+
const collection = createCollection({
|
|
1199
|
+
name: 'items',
|
|
1200
|
+
schema: SchemaWithOptional,
|
|
1201
|
+
})
|
|
1202
|
+
|
|
1203
|
+
const db = createDB({
|
|
1204
|
+
collections: [collection],
|
|
1205
|
+
})
|
|
1206
|
+
|
|
1207
|
+
await db.collections.items.insert({ id: '1', name: 'A', score: 10 })
|
|
1208
|
+
await db.collections.items.insert({ id: '2', name: 'B' }) // no score
|
|
1209
|
+
await db.collections.items.insert({ id: '3', name: 'C', score: 5 })
|
|
1210
|
+
|
|
1211
|
+
const testWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) =>
|
|
1212
|
+
React.createElement(DBProvider, { db }, children)
|
|
1213
|
+
|
|
1214
|
+
const { result } = renderHook(
|
|
1215
|
+
() =>
|
|
1216
|
+
useQuery({
|
|
1217
|
+
from: 'items',
|
|
1218
|
+
orderBy: { score: 'asc' },
|
|
1219
|
+
}),
|
|
1220
|
+
{ wrapper: testWrapper }
|
|
1221
|
+
)
|
|
1222
|
+
|
|
1223
|
+
await waitFor(() => {
|
|
1224
|
+
// Nulls should be last or first consistently
|
|
1225
|
+
expect(result.current.data).toHaveLength(3)
|
|
1226
|
+
})
|
|
1227
|
+
})
|
|
1228
|
+
})
|
|
1229
|
+
|
|
1230
|
+
// ============================================================================
|
|
1231
|
+
// DBProvider Tests
|
|
1232
|
+
// ============================================================================
|
|
1233
|
+
|
|
1234
|
+
describe('@mdxui/terminal data layer - DBProvider', () => {
|
|
1235
|
+
it('exports DBProvider component', async () => {
|
|
1236
|
+
const { DBProvider } = await import('@mdxui/terminal')
|
|
1237
|
+
|
|
1238
|
+
expect(DBProvider).toBeDefined()
|
|
1239
|
+
})
|
|
1240
|
+
|
|
1241
|
+
it('provides database context to children', async () => {
|
|
1242
|
+
const { createDB, DBProvider, useDBContext } = await import('@mdxui/terminal')
|
|
1243
|
+
|
|
1244
|
+
const db = createDB()
|
|
1245
|
+
|
|
1246
|
+
const wrapper: React.FC<{ children: React.ReactNode }> = ({ children }) =>
|
|
1247
|
+
React.createElement(DBProvider, { db }, children)
|
|
1248
|
+
|
|
1249
|
+
const { result } = renderHook(() => useDBContext(), { wrapper })
|
|
1250
|
+
|
|
1251
|
+
expect(result.current).toBe(db)
|
|
1252
|
+
})
|
|
1253
|
+
|
|
1254
|
+
it('throws error when useQuery used outside DBProvider', async () => {
|
|
1255
|
+
const { useQuery } = await import('@mdxui/terminal')
|
|
1256
|
+
|
|
1257
|
+
expect(() =>
|
|
1258
|
+
renderHook(() =>
|
|
1259
|
+
useQuery({
|
|
1260
|
+
from: 'users',
|
|
1261
|
+
})
|
|
1262
|
+
)
|
|
1263
|
+
).toThrow()
|
|
1264
|
+
})
|
|
1265
|
+
})
|