@loro-extended/change 0.2.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/LICENSE +21 -0
- package/README.md +565 -0
- package/dist/index.d.ts +339 -0
- package/dist/index.js +1491 -0
- package/dist/index.js.map +1 -0
- package/package.json +39 -0
- package/src/change.test.ts +2006 -0
- package/src/change.ts +105 -0
- package/src/conversion.test.ts +728 -0
- package/src/conversion.ts +220 -0
- package/src/draft-nodes/base.ts +34 -0
- package/src/draft-nodes/counter.ts +21 -0
- package/src/draft-nodes/doc.ts +81 -0
- package/src/draft-nodes/list-base.ts +326 -0
- package/src/draft-nodes/list.ts +18 -0
- package/src/draft-nodes/map.ts +156 -0
- package/src/draft-nodes/movable-list.ts +26 -0
- package/src/draft-nodes/record.ts +215 -0
- package/src/draft-nodes/text.ts +48 -0
- package/src/draft-nodes/tree.ts +31 -0
- package/src/draft-nodes/utils.ts +55 -0
- package/src/index.ts +33 -0
- package/src/json-patch.test.ts +697 -0
- package/src/json-patch.ts +391 -0
- package/src/overlay.ts +90 -0
- package/src/record.test.ts +188 -0
- package/src/schema.fixtures.ts +138 -0
- package/src/shape.ts +348 -0
- package/src/types.ts +15 -0
- package/src/utils/type-guards.ts +210 -0
- package/src/validation.ts +261 -0
|
@@ -0,0 +1,728 @@
|
|
|
1
|
+
/** biome-ignore-all lint/suspicious/noExplicitAny: allow for tests */
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type Container,
|
|
5
|
+
type LoroCounter,
|
|
6
|
+
LoroList,
|
|
7
|
+
type LoroMap,
|
|
8
|
+
type LoroMovableList,
|
|
9
|
+
type LoroText,
|
|
10
|
+
} from "loro-crdt"
|
|
11
|
+
import { describe, expect, it } from "vitest"
|
|
12
|
+
import { convertInputToNode } from "./conversion.js"
|
|
13
|
+
import { Shape } from "./shape.js"
|
|
14
|
+
import {
|
|
15
|
+
isContainer,
|
|
16
|
+
isLoroCounter,
|
|
17
|
+
isLoroList,
|
|
18
|
+
isLoroMap,
|
|
19
|
+
isLoroMovableList,
|
|
20
|
+
isLoroText,
|
|
21
|
+
} from "./utils/type-guards.js"
|
|
22
|
+
|
|
23
|
+
describe("Conversion Functions", () => {
|
|
24
|
+
describe("convertInputToNode - Text Conversion", () => {
|
|
25
|
+
it("should convert string to LoroText", () => {
|
|
26
|
+
const shape = Shape.text()
|
|
27
|
+
const result = convertInputToNode("Hello World", shape)
|
|
28
|
+
|
|
29
|
+
expect(isContainer(result)).toBe(true)
|
|
30
|
+
expect(isLoroText(result as any)).toBe(true)
|
|
31
|
+
|
|
32
|
+
const text = result as LoroText
|
|
33
|
+
expect(text.toString()).toBe("Hello World")
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it("should handle empty string", () => {
|
|
37
|
+
const shape = Shape.text()
|
|
38
|
+
const result = convertInputToNode("", shape)
|
|
39
|
+
|
|
40
|
+
expect(isLoroText(result as any)).toBe(true)
|
|
41
|
+
const text = result as LoroText
|
|
42
|
+
expect(text.toString()).toBe("")
|
|
43
|
+
expect(text.length).toBe(0)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it("should handle unicode and special characters", () => {
|
|
47
|
+
const shape = Shape.text()
|
|
48
|
+
const testStrings = [
|
|
49
|
+
"Hello ไธ็ ๐",
|
|
50
|
+
"Special chars: !@#$%^&*()",
|
|
51
|
+
"Newlines\nand\ttabs",
|
|
52
|
+
"รmojis: ๐โญ๐",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
for (const testString of testStrings) {
|
|
56
|
+
const result = convertInputToNode(testString, shape)
|
|
57
|
+
expect(isLoroText(result as any)).toBe(true)
|
|
58
|
+
const text = result as LoroText
|
|
59
|
+
expect(text.toString()).toBe(testString)
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it("should throw error for non-string input", () => {
|
|
64
|
+
const shape = Shape.text()
|
|
65
|
+
|
|
66
|
+
expect(() => convertInputToNode(123 as any, shape)).toThrow(
|
|
67
|
+
"string expected",
|
|
68
|
+
)
|
|
69
|
+
expect(() => convertInputToNode(null as any, shape)).toThrow(
|
|
70
|
+
"string expected",
|
|
71
|
+
)
|
|
72
|
+
expect(() => convertInputToNode([] as any, shape)).toThrow(
|
|
73
|
+
"string expected",
|
|
74
|
+
)
|
|
75
|
+
expect(() => convertInputToNode({} as any, shape)).toThrow(
|
|
76
|
+
"string expected",
|
|
77
|
+
)
|
|
78
|
+
expect(() => convertInputToNode(true as any, shape)).toThrow(
|
|
79
|
+
"string expected",
|
|
80
|
+
)
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
describe("convertInputToNode - Counter Conversion", () => {
|
|
85
|
+
it("should convert number to LoroCounter", () => {
|
|
86
|
+
const shape = Shape.counter()
|
|
87
|
+
const result = convertInputToNode(42, shape)
|
|
88
|
+
|
|
89
|
+
expect(isContainer(result)).toBe(true)
|
|
90
|
+
expect(isLoroCounter(result as any)).toBe(true)
|
|
91
|
+
|
|
92
|
+
const counter = result as LoroCounter
|
|
93
|
+
expect(counter.value).toBe(42)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it("should handle zero value", () => {
|
|
97
|
+
const shape = Shape.counter()
|
|
98
|
+
const result = convertInputToNode(0, shape)
|
|
99
|
+
|
|
100
|
+
expect(isLoroCounter(result as any)).toBe(true)
|
|
101
|
+
const counter = result as LoroCounter
|
|
102
|
+
expect(counter.value).toBe(0)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it("should handle negative numbers", () => {
|
|
106
|
+
const shape = Shape.counter()
|
|
107
|
+
const result = convertInputToNode(-15, shape)
|
|
108
|
+
|
|
109
|
+
expect(isLoroCounter(result as any)).toBe(true)
|
|
110
|
+
const counter = result as LoroCounter
|
|
111
|
+
expect(counter.value).toBe(-15)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it("should handle floating point numbers", () => {
|
|
115
|
+
const shape = Shape.counter()
|
|
116
|
+
const result = convertInputToNode(3.14, shape)
|
|
117
|
+
|
|
118
|
+
expect(isLoroCounter(result as any)).toBe(true)
|
|
119
|
+
const counter = result as LoroCounter
|
|
120
|
+
expect(counter.value).toBe(3.14)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it("should throw error for non-number input", () => {
|
|
124
|
+
const shape = Shape.counter()
|
|
125
|
+
|
|
126
|
+
expect(() => convertInputToNode("123" as any, shape)).toThrow(
|
|
127
|
+
"number expected",
|
|
128
|
+
)
|
|
129
|
+
expect(() => convertInputToNode(null as any, shape)).toThrow(
|
|
130
|
+
"number expected",
|
|
131
|
+
)
|
|
132
|
+
expect(() => convertInputToNode([] as any, shape)).toThrow(
|
|
133
|
+
"number expected",
|
|
134
|
+
)
|
|
135
|
+
expect(() => convertInputToNode({} as any, shape)).toThrow(
|
|
136
|
+
"number expected",
|
|
137
|
+
)
|
|
138
|
+
expect(() => convertInputToNode(true as any, shape)).toThrow(
|
|
139
|
+
"number expected",
|
|
140
|
+
)
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
describe("convertInputToNode - List Conversion", () => {
|
|
145
|
+
it("should convert array to LoroList with value items", () => {
|
|
146
|
+
const shape = Shape.list(Shape.plain.string())
|
|
147
|
+
const result = convertInputToNode(["hello", "world"], shape)
|
|
148
|
+
|
|
149
|
+
expect(isContainer(result)).toBe(true)
|
|
150
|
+
expect(isLoroList(result as any)).toBe(true)
|
|
151
|
+
|
|
152
|
+
const list = result as LoroList
|
|
153
|
+
expect(list.length).toBe(2)
|
|
154
|
+
expect(list.get(0)).toBe("hello")
|
|
155
|
+
expect(list.get(1)).toBe("world")
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it("should convert array to LoroList with container items", () => {
|
|
159
|
+
const shape = Shape.list(Shape.text())
|
|
160
|
+
const result = convertInputToNode(["first", "second"], shape)
|
|
161
|
+
|
|
162
|
+
expect(isLoroList(result as any)).toBe(true)
|
|
163
|
+
const list = result as LoroList
|
|
164
|
+
expect(list.length).toBe(2)
|
|
165
|
+
|
|
166
|
+
// Items should be LoroText containers
|
|
167
|
+
const firstItem = list.get(0)
|
|
168
|
+
const secondItem = list.get(1)
|
|
169
|
+
expect((firstItem as Container).getShallowValue()).toBe("first")
|
|
170
|
+
expect((secondItem as Container).getShallowValue()).toBe("second")
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it("should handle empty array", () => {
|
|
174
|
+
const shape = Shape.list(Shape.plain.string())
|
|
175
|
+
const result = convertInputToNode([], shape)
|
|
176
|
+
|
|
177
|
+
expect(isLoroList(result as any)).toBe(true)
|
|
178
|
+
const list = result as LoroList
|
|
179
|
+
expect(list.length).toBe(0)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it("should handle mixed value types in list", () => {
|
|
183
|
+
const shape = Shape.list(Shape.plain.number())
|
|
184
|
+
const result = convertInputToNode([1, 2.5, -3, 0], shape)
|
|
185
|
+
|
|
186
|
+
expect(isLoroList(result as any)).toBe(true)
|
|
187
|
+
const list = result as LoroList
|
|
188
|
+
expect(list.length).toBe(4)
|
|
189
|
+
expect(list.get(0)).toBe(1)
|
|
190
|
+
expect(list.get(1)).toBe(2.5)
|
|
191
|
+
expect(list.get(2)).toBe(-3)
|
|
192
|
+
expect(list.get(3)).toBe(0)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it("should return plain array for value shape", () => {
|
|
196
|
+
const shape = Shape.plain.array(Shape.plain.string())
|
|
197
|
+
const input = ["hello", "world"]
|
|
198
|
+
const result = convertInputToNode(input, shape)
|
|
199
|
+
|
|
200
|
+
expect(isContainer(result)).toBe(false)
|
|
201
|
+
expect(Array.isArray(result)).toBe(true)
|
|
202
|
+
expect(result).toEqual(["hello", "world"])
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it("should handle nested container conversion", () => {
|
|
206
|
+
const shape = Shape.list(Shape.counter())
|
|
207
|
+
const result = convertInputToNode([5, 10, 15], shape)
|
|
208
|
+
|
|
209
|
+
expect(isLoroList(result as any)).toBe(true)
|
|
210
|
+
const list = result as LoroList
|
|
211
|
+
expect(list.length).toBe(3)
|
|
212
|
+
expect((list.get(0) as Container).getShallowValue()).toBe(5)
|
|
213
|
+
expect((list.get(1) as Container).getShallowValue()).toBe(10)
|
|
214
|
+
expect((list.get(2) as Container).getShallowValue()).toBe(15)
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it("should throw error for non-array input", () => {
|
|
218
|
+
const shape = Shape.list(Shape.plain.string())
|
|
219
|
+
|
|
220
|
+
expect(() => convertInputToNode("not array" as any, shape)).toThrow(
|
|
221
|
+
"array expected",
|
|
222
|
+
)
|
|
223
|
+
expect(() => convertInputToNode(123 as any, shape)).toThrow(
|
|
224
|
+
"array expected",
|
|
225
|
+
)
|
|
226
|
+
expect(() => convertInputToNode({} as any, shape)).toThrow(
|
|
227
|
+
"array expected",
|
|
228
|
+
)
|
|
229
|
+
expect(() => convertInputToNode(null as any, shape)).toThrow(
|
|
230
|
+
"array expected",
|
|
231
|
+
)
|
|
232
|
+
})
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
describe("convertInputToNode - MovableList Conversion", () => {
|
|
236
|
+
it("should convert array to LoroMovableList with value items", () => {
|
|
237
|
+
const shape = Shape.movableList(Shape.plain.string())
|
|
238
|
+
const result = convertInputToNode(["first", "second", "third"], shape)
|
|
239
|
+
|
|
240
|
+
expect(isContainer(result)).toBe(true)
|
|
241
|
+
expect(isLoroMovableList(result as any)).toBe(true)
|
|
242
|
+
|
|
243
|
+
const list = result as LoroMovableList
|
|
244
|
+
expect(list.length).toBe(3)
|
|
245
|
+
expect(list.get(0)).toBe("first")
|
|
246
|
+
expect(list.get(1)).toBe("second")
|
|
247
|
+
expect(list.get(2)).toBe("third")
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
it("should convert array to LoroMovableList with container items", () => {
|
|
251
|
+
const shape = Shape.movableList(Shape.counter())
|
|
252
|
+
const result = convertInputToNode([1, 5, 10], shape)
|
|
253
|
+
|
|
254
|
+
expect(isLoroMovableList(result as any)).toBe(true)
|
|
255
|
+
const list = result as LoroMovableList
|
|
256
|
+
expect(list.length).toBe(3)
|
|
257
|
+
expect((list.get(0) as Container).getShallowValue()).toBe(1)
|
|
258
|
+
expect((list.get(1) as Container).getShallowValue()).toBe(5)
|
|
259
|
+
expect((list.get(2) as Container).getShallowValue()).toBe(10)
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
it("should handle empty movable list", () => {
|
|
263
|
+
const shape = Shape.movableList(Shape.plain.boolean())
|
|
264
|
+
const result = convertInputToNode([], shape)
|
|
265
|
+
|
|
266
|
+
expect(isLoroMovableList(result as any)).toBe(true)
|
|
267
|
+
const list = result as LoroMovableList
|
|
268
|
+
expect(list.length).toBe(0)
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
it("should return plain array for value shape", () => {
|
|
272
|
+
const shape = Shape.plain.array(Shape.plain.number())
|
|
273
|
+
const input = [1, 2, 3]
|
|
274
|
+
const result = convertInputToNode(input, shape)
|
|
275
|
+
|
|
276
|
+
expect(isContainer(result)).toBe(false)
|
|
277
|
+
expect(Array.isArray(result)).toBe(true)
|
|
278
|
+
expect(result).toEqual([1, 2, 3])
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
it("should throw error for non-array input", () => {
|
|
282
|
+
const shape = Shape.movableList(Shape.plain.string())
|
|
283
|
+
|
|
284
|
+
expect(() => convertInputToNode("not array" as any, shape)).toThrow(
|
|
285
|
+
"array expected",
|
|
286
|
+
)
|
|
287
|
+
expect(() => convertInputToNode(123 as any, shape)).toThrow(
|
|
288
|
+
"array expected",
|
|
289
|
+
)
|
|
290
|
+
expect(() => convertInputToNode({} as any, shape)).toThrow(
|
|
291
|
+
"array expected",
|
|
292
|
+
)
|
|
293
|
+
expect(() => convertInputToNode(null as any, shape)).toThrow(
|
|
294
|
+
"array expected",
|
|
295
|
+
)
|
|
296
|
+
})
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
describe("convertInputToNode - Map Conversion", () => {
|
|
300
|
+
it("should convert object to LoroMap with value properties", () => {
|
|
301
|
+
const shape = Shape.map({
|
|
302
|
+
name: Shape.plain.string(),
|
|
303
|
+
age: Shape.plain.number(),
|
|
304
|
+
active: Shape.plain.boolean(),
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
const input = {
|
|
308
|
+
name: "John",
|
|
309
|
+
age: 30,
|
|
310
|
+
active: true,
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const result = convertInputToNode(input, shape)
|
|
314
|
+
|
|
315
|
+
expect(isContainer(result)).toBe(true)
|
|
316
|
+
expect(isLoroMap(result as any)).toBe(true)
|
|
317
|
+
|
|
318
|
+
const map = result as LoroMap
|
|
319
|
+
expect(map.get("name")).toBe("John")
|
|
320
|
+
expect(map.get("age")).toBe(30)
|
|
321
|
+
expect(map.get("active")).toBe(true)
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
it("should convert object to LoroMap with container properties", () => {
|
|
325
|
+
const shape = Shape.map({
|
|
326
|
+
title: Shape.text(),
|
|
327
|
+
count: Shape.counter(),
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
const input = {
|
|
331
|
+
title: "Hello World",
|
|
332
|
+
count: 42,
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const result = convertInputToNode(input, shape)
|
|
336
|
+
|
|
337
|
+
expect(isLoroMap(result as any)).toBe(true)
|
|
338
|
+
const map = result as LoroMap
|
|
339
|
+
expect((map.get("title") as Container).getShallowValue()).toBe(
|
|
340
|
+
"Hello World",
|
|
341
|
+
)
|
|
342
|
+
expect((map.get("count") as Container).getShallowValue()).toBe(42)
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
it("should handle empty object", () => {
|
|
346
|
+
const shape = Shape.map({})
|
|
347
|
+
const result = convertInputToNode({}, shape)
|
|
348
|
+
|
|
349
|
+
expect(isLoroMap(result as any)).toBe(true)
|
|
350
|
+
const map = result as LoroMap
|
|
351
|
+
expect(map.size).toBe(0)
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
it("should handle object with extra properties not in schema", () => {
|
|
355
|
+
const shape = Shape.map({
|
|
356
|
+
name: Shape.plain.string(),
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
const input = {
|
|
360
|
+
name: "John",
|
|
361
|
+
extraProp: "should be ignored", // This should be set as-is
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const result = convertInputToNode(input, shape)
|
|
365
|
+
|
|
366
|
+
expect(isLoroMap(result as any)).toBe(true)
|
|
367
|
+
const map = result as LoroMap
|
|
368
|
+
expect(map.get("name")).toBe("John")
|
|
369
|
+
// Note: The conversion function has a bug on line 117 - it sets `value` instead of `v`
|
|
370
|
+
// This test documents the current behavior
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
it("should handle nested map structures", () => {
|
|
374
|
+
const shape = Shape.map({
|
|
375
|
+
user: Shape.map({
|
|
376
|
+
name: Shape.plain.string(),
|
|
377
|
+
profile: Shape.map({
|
|
378
|
+
bio: Shape.text(),
|
|
379
|
+
}),
|
|
380
|
+
}),
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
const input = {
|
|
384
|
+
user: {
|
|
385
|
+
name: "Alice",
|
|
386
|
+
profile: {
|
|
387
|
+
bio: "Software developer",
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const result = convertInputToNode(input, shape)
|
|
393
|
+
|
|
394
|
+
expect(isLoroMap(result as any)).toBe(true)
|
|
395
|
+
const map = result as LoroMap
|
|
396
|
+
const user = map.get("user")
|
|
397
|
+
expect(user).toBeDefined()
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
it("should return plain object for value shape", () => {
|
|
401
|
+
const shape = Shape.plain.object({
|
|
402
|
+
name: Shape.plain.string(),
|
|
403
|
+
age: Shape.plain.number(),
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
const input = { name: "John", age: 30 }
|
|
407
|
+
const result = convertInputToNode(input, shape)
|
|
408
|
+
|
|
409
|
+
expect(isContainer(result)).toBe(false)
|
|
410
|
+
expect(typeof result).toBe("object")
|
|
411
|
+
expect(result).toEqual({ name: "John", age: 30 })
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
it("should throw error for non-object input", () => {
|
|
415
|
+
const shape = Shape.map({
|
|
416
|
+
name: Shape.plain.string(),
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
expect(() => convertInputToNode("not object" as any, shape)).toThrow(
|
|
420
|
+
"object expected",
|
|
421
|
+
)
|
|
422
|
+
expect(() => convertInputToNode(123 as any, shape)).toThrow(
|
|
423
|
+
"object expected",
|
|
424
|
+
)
|
|
425
|
+
expect(() => convertInputToNode([] as any, shape)).toThrow(
|
|
426
|
+
"object expected",
|
|
427
|
+
)
|
|
428
|
+
expect(() => convertInputToNode(null as any, shape)).toThrow(
|
|
429
|
+
"object expected",
|
|
430
|
+
)
|
|
431
|
+
})
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
describe("convertInputToNode - Value Types", () => {
|
|
435
|
+
it("should return value as-is for value shapes", () => {
|
|
436
|
+
const stringShape = Shape.plain.string()
|
|
437
|
+
const numberShape = Shape.plain.number()
|
|
438
|
+
const booleanShape = Shape.plain.boolean()
|
|
439
|
+
const nullShape = Shape.plain.null()
|
|
440
|
+
|
|
441
|
+
expect(convertInputToNode("hello", stringShape)).toBe("hello")
|
|
442
|
+
expect(convertInputToNode(42, numberShape)).toBe(42)
|
|
443
|
+
expect(convertInputToNode(true, booleanShape)).toBe(true)
|
|
444
|
+
expect(convertInputToNode(null, nullShape)).toBe(null)
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
it("should handle complex value shapes", () => {
|
|
448
|
+
const arrayShape = Shape.plain.array(Shape.plain.string())
|
|
449
|
+
const objectShape = Shape.plain.object({
|
|
450
|
+
name: Shape.plain.string(),
|
|
451
|
+
count: Shape.plain.number(),
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
const arrayInput = ["a", "b", "c"]
|
|
455
|
+
const objectInput = { name: "test", count: 5 }
|
|
456
|
+
|
|
457
|
+
expect(convertInputToNode(arrayInput, arrayShape)).toEqual(arrayInput)
|
|
458
|
+
expect(convertInputToNode(objectInput, objectShape)).toEqual(objectInput)
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
it("should handle Uint8Array values", () => {
|
|
462
|
+
const shape = Shape.plain.uint8Array()
|
|
463
|
+
const input = new Uint8Array([1, 2, 3, 4])
|
|
464
|
+
|
|
465
|
+
const result = convertInputToNode(input, shape)
|
|
466
|
+
expect(result).toBe(input)
|
|
467
|
+
expect(result instanceof Uint8Array).toBe(true)
|
|
468
|
+
})
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
describe("convertInputToNode - Error Cases", () => {
|
|
472
|
+
it("should throw error for tree type (unimplemented)", () => {
|
|
473
|
+
const shape = Shape.tree(Shape.map({}))
|
|
474
|
+
|
|
475
|
+
expect(() => convertInputToNode({}, shape)).toThrow(
|
|
476
|
+
"tree type unimplemented",
|
|
477
|
+
)
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
it("should throw error for invalid value shape", () => {
|
|
481
|
+
const invalidShape = { _type: "value", valueType: "invalid" } as any
|
|
482
|
+
|
|
483
|
+
expect(() => convertInputToNode("test", invalidShape)).toThrow(
|
|
484
|
+
"value expected",
|
|
485
|
+
)
|
|
486
|
+
})
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
describe("convertInputToNode - Complex Nested Structures", () => {
|
|
490
|
+
it("should handle deeply nested container structures", () => {
|
|
491
|
+
const shape = Shape.list(
|
|
492
|
+
Shape.map({
|
|
493
|
+
title: Shape.text(),
|
|
494
|
+
metadata: Shape.map({
|
|
495
|
+
views: Shape.counter(),
|
|
496
|
+
tags: Shape.list(Shape.plain.string()),
|
|
497
|
+
}),
|
|
498
|
+
}),
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
const input = [
|
|
502
|
+
{
|
|
503
|
+
title: "Article 1",
|
|
504
|
+
metadata: {
|
|
505
|
+
views: 100,
|
|
506
|
+
tags: ["tech", "programming"],
|
|
507
|
+
},
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
title: "Article 2",
|
|
511
|
+
metadata: {
|
|
512
|
+
views: 50,
|
|
513
|
+
tags: ["design"],
|
|
514
|
+
},
|
|
515
|
+
},
|
|
516
|
+
]
|
|
517
|
+
|
|
518
|
+
const result = convertInputToNode(input, shape)
|
|
519
|
+
|
|
520
|
+
expect(isLoroList(result as any)).toBe(true)
|
|
521
|
+
const list = result as LoroList
|
|
522
|
+
expect(list.length).toBe(2)
|
|
523
|
+
})
|
|
524
|
+
|
|
525
|
+
it("should handle mixed container and value types", () => {
|
|
526
|
+
const shape = Shape.map({
|
|
527
|
+
plainString: Shape.plain.string(),
|
|
528
|
+
plainArray: Shape.plain.array(Shape.plain.number()),
|
|
529
|
+
loroText: Shape.text(),
|
|
530
|
+
loroList: Shape.list(Shape.plain.string()),
|
|
531
|
+
nestedMap: Shape.map({
|
|
532
|
+
counter: Shape.counter(),
|
|
533
|
+
plainBool: Shape.plain.boolean(),
|
|
534
|
+
}),
|
|
535
|
+
})
|
|
536
|
+
|
|
537
|
+
const input = {
|
|
538
|
+
plainString: "hello",
|
|
539
|
+
plainArray: [1, 2, 3],
|
|
540
|
+
loroText: "loro text content",
|
|
541
|
+
loroList: ["item1", "item2"],
|
|
542
|
+
nestedMap: {
|
|
543
|
+
counter: 42,
|
|
544
|
+
plainBool: true,
|
|
545
|
+
},
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const result = convertInputToNode(input, shape)
|
|
549
|
+
|
|
550
|
+
expect(isLoroMap(result as any)).toBe(true)
|
|
551
|
+
const map = result as LoroMap
|
|
552
|
+
expect(map.get("plainString")).toBe("hello")
|
|
553
|
+
expect(map.get("plainArray")).toEqual([1, 2, 3])
|
|
554
|
+
expect((map.get("loroText") as Container).toString()).toBe(
|
|
555
|
+
"loro text content",
|
|
556
|
+
)
|
|
557
|
+
})
|
|
558
|
+
|
|
559
|
+
it("should handle lists of lists", () => {
|
|
560
|
+
const shape = Shape.list(Shape.list(Shape.plain.number()))
|
|
561
|
+
const input = [
|
|
562
|
+
[1, 2, 3],
|
|
563
|
+
[4, 5, 6],
|
|
564
|
+
[7, 8, 9],
|
|
565
|
+
]
|
|
566
|
+
|
|
567
|
+
const result = convertInputToNode(input, shape)
|
|
568
|
+
|
|
569
|
+
expect(isLoroList(result as any)).toBe(true)
|
|
570
|
+
const outerList = result as LoroList
|
|
571
|
+
expect(outerList.length).toBe(3)
|
|
572
|
+
})
|
|
573
|
+
|
|
574
|
+
it("should handle movable lists with complex items", () => {
|
|
575
|
+
const shape = Shape.movableList(
|
|
576
|
+
Shape.map({
|
|
577
|
+
id: Shape.plain.string(),
|
|
578
|
+
title: Shape.text(),
|
|
579
|
+
completed: Shape.plain.boolean(),
|
|
580
|
+
}),
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
const input = [
|
|
584
|
+
{ id: "1", title: "Task 1", completed: false },
|
|
585
|
+
{ id: "2", title: "Task 2", completed: true },
|
|
586
|
+
]
|
|
587
|
+
|
|
588
|
+
const result = convertInputToNode(input, shape)
|
|
589
|
+
|
|
590
|
+
expect(isLoroMovableList(result as any)).toBe(true)
|
|
591
|
+
const list = result as LoroMovableList
|
|
592
|
+
expect(list.length).toBe(2)
|
|
593
|
+
})
|
|
594
|
+
})
|
|
595
|
+
|
|
596
|
+
describe("convertInputToNode - Edge Cases", () => {
|
|
597
|
+
it("should handle null and undefined values appropriately", () => {
|
|
598
|
+
const nullShape = Shape.plain.null()
|
|
599
|
+
const undefinedShape = Shape.plain.undefined()
|
|
600
|
+
|
|
601
|
+
expect(convertInputToNode(null, nullShape)).toBe(null)
|
|
602
|
+
expect(convertInputToNode(undefined, undefinedShape)).toBe(undefined)
|
|
603
|
+
})
|
|
604
|
+
|
|
605
|
+
it("should handle empty containers", () => {
|
|
606
|
+
const emptyListShape = Shape.list(Shape.plain.string())
|
|
607
|
+
const emptyMapShape = Shape.map({})
|
|
608
|
+
const emptyMovableListShape = Shape.movableList(Shape.plain.number())
|
|
609
|
+
|
|
610
|
+
const emptyList = convertInputToNode([], emptyListShape)
|
|
611
|
+
const emptyMap = convertInputToNode({}, emptyMapShape)
|
|
612
|
+
const emptyMovableList = convertInputToNode([], emptyMovableListShape)
|
|
613
|
+
|
|
614
|
+
expect(isLoroList(emptyList as any)).toBe(true)
|
|
615
|
+
expect((emptyList as LoroList).length).toBe(0)
|
|
616
|
+
|
|
617
|
+
expect(isLoroMap(emptyMap as any)).toBe(true)
|
|
618
|
+
expect((emptyMap as LoroMap).size).toBe(0)
|
|
619
|
+
|
|
620
|
+
expect(isLoroMovableList(emptyMovableList as any)).toBe(true)
|
|
621
|
+
expect((emptyMovableList as LoroMovableList).length).toBe(0)
|
|
622
|
+
})
|
|
623
|
+
|
|
624
|
+
it("should handle very large numbers", () => {
|
|
625
|
+
const shape = Shape.counter()
|
|
626
|
+
const largeNumber = Number.MAX_SAFE_INTEGER
|
|
627
|
+
const result = convertInputToNode(largeNumber, shape)
|
|
628
|
+
|
|
629
|
+
expect(isLoroCounter(result as any)).toBe(true)
|
|
630
|
+
expect((result as LoroCounter).value).toBe(largeNumber)
|
|
631
|
+
})
|
|
632
|
+
|
|
633
|
+
it("should handle very long strings", () => {
|
|
634
|
+
const shape = Shape.text()
|
|
635
|
+
const longString = "a".repeat(10000)
|
|
636
|
+
const result = convertInputToNode(longString, shape)
|
|
637
|
+
|
|
638
|
+
expect(isLoroText(result as any)).toBe(true)
|
|
639
|
+
expect((result as LoroText).toString()).toBe(longString)
|
|
640
|
+
expect((result as LoroText).length).toBe(10000)
|
|
641
|
+
})
|
|
642
|
+
|
|
643
|
+
it("should handle arrays with many items", () => {
|
|
644
|
+
const shape = Shape.list(Shape.plain.number())
|
|
645
|
+
const largeArray = Array.from({ length: 1000 }, (_, i) => i)
|
|
646
|
+
const result = convertInputToNode(largeArray, shape)
|
|
647
|
+
|
|
648
|
+
expect(isLoroList(result as any)).toBe(true)
|
|
649
|
+
const list = result as LoroList
|
|
650
|
+
expect(list.length).toBe(1000)
|
|
651
|
+
expect(list.get(0)).toBe(0)
|
|
652
|
+
expect(list.get(999)).toBe(999)
|
|
653
|
+
})
|
|
654
|
+
|
|
655
|
+
it("should handle objects with many properties", () => {
|
|
656
|
+
const shapes: Record<string, any> = {}
|
|
657
|
+
const input: Record<string, any> = {}
|
|
658
|
+
|
|
659
|
+
// Create 100 properties
|
|
660
|
+
for (let i = 0; i < 100; i++) {
|
|
661
|
+
shapes[`prop${i}`] = Shape.plain.string()
|
|
662
|
+
input[`prop${i}`] = `value${i}`
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const shape = Shape.map(shapes)
|
|
666
|
+
const result = convertInputToNode(input, shape)
|
|
667
|
+
|
|
668
|
+
expect(isLoroMap(result as any)).toBe(true)
|
|
669
|
+
const map = result as LoroMap
|
|
670
|
+
expect(map.size).toBe(100)
|
|
671
|
+
expect(map.get("prop0")).toBe("value0")
|
|
672
|
+
expect(map.get("prop99")).toBe("value99")
|
|
673
|
+
})
|
|
674
|
+
})
|
|
675
|
+
|
|
676
|
+
describe("convertInputToNode - Type Safety", () => {
|
|
677
|
+
it("should maintain referential integrity for containers", () => {
|
|
678
|
+
const shape = Shape.list(Shape.text())
|
|
679
|
+
const result = convertInputToNode(["test"], shape)
|
|
680
|
+
|
|
681
|
+
expect(isLoroList(result as any)).toBe(true)
|
|
682
|
+
const list = result as LoroList
|
|
683
|
+
|
|
684
|
+
// The container should be a new instance
|
|
685
|
+
expect(list).toBeInstanceOf(LoroList)
|
|
686
|
+
expect(list.id).toBeDefined()
|
|
687
|
+
})
|
|
688
|
+
|
|
689
|
+
it("should create independent container instances", () => {
|
|
690
|
+
const shape = Shape.counter()
|
|
691
|
+
const result1 = convertInputToNode(5, shape)
|
|
692
|
+
const result2 = convertInputToNode(5, shape)
|
|
693
|
+
|
|
694
|
+
expect(isLoroCounter(result1 as any)).toBe(true)
|
|
695
|
+
expect(isLoroCounter(result2 as any)).toBe(true)
|
|
696
|
+
|
|
697
|
+
const counter1 = result1 as LoroCounter
|
|
698
|
+
const counter2 = result2 as LoroCounter
|
|
699
|
+
|
|
700
|
+
// Should be the same IDs since the containers are detached
|
|
701
|
+
expect(counter1.id).toBe(counter2.id)
|
|
702
|
+
|
|
703
|
+
// Should be different instances
|
|
704
|
+
expect(counter1).not.toBe(counter2)
|
|
705
|
+
|
|
706
|
+
expect(counter1.value).toBe(counter2.value) // Same value though
|
|
707
|
+
})
|
|
708
|
+
|
|
709
|
+
it("should handle recursive structures without infinite loops", () => {
|
|
710
|
+
// Test that the conversion doesn't get stuck in infinite recursion
|
|
711
|
+
const shape = Shape.map({
|
|
712
|
+
name: Shape.plain.string(),
|
|
713
|
+
children: Shape.list(Shape.plain.string()), // Not recursive, but nested
|
|
714
|
+
})
|
|
715
|
+
|
|
716
|
+
const input = {
|
|
717
|
+
name: "parent",
|
|
718
|
+
children: ["child1", "child2"],
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const result = convertInputToNode(input, shape)
|
|
722
|
+
|
|
723
|
+
expect(isLoroMap(result as any)).toBe(true)
|
|
724
|
+
const map = result as LoroMap
|
|
725
|
+
expect(map.get("name")).toBe("parent")
|
|
726
|
+
})
|
|
727
|
+
})
|
|
728
|
+
})
|