useDocHandle.test.tsx
1 import { 2 type AutomergeUrl, 3 type DocHandle, 4 type PeerId, 5 Repo, 6 } from "@automerge/automerge-repo" 7 import {render, renderHook, waitFor} from "@solidjs/testing-library" 8 import {afterEach, describe, expect, it, vi} from "vitest" 9 import useDocHandle from "../src/useDocHandle.js" 10 import {RepoContext} from "../src/context.js" 11 import { 12 createEffect, 13 createSignal, 14 on, 15 Suspense, 16 untrack, 17 type ParentComponent, 18 } from "solid-js" 19 import type {UseDocHandleOptions} from "../src/types.js" 20 21 interface ExampleDoc { 22 foo: string 23 } 24 25 function getRepoWrapper(repo: Repo): ParentComponent { 26 return props => ( 27 <RepoContext.Provider value={repo}>{props.children}</RepoContext.Provider> 28 ) 29 } 30 31 describe("useDocHandle", () => { 32 afterEach(() => { 33 document.body.innerHTML = "" 34 }) 35 const repo = new Repo({ 36 peerId: "bob" as PeerId, 37 }) 38 39 function setup() { 40 const handleA = repo.create<ExampleDoc>() 41 handleA.change(doc => (doc.foo = "A")) 42 43 const handleB = repo.create<ExampleDoc>() 44 handleB.change(doc => (doc.foo = "B")) 45 46 return { 47 repo, 48 handleA, 49 handleB, 50 wrapper: getRepoWrapper(repo), 51 } 52 } 53 54 const Component = (props: { 55 url: AutomergeUrl | undefined 56 onHandle: (handle: DocHandle<unknown> | undefined) => void 57 options?: UseDocHandleOptions 58 }) => { 59 const handle = useDocHandle( 60 () => props.url, 61 untrack(() => props.options) 62 ) 63 createEffect( 64 on([handle], () => { 65 props.onHandle(handle()) 66 }) 67 ) 68 69 return ( 70 <Suspense fallback={<div>fallback</div>}> 71 <button>{handle.latest?.url ?? "🕯️🕯️🕯️🕯️"}</button> 72 </Suspense> 73 ) 74 } 75 76 it("loads a handle", async () => { 77 const {handleA, wrapper} = setup() 78 const onHandle = vi.fn() 79 80 render(() => <Component url={handleA.url} onHandle={onHandle} />, { 81 wrapper, 82 }) 83 await waitFor(() => expect(onHandle).toHaveBeenLastCalledWith(handleA)) 84 }) 85 86 it("throws if called without any kinda repo", async () => { 87 const {handleA} = setup() 88 const onHandle = vi.fn() 89 90 expect(() => 91 render(() => <Component url={handleA.url} onHandle={onHandle} />, {}) 92 ).toThrowErrorMatchingInlineSnapshot( 93 `[Error: use outside <RepoContext> requires options.repo]` 94 ) 95 }) 96 97 it("works without a context if given a repo in options", async () => { 98 const {handleA} = setup() 99 const onHandle = vi.fn() 100 101 render( 102 () => ( 103 <Component url={handleA.url} onHandle={onHandle} options={{repo}} /> 104 ), 105 {} 106 ) 107 await waitFor(() => expect(onHandle).toHaveBeenLastCalledWith(handleA)) 108 }) 109 110 it("returns undefined when no url given", async () => { 111 const {wrapper} = setup() 112 const onHandle = vi.fn() 113 114 render(() => <Component url={undefined} onHandle={onHandle} />, {wrapper}) 115 await waitFor(() => expect(onHandle).toHaveBeenLastCalledWith(undefined)) 116 }) 117 118 it("updates the handle when the url changes", async () => { 119 const {handleA, handleB, wrapper} = setup() 120 const onHandle = vi.fn() 121 const [url, updateURL] = createSignal<AutomergeUrl | undefined>(undefined) 122 123 let hookResult = renderHook(useDocHandle, { 124 initialProps: [url], 125 wrapper, 126 }) 127 128 let componentResult = render( 129 () => <Component url={url()} onHandle={onHandle} />, 130 {wrapper} 131 ) 132 let button = componentResult.getByRole("button") 133 134 // set url to doc A 135 updateURL(handleA.url) 136 await waitFor(() => expect(hookResult.result.latest).toBe(handleA)) 137 await waitFor(() => expect(onHandle).toHaveBeenLastCalledWith(handleA)) 138 await waitFor(() => expect(button).toHaveTextContent(handleA.url)) 139 140 // set url to doc B 141 updateURL(handleB.url) 142 await waitFor(() => expect(hookResult.result.latest?.url).toBe(handleB.url)) 143 await waitFor(() => expect(button).toHaveTextContent(handleB.url)) 144 145 // set url to undefined 146 updateURL(undefined) 147 await waitFor(() => expect(hookResult.result.latest?.url).toBe(undefined)) 148 await waitFor(() => expect(button).toHaveTextContent("🕯️🕯️🕯️🕯️")) 149 }) 150 151 it("does not return undefined after the url is updated", async () => { 152 const {wrapper, handleA, handleB} = setup() 153 const onHandle = vi.fn() 154 const [url, updateURL] = createSignal<AutomergeUrl | undefined>(handleA.url) 155 156 render(() => <Component url={url()} onHandle={onHandle} />, {wrapper}) 157 await waitFor(() => expect(onHandle).toHaveBeenLastCalledWith(handleA)) 158 159 const onHandle2 = vi.fn() 160 161 // set url to doc B 162 updateURL(handleB.url) 163 await waitFor(() => expect(onHandle2).not.toHaveBeenCalledWith(undefined)) 164 }) 165 166 it("does not return a handle for a different url after the url is updated", async () => { 167 const {wrapper, handleA, handleB} = setup() 168 const onHandle = vi.fn() 169 const [url, updateURL] = createSignal<AutomergeUrl | undefined>(handleA.url) 170 171 render(() => <Component url={url()} onHandle={onHandle} />, {wrapper}) 172 await waitFor(() => expect(onHandle).toHaveBeenLastCalledWith(handleA)) 173 174 const onHandle2 = vi.fn() 175 176 // set url to doc B 177 updateURL(handleB.url) 178 await waitFor(() => expect(onHandle2).not.toHaveBeenCalledWith(handleA)) 179 }) 180 })