Y_test.res
1// test/Y_test.res
2open VitestHelpers
3
4// Record types for unmarshalling tests
5type route = {from: string, to: string, distance: int}
6
7type tripInfo = {
8 origin: string,
9 destination: string,
10 distanceKm: int,
11}
12
13// Helper to insert text via raw JS (bindings are read-only)
14let insertText = (ytext: Y.Text.t, index: int, text: string): unit => {
15 let _ = %raw(`(yt, idx, str) => { yt.insert(idx, str); }`)(ytext, index, text)
16}
17
18// Helper to push to Y.Array via raw JS
19let pushArray = (yarray: Y.Array.t, values: array<'a>): unit => {
20 let _ = %raw(`(ya, vals) => { ya.push(vals); }`)(yarray, values)
21}
22
23// Helper to set on Y.Map via raw JS
24let setMap = (ymap: Y.Map.t, key: string, value: 'a): unit => {
25 let _ = %raw(`(ym, k, v) => { ym.set(k, v); }`)(ymap, key, value)
26}
27
28describe("Y.Doc", () => {
29 testAsync("make creates a doc instance", async t => {
30 let doc = Y.Doc.make()
31 t->expect(doc)->not_->toBeNull
32 })
33
34 testAsync("onAfterTransaction fires after a transaction", async t => {
35 let doc = Y.Doc.make()
36 let events = ref([])
37 doc->Y.Doc.onAfterTransaction(event => {
38 events := events.contents->Array.concat([event])
39 })
40 // Modify content to trigger a transaction
41 let ytext = doc->Y.Text.get("trigger")
42 insertText(ytext, 0, "test")
43 t->expect(events.contents->Array.length)->toBeGreaterThan(0)
44 })
45
46 testAsync("onAfterTransaction receives doc and transaction", async t => {
47 let doc = Y.Doc.make()
48 let receivedDoc = ref(None)
49 doc->Y.Doc.onAfterTransaction(event => {
50 receivedDoc := Some(event.doc)
51 })
52 let _ = doc->Y.Text.get("trigger")
53 t->expect(receivedDoc.contents)->not_->toBeNull
54 })
55})
56
57describe("Y.Text", () => {
58 testAsync("get returns a Y.Text instance", async t => {
59 let doc = Y.Doc.make()
60 let ytext = doc->Y.Text.get("mytext")
61 t->expect(ytext)->not_->toBeNull
62 })
63
64 testAsync("toString returns string content", async t => {
65 let doc = Y.Doc.make()
66 let ytext = doc->Y.Text.get("mytext")
67 insertText(ytext, 0, "hello world")
68 let result = ytext->Y.Text.toString
69 t->expect(result)->toEqual("hello world")
70 })
71
72 testAsync("multiple text types in one doc", async t => {
73 let doc = Y.Doc.make()
74 let title = doc->Y.Text.get("title")
75 let body = doc->Y.Text.get("body")
76 insertText(title, 0, "Trip Plan")
77 insertText(body, 0, "Details here...")
78 t->expect(title->Y.Text.toString)->toEqual("Trip Plan")
79 t->expect(body->Y.Text.toString)->toEqual("Details here...")
80 })
81})
82
83describe("Y.Array", () => {
84 testAsync("get returns a Y.Array instance", async t => {
85 let doc = Y.Doc.make()
86 let yarray = doc->Y.Array.get("myarray")
87 t->expect(yarray)->not_->toBeNull
88 })
89
90 testAsync("toJSON returns array of JSON values", async t => {
91 let doc = Y.Doc.make()
92 let yarray = doc->Y.Array.get("myarray")
93 pushArray(yarray, [1, 2, 3])
94 let result = yarray->Y.Array.toJSON
95 t->expect(result->Array.length)->toEqual(3)
96 t->expect(result->Array.get(0)->Option.flatMap(JSON.Decode.float))->toEqual(Some(1.0))
97 t->expect(result->Array.get(1)->Option.flatMap(JSON.Decode.float))->toEqual(Some(2.0))
98 t->expect(result->Array.get(2)->Option.flatMap(JSON.Decode.float))->toEqual(Some(3.0))
99 })
100
101 testAsync("toJSON with objects can be unmarshalled to records", async t => {
102 let doc = Y.Doc.make()
103 let yarray = doc->Y.Array.get("routes")
104
105 // Push JS objects into the array via raw JS
106 let pushRouteFn: Y.Array.t => unit = %raw(`function(ya) {
107 ya.push([{from: 'A', to: 'B', distance: 120}, {from: 'B', to: 'C', distance: 85}]);
108 }`)
109 pushRouteFn(yarray)
110
111 let routes: array<route> = yarray->Y.Array.toJSON->Array.map(json => {
112 switch json->JSON.Decode.object {
113 | Some(dict) => {
114 let from = dict->Dict.get("from")->Option.flatMap(JSON.Decode.string)->Option.getOr("")
115 let to = dict->Dict.get("to")->Option.flatMap(JSON.Decode.string)->Option.getOr("")
116 let distance = dict->Dict.get("distance")->Option.flatMap(JSON.Decode.float)->Option.map(Float.toInt)->Option.getOr(0)
117 {from: from, to: to, distance: distance}
118 }
119 | None => {from: "", to: "", distance: 0}
120 }
121 })
122
123 t->expect(routes->Array.length)->toEqual(2)
124 t->expect(routes->Array.get(0))->toEqual(Some({from: "A", to: "B", distance: 120}))
125 t->expect(routes->Array.get(1))->toEqual(Some({from: "B", to: "C", distance: 85}))
126 })
127})
128
129describe("Y.Map", () => {
130 testAsync("get returns a Y.Map instance", async t => {
131 let doc = Y.Doc.make()
132 let ymap = doc->Y.Map.get("mymap")
133 t->expect(ymap)->not_->toBeNull
134 })
135
136 testAsync("toJSON returns JSON object", async t => {
137 let doc = Y.Doc.make()
138 let ymap = doc->Y.Map.get("mymap")
139 setMap(ymap, "key", "value")
140 let result = ymap->Y.Map.toJSON
141 switch result->JSON.Decode.object {
142 | Some(dict) =>
143 switch dict->Dict.get("key") {
144 | Some(v) =>
145 switch v->JSON.Decode.string {
146 | Some("value") => t->expect(true)->toEqual(true)
147 | _ => t->expect("decode string failed")->toEqual("")
148 }
149 | None => t->expect("key missing")->toEqual("")
150 }
151 | None => t->expect("not object")->toEqual("")
152 }
153 })
154
155 testAsync("toJSON can be unmarshalled to a record", async t => {
156 let doc = Y.Doc.make()
157 let ymap = doc->Y.Map.get("tripInfo")
158
159 // Set nested values via raw JS
160 let setTripInfoFn: Y.Map.t => unit = %raw(`function(ym) {
161 ym.set('origin', 'Kathmandu');
162 ym.set('destination', 'Pokhara');
163 ym.set('distanceKm', 200);
164 }`)
165 setTripInfoFn(ymap)
166
167 let info = switch ymap->Y.Map.toJSON->JSON.Decode.object {
168 | Some(dict) => {
169 origin: dict->Dict.get("origin")->Option.flatMap(JSON.Decode.string)->Option.getOr(""),
170 destination: dict->Dict.get("destination")->Option.flatMap(JSON.Decode.string)->Option.getOr(""),
171 distanceKm: dict->Dict.get("distanceKm")->Option.flatMap(JSON.Decode.float)->Option.map(Float.toInt)->Option.getOr(0),
172 }
173 | None => {origin: "", destination: "", distanceKm: 0}
174 }
175
176 t->expect(info.origin)->toEqual("Kathmandu")
177 t->expect(info.destination)->toEqual("Pokhara")
178 t->expect(info.distanceKm)->toEqual(200)
179 })
180})
181
182describe("Y.Doc with multiple shared types", () => {
183 testAsync("doc can hold Text, Array, and Map simultaneously", async t => {
184 let doc = Y.Doc.make()
185
186 let ytext = doc->Y.Text.get("notes")
187 let yarray = doc->Y.Array.get("waypoints")
188 let ymap = doc->Y.Map.get("metadata")
189
190 insertText(ytext, 0, "Day 1: Kathmandu to Pokhara")
191 pushArray(yarray, ["Kathmandu", "Pokhara"])
192 setMap(ymap, "author", "Rider One")
193
194 t->expect(ytext->Y.Text.toString)->toEqual("Day 1: Kathmandu to Pokhara")
195 t->expect(yarray->Y.Array.toJSON->Array.length)->toEqual(2)
196
197 switch ymap->Y.Map.toJSON->JSON.Decode.object {
198 | Some(dict) =>
199 switch dict->Dict.get("author")->Option.flatMap(JSON.Decode.string) {
200 | Some("Rider One") => t->expect(true)->toEqual(true)
201 | _ => t->expect("author decode failed")->toEqual("")
202 }
203 | None => t->expect("not object")->toEqual("")
204 }
205 })
206
207 testAsync("onAfterTransaction fires for any shared type change", async t => {
208 let doc = Y.Doc.make()
209 let changeCount = ref(0)
210 doc->Y.Doc.onAfterTransaction(_event => {
211 changeCount := changeCount.contents + 1
212 })
213
214 let _ = doc->Y.Text.get("text1")
215 let _ = doc->Y.Array.get("array1")
216 let _ = doc->Y.Map.get("map1")
217
218 t->expect(changeCount.contents)->toBeGreaterThan(0)
219 })
220})