useDocument.test.tsx
1 import {PeerId, Repo, type AutomergeUrl} from "@automerge/automerge-repo" 2 import {render, renderHook, testEffect} from "@solidjs/testing-library" 3 import {describe, expect, it, vi} from "vitest" 4 import {RepoContext} from "../src/context.js" 5 import { 6 createEffect, 7 createSignal, 8 type Accessor, 9 type ParentComponent, 10 } from "solid-js" 11 import useDocument from "../src/useDocument.js" 12 13 describe("useDocument", () => { 14 function setup() { 15 const repo = new Repo({ 16 peerId: "bob" as PeerId, 17 }) 18 19 const create = () => 20 repo.create<ExampleDoc>({ 21 key: "value", 22 array: [1, 2, 3], 23 hellos: [{hello: "world"}, {hello: "hedgehog"}], 24 projects: [ 25 {title: "one", items: [{title: "go shopping"}]}, 26 {title: "two", items: []}, 27 ], 28 }) 29 30 const handle = create() 31 const wrapper: ParentComponent = props => { 32 return ( 33 <RepoContext.Provider value={repo}> 34 {props.children} 35 </RepoContext.Provider> 36 ) 37 } 38 39 return { 40 repo, 41 handle, 42 wrapper, 43 create, 44 options: {repo}, 45 } 46 } 47 48 it("should notify on a property change", async () => { 49 const {create, options} = setup() 50 51 await testEffect(done => { 52 const [doc, handle] = useDocument<ExampleDoc>(create().url, options) 53 createEffect((run: number = 0) => { 54 if (run == 0) { 55 expect(doc()?.key).toBe("value") 56 handle()?.change(doc => (doc.key = "hello world!")) 57 } else if (run == 1) { 58 expect(doc()?.key).toBe("hello world!") 59 handle()?.change(doc => (doc.key = "friday night!")) 60 } else if (run == 2) { 61 expect(doc()?.key).toBe("friday night!") 62 done() 63 } 64 return run + 1 65 }) 66 }) 67 }) 68 69 it("should not apply patches multiple times just because there are multiple projections", async () => { 70 const { 71 handle: {url}, 72 options, 73 } = setup() 74 75 const done2 = testEffect(done => { 76 const [two, handle] = useDocument<ExampleDoc>(url, options) 77 createEffect((run: number = 0) => { 78 if (run == 0) { 79 expect(two()?.array).toEqual([1, 2, 3]) 80 } else if (run == 1) { 81 expect(two()?.array).toEqual([1, 2, 3, 4]) 82 handle()?.change(doc => doc.array.push(5)) 83 } else if (run == 2) { 84 expect(two()?.array).toEqual([1, 2, 3, 4, 5]) 85 done() 86 } 87 return run + 1 88 }) 89 }) 90 91 const done1 = testEffect(done => { 92 const [one, handle] = useDocument<ExampleDoc>(url, options) 93 createEffect((run: number = 0) => { 94 if (run == 0) { 95 expect(one()?.array).toEqual([1, 2, 3]) 96 handle()?.change(doc => doc.array.push(4)) 97 } else if (run == 1) { 98 expect(one()?.array).toEqual([1, 2, 3, 4]) 99 } else if (run == 2) { 100 expect(one()?.array).toEqual([1, 2, 3, 4, 5]) 101 done() 102 } 103 return run + 1 104 }) 105 }) 106 107 return Promise.allSettled([done1, done2]) 108 }) 109 110 it("should work with a signal url", async () => { 111 const {create, wrapper} = setup() 112 const [url, setURL] = createSignal<AutomergeUrl>() 113 const { 114 result: [doc, handle], 115 owner, 116 } = renderHook(useDocument<ExampleDoc>, { 117 initialProps: [url], 118 wrapper, 119 }) 120 const done = testEffect(done => { 121 createEffect((run: number = 0) => { 122 if (run == 0) { 123 expect(doc()?.key).toBe(undefined) 124 setURL(create().url) 125 } else if (run == 1) { 126 expect(doc()?.key).toBe("value") 127 handle()?.change(doc => (doc.key = "hello world!")) 128 } else if (run == 2) { 129 expect(doc()?.key).toBe("hello world!") 130 setURL(create().url) 131 } else if (run == 3) { 132 expect(doc()?.key).toBe("value") 133 handle()?.change(doc => (doc.key = "friday night!")) 134 } else if (run == 4) { 135 expect(doc()?.key).toBe("friday night!") 136 done() 137 } 138 139 return run + 1 140 }) 141 }, owner!) 142 return done 143 }) 144 145 it("should clear the store when the url signal returns to nothing", async () => { 146 const {create, options} = setup() 147 const [url, setURL] = createSignal<AutomergeUrl>() 148 149 const done = testEffect(done => { 150 const [doc, handle] = useDocument<ExampleDoc>(url, options) 151 createEffect((run: number = 0) => { 152 if (run == 0) { 153 expect(doc()?.key).toBe(undefined) 154 expect(handle()).toBe(undefined) 155 setURL(create().url) 156 } else if (run == 1) { 157 expect(doc()?.key).toBe("value") 158 expect(handle()).not.toBe(undefined) 159 setURL(undefined) 160 } else if (run == 2) { 161 expect(doc()?.key).toBe(undefined) 162 expect(handle()).toBe(undefined) 163 setURL(create().url) 164 } else if (run == 3) { 165 expect(doc()?.key).toBe("value") 166 expect(handle()).not.toBe(undefined) 167 done() 168 } 169 170 return run + 1 171 }) 172 }) 173 return done 174 }) 175 176 it("should not return the wrong store when url changes", async () => { 177 const {create, repo} = setup() 178 const h1 = create() 179 const h2 = create() 180 const u1 = h1.url 181 const u2 = h2.url 182 183 const [stableURL] = createSignal(u1) 184 const [changingURL, setChangingURL] = createSignal(u1) 185 186 await testEffect(async done => { 187 const result = render(() => { 188 function Component(props: { 189 stableURL: Accessor<AutomergeUrl> 190 changingURL: Accessor<AutomergeUrl> 191 }) { 192 const [stableDoc] = useDocument<ExampleDoc>(() => props.stableURL()) 193 194 const [changingDoc] = useDocument<ExampleDoc>(() => 195 props.changingURL() 196 ) 197 198 return ( 199 <> 200 <div data-testid="key-stable">{stableDoc()?.key}</div> 201 <div data-testid="key-changing">{changingDoc()?.key}</div> 202 </> 203 ) 204 } 205 206 return ( 207 <RepoContext.Provider value={repo}> 208 <Component stableURL={stableURL} changingURL={changingURL} /> 209 </RepoContext.Provider> 210 ) 211 }) 212 213 h2.change(doc => (doc.key = "document-2")) 214 expect(result.getByTestId("key-stable").textContent).toBe("value") 215 expect(result.getByTestId("key-changing").textContent).toBe("value") 216 217 h1.change(doc => (doc.key = "hello")) 218 await new Promise(yay => setImmediate(yay)) 219 220 expect(result.getByTestId("key-stable").textContent).toBe("hello") 221 expect(result.getByTestId("key-changing").textContent).toBe("hello") 222 223 setChangingURL(u2) 224 await new Promise(yay => setImmediate(yay)) 225 expect(result.getByTestId("key-stable").textContent).toBe("hello") 226 expect(result.getByTestId("key-changing").textContent).toBe("document-2") 227 h2.change(doc => (doc.key = "world")) 228 229 setChangingURL(u1) 230 await new Promise(yay => setImmediate(yay)) 231 expect(result.getByTestId("key-stable").textContent).toBe("hello") 232 expect(result.getByTestId("key-changing").textContent).toBe("hello") 233 234 setChangingURL(u2) 235 await new Promise(yay => setImmediate(yay)) 236 237 expect(result.getByTestId("key-stable").textContent).toBe("hello") 238 expect(result.getByTestId("key-changing").textContent).toBe("world") 239 240 done() 241 }) 242 }) 243 244 it("should work with a slow handle", async () => { 245 const {repo} = setup() 246 247 const slowHandle = repo.create({im: "slow"}) 248 const originalFind = repo.find.bind(repo) 249 repo.find = vi.fn().mockImplementation(async (...args) => { 250 await new Promise(resolve => setTimeout(resolve, 100)) 251 // @ts-expect-error i'm ok i promise 252 return await originalFind(...args) 253 }) 254 255 const done = testEffect(done => { 256 const [doc] = useDocument<{im: "slow"}>(() => slowHandle.url, { 257 repo, 258 "~skipInitialValue": true, 259 }) 260 createEffect((run: number = 0) => { 261 if (run == 0) { 262 expect(doc()?.im).toBe(undefined) 263 } else if (run == 1) { 264 expect(doc()?.im).toBe("slow") 265 done() 266 } 267 return run + 1 268 }) 269 }) 270 repo.find = originalFind 271 return done 272 }) 273 274 it("should not notify on properties nobody cares about", async () => { 275 const { 276 handle: {url}, 277 options, 278 } = setup() 279 280 let fn = vi.fn() 281 282 const [doc, handle] = useDocument<ExampleDoc>(url, options) 283 284 testEffect(() => { 285 createEffect(() => { 286 fn(doc()?.projects[1].title) 287 }) 288 }) 289 const arrayDotThree = testEffect(done => { 290 createEffect((run: number = 0) => { 291 if (run == 0) { 292 expect(doc()?.array[3]).toBeUndefined() 293 handle()?.change(doc => (doc.array[2] = 22)) 294 handle()?.change(doc => (doc.key = "hello world!")) 295 handle()?.change(doc => (doc.array[1] = 11)) 296 handle()?.change(doc => (doc.array[3] = 145)) 297 } else if (run == 1) { 298 expect(doc()?.array[3]).toBe(145) 299 handle()?.change(doc => (doc.projects[0].title = "hello world!")) 300 handle()?.change( 301 doc => (doc.projects[0].items[0].title = "hello world!") 302 ) 303 handle()?.change(doc => (doc.array[3] = 147)) 304 } else if (run == 2) { 305 expect(doc()?.array[3]).toBe(147) 306 done() 307 } 308 return run + 1 309 }) 310 }) 311 312 const projectZeroItemZeroTitle = testEffect(done => { 313 createEffect((run: number = 0) => { 314 if (run == 0) { 315 expect(doc()?.projects[0].items[0].title).toBe("hello world!") 316 done() 317 } 318 return run + 1 319 }) 320 }) 321 322 expect(fn).toHaveBeenCalledOnce() 323 expect(fn).toHaveBeenCalledWith("two") 324 325 return Promise.all([arrayDotThree, projectZeroItemZeroTitle]) 326 }) 327 }) 328 329 interface ExampleDoc { 330 key: string 331 array: number[] 332 hellos: {hello: string}[] 333 projects: { 334 title: string 335 items: {title: string; complete?: number}[] 336 }[] 337 }