/ test / useDocHandle.test.tsx
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  })