@automerge/automerge-repo-react-hooks 2.0.0-beta.1 → 2.0.0-beta.5

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.
@@ -0,0 +1,22 @@
1
+ import { Message, NetworkAdapter, PeerId } from '@automerge/automerge-repo/slim';
2
+
3
+ export declare const pause: (t?: number) => Promise<void>;
4
+ export declare class DummyNetworkAdapter extends NetworkAdapter {
5
+ #private;
6
+ isReady(): boolean;
7
+ whenReady(): Promise<void>;
8
+ forceReady(): void;
9
+ constructor(opts?: {
10
+ startReady: boolean;
11
+ sendMessage: (_message: Message) => void;
12
+ });
13
+ connect(peerId: PeerId): void;
14
+ disconnect(): void;
15
+ peerCandidate(peerId: PeerId): void;
16
+ send(message: Message): void;
17
+ receive(message: Message): void;
18
+ static createConnectedPair({ latency }?: {
19
+ latency?: number | undefined;
20
+ }): DummyNetworkAdapter[];
21
+ }
22
+ //# sourceMappingURL=DummyNetworkAdapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DummyNetworkAdapter.d.ts","sourceRoot":"","sources":["../../src/helpers/DummyNetworkAdapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,gCAAgC,CAAA;AAEhF,eAAO,MAAM,KAAK,GAAI,UAAK,kBACmC,CAAA;AAI9D,qBAAa,mBAAoB,SAAQ,cAAc;;IAOrD,OAAO;IAGP,SAAS;IAUT,UAAU;gBAIR,IAAI;;gCAA+C,OAAO;KAAS;IAQrE,OAAO,CAAC,MAAM,EAAE,MAAM;IAGtB,UAAU;IACV,aAAa,CAAC,MAAM,EAAE,MAAM;IAG5B,IAAI,CAAC,OAAO,EAAE,OAAO;IAGrB,OAAO,CAAC,OAAO,EAAE,OAAO;IAGxB,MAAM,CAAC,mBAAmB,CAAC,EAAE,OAAY,EAAE;;KAAK;CAajD"}
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import q, { createContext as I, useContext as j, useRef as $, useState as x, useEffect as _, useCallback as P } from "react";
1
+ import q, { createContext as I, useContext as j, useRef as $, useState as x, useEffect as _, useCallback as S } from "react";
2
2
  function O(s) {
3
3
  let l = "pending", r, f;
4
4
  const h = s.then(
@@ -32,21 +32,26 @@ function k() {
32
32
  const E = /* @__PURE__ */ new Map();
33
33
  function N(s, { suspense: l } = { suspense: !1 }) {
34
34
  const r = k(), f = $(), [h, p] = x();
35
- let u = s ? E.get(s) : void 0;
36
- if (!u && s) {
35
+ let u = h;
36
+ if (s && !u) {
37
+ const t = r.findWithProgress(s);
38
+ t.state === "ready" && (u = t.handle);
39
+ }
40
+ let n = s ? E.get(s) : void 0;
41
+ if (!n && s) {
37
42
  f.current?.abort(), f.current = new AbortController();
38
- const n = r.find(s, { signal: f.current.signal });
39
- u = O(n), E.set(s, u);
43
+ const t = r.find(s, { signal: f.current.signal });
44
+ n = O(t), E.set(s, n);
40
45
  }
41
46
  return _(() => {
42
- l || !u || u.promise.then((n) => {
43
- p(n);
47
+ l || !n || n.promise.then((t) => {
48
+ p(t);
44
49
  }).catch(() => {
45
50
  p(void 0);
46
51
  });
47
- }, [l, u]), l && u ? u.read() : h;
52
+ }, [l, n]), u || !l || !n ? u : n.read();
48
53
  }
49
- function V(s, l = { suspense: !1 }) {
54
+ function Q(s, l = { suspense: !1 }) {
50
55
  const r = N(s, l), [f, h] = x(() => r?.doc()), [p, u] = x();
51
56
  _(() => {
52
57
  h(r?.doc());
@@ -60,7 +65,7 @@ function V(s, l = { suspense: !1 }) {
60
65
  r.removeListener("change", t), r.removeListener("delete", e);
61
66
  };
62
67
  }, [r, s]);
63
- const n = P(
68
+ const n = S(
64
69
  (t, e) => {
65
70
  r.change(t, e);
66
71
  },
@@ -104,7 +109,7 @@ function T(s, { suspense: l = !1 } = {}) {
104
109
  throw Promise.all(p.map((n) => n.promise));
105
110
  return f;
106
111
  }
107
- function W(s, { suspense: l = !0 } = {}) {
112
+ function V(s, { suspense: l = !0 } = {}) {
108
113
  const r = T(s, { suspense: l }), [f, h] = x(() => /* @__PURE__ */ new Map());
109
114
  _(() => {
110
115
  const u = /* @__PURE__ */ new Map();
@@ -133,7 +138,7 @@ function W(s, { suspense: l = !0 } = {}) {
133
138
  });
134
139
  };
135
140
  }, [r]);
136
- const p = P(
141
+ const p = S(
137
142
  (u, n, t) => {
138
143
  const e = r.get(u);
139
144
  e && e.change(n, t);
@@ -142,11 +147,11 @@ function W(s, { suspense: l = !0 } = {}) {
142
147
  );
143
148
  return [f, p];
144
149
  }
145
- function A(s) {
150
+ function H(s) {
146
151
  return s && s.__esModule && Object.prototype.hasOwnProperty.call(s, "default") ? s.default : s;
147
152
  }
148
153
  var C, R;
149
- function z() {
154
+ function W() {
150
155
  if (R) return C;
151
156
  R = 1;
152
157
  var s = q, l = function(f) {
@@ -159,11 +164,11 @@ function z() {
159
164
  };
160
165
  return C = r, C;
161
166
  }
162
- var B = z();
163
- const L = /* @__PURE__ */ A(B);
164
- var D = { exports: {} }, S;
165
- function G() {
166
- return S || (S = 1, function(s) {
167
+ var z = W();
168
+ const L = /* @__PURE__ */ H(z);
169
+ var D = { exports: {} }, P;
170
+ function B() {
171
+ return P || (P = 1, function(s) {
167
172
  var l = Object.prototype.hasOwnProperty, r = "~";
168
173
  function f() {
169
174
  }
@@ -222,8 +227,8 @@ function G() {
222
227
  y[d - 1] = arguments[d];
223
228
  a.fn.apply(a.context, y);
224
229
  } else {
225
- var H = a.length, b;
226
- for (d = 0; d < H; d++)
230
+ var A = a.length, b;
231
+ for (d = 0; d < A; d++)
227
232
  switch (a[d].once && this.removeListener(e, a[d].fn, void 0, !0), g) {
228
233
  case 1:
229
234
  a[d].fn.call(a[d].context);
@@ -268,8 +273,8 @@ function G() {
268
273
  }, n.prototype.off = n.prototype.removeListener, n.prototype.addListener = n.prototype.on, n.prefixed = r, n.EventEmitter = n, s.exports = n;
269
274
  }(D)), D.exports;
270
275
  }
271
- var J = G();
272
- const K = /* @__PURE__ */ A(J), M = new K(), X = ({
276
+ var G = B();
277
+ const J = /* @__PURE__ */ H(G), M = new J(), X = ({
273
278
  handle: s,
274
279
  localUserId: l,
275
280
  offlineTimeout: r = 3e4,
@@ -336,8 +341,8 @@ export {
336
341
  F as RepoContext,
337
342
  N as useDocHandle,
338
343
  T as useDocHandles,
339
- V as useDocument,
340
- W as useDocuments,
344
+ Q as useDocument,
345
+ V as useDocuments,
341
346
  Y as useLocalAwareness,
342
347
  X as useRemoteAwareness,
343
348
  k as useRepo
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automerge/automerge-repo-react-hooks",
3
- "version": "2.0.0-beta.1",
3
+ "version": "2.0.0-beta.5",
4
4
  "description": "Hooks to access an Automerge Repo from your react app.",
5
5
  "repository": "https://github.com/automerge/automerge-repo/tree/master/packages/automerge-repo-react-hooks",
6
6
  "author": "Peter van Hardenberg <pvh@pvh.ca>",
@@ -15,10 +15,8 @@
15
15
  },
16
16
  "dependencies": {
17
17
  "@automerge/automerge": "^2.2.8",
18
- "@automerge/automerge-repo": "2.0.0-beta.1",
18
+ "@automerge/automerge-repo": "2.0.0-beta.5",
19
19
  "eventemitter3": "^5.0.1",
20
- "react": "^18.3.0",
21
- "react-dom": "^18.3.0",
22
20
  "react-usestateref": "^1.0.8"
23
21
  },
24
22
  "devDependencies": {
@@ -26,14 +24,16 @@
26
24
  "@testing-library/react": "^14.0.0",
27
25
  "eslint-plugin-react-hooks": "^5.1.0",
28
26
  "jsdom": "^22.1.0",
27
+ "react": "^18.2.0",
28
+ "react-dom": "^18.2.0",
29
29
  "react-error-boundary": "^5.0.0",
30
30
  "rollup-plugin-visualizer": "^5.9.3",
31
31
  "vite": "^6.0.7",
32
32
  "vite-plugin-dts": "^3.9.1"
33
33
  },
34
34
  "peerDependencies": {
35
- "react": ">18.3.0",
36
- "react-dom": ">18.3.0"
35
+ "react": "^18.0.0",
36
+ "react-dom": "^18.0.0"
37
37
  },
38
38
  "watch": {
39
39
  "build": {
@@ -46,5 +46,5 @@
46
46
  "publishConfig": {
47
47
  "access": "public"
48
48
  },
49
- "gitHead": "561e9142496d89cf34ad78cb72b27329990cae07"
49
+ "gitHead": "0a1f8195dbe0a84c21611c5f58003b68f35b9dc1"
50
50
  }
@@ -0,0 +1,66 @@
1
+ import { Message, NetworkAdapter, PeerId } from "@automerge/automerge-repo/slim"
2
+
3
+ export const pause = (t = 0) =>
4
+ new Promise<void>(resolve => setTimeout(() => resolve(), t))
5
+
6
+ type SendMessageFn = (message: Message) => void
7
+
8
+ export class DummyNetworkAdapter extends NetworkAdapter {
9
+ #sendMessage: SendMessageFn
10
+ #ready = false
11
+ #readyResolver: ((value: void) => void) | undefined
12
+ #readyPromise = new Promise<void>(resolve => {
13
+ this.#readyResolver = resolve
14
+ })
15
+ isReady() {
16
+ return this.#ready
17
+ }
18
+ whenReady() {
19
+ return this.#readyPromise
20
+ }
21
+ #forceReady() {
22
+ if (!this.#ready) {
23
+ this.#ready = true
24
+ this.#readyResolver?.()
25
+ }
26
+ }
27
+ // A public wrapper for use in tests!
28
+ forceReady() {
29
+ this.#forceReady()
30
+ }
31
+ constructor(
32
+ opts = { startReady: true, sendMessage: (_message: Message) => {} }
33
+ ) {
34
+ super()
35
+ if (opts.startReady) {
36
+ this.#forceReady()
37
+ }
38
+ this.#sendMessage = opts.sendMessage
39
+ }
40
+ connect(peerId: PeerId) {
41
+ this.peerId = peerId
42
+ }
43
+ disconnect() {}
44
+ peerCandidate(peerId: PeerId) {
45
+ this.emit("peer-candidate", { peerId, peerMetadata: {} })
46
+ }
47
+ send(message: Message) {
48
+ this.#sendMessage?.(message)
49
+ }
50
+ receive(message: Message) {
51
+ this.emit("message", message)
52
+ }
53
+ static createConnectedPair({ latency = 10 } = {}) {
54
+ const adapter1 = new DummyNetworkAdapter({
55
+ startReady: true,
56
+ sendMessage: (message: Message) =>
57
+ pause(latency).then(() => adapter2.receive(message)),
58
+ })
59
+ const adapter2 = new DummyNetworkAdapter({
60
+ startReady: true,
61
+ sendMessage: message =>
62
+ pause(latency).then(() => adapter1.receive(message)),
63
+ })
64
+ return [adapter1, adapter2]
65
+ }
66
+ }
@@ -39,6 +39,15 @@ export function useDocHandle<T>(
39
39
  const controllerRef = useRef<AbortController>()
40
40
  const [handle, setHandle] = useState<DocHandle<T> | undefined>()
41
41
 
42
+ let currentHandle: DocHandle<T> | undefined = handle
43
+ if (id && !currentHandle) {
44
+ // if we haven't saved a handle yet, check if one is immediately available
45
+ const progress = repo.findWithProgress<T>(id)
46
+ if (progress.state === "ready") {
47
+ currentHandle = progress.handle
48
+ }
49
+ }
50
+
42
51
  let wrapper = id ? wrapperCache.get(id) : undefined
43
52
  if (!wrapper && id) {
44
53
  controllerRef.current?.abort()
@@ -68,9 +77,9 @@ export function useDocHandle<T>(
68
77
  })
69
78
  }, [suspense, wrapper])
70
79
 
71
- if (suspense && wrapper) {
72
- return wrapper.read() as DocHandle<T>
73
- } else {
74
- return handle
80
+ if (currentHandle || !suspense || !wrapper) {
81
+ return currentHandle
75
82
  }
83
+
84
+ return wrapper.read() as DocHandle<T>
76
85
  }
@@ -13,6 +13,7 @@ import { describe, expect, it, vi } from "vitest"
13
13
  import { useDocHandle } from "../src/useDocHandle"
14
14
  import { RepoContext } from "../src/useRepo"
15
15
  import { ErrorBoundary } from "react-error-boundary"
16
+ import { DummyNetworkAdapter } from "../src/helpers/DummyNetworkAdapter"
16
17
 
17
18
  interface ExampleDoc {
18
19
  foo: string
@@ -44,6 +45,39 @@ describe("useDocHandle", () => {
44
45
  }
45
46
  }
46
47
 
48
+ function setupPairedRepos(latency = 10) {
49
+ // Create two connected repos with network delay
50
+ const [adapterCreator, adapterFinder] =
51
+ DummyNetworkAdapter.createConnectedPair({
52
+ latency,
53
+ })
54
+
55
+ // TODO: fix types here
56
+ const repoCreator = new Repo({
57
+ peerId: "peer-creator" as PeerId,
58
+ network: [adapterCreator],
59
+ })
60
+ const repoFinder = new Repo({
61
+ peerId: "peer-finder" as PeerId,
62
+ network: [adapterFinder],
63
+ })
64
+
65
+ // TODO: dummynetwork adapter should probably take care of this
66
+ // Initialize the network.
67
+ adapterCreator.peerCandidate(`peer-finder` as PeerId)
68
+ adapterFinder.peerCandidate(`peer-creator` as PeerId)
69
+
70
+ const wrapper = ({ children }) => {
71
+ return (
72
+ <RepoContext.Provider value={repoFinder}>
73
+ {children}
74
+ </RepoContext.Provider>
75
+ )
76
+ }
77
+
78
+ return { repoCreator, repoFinder, wrapper }
79
+ }
80
+
47
81
  const Component = ({
48
82
  url,
49
83
  onHandle,
@@ -133,16 +167,10 @@ describe("useDocHandle", () => {
133
167
  })
134
168
 
135
169
  it("handles slow network correctly", async () => {
136
- const { handleA, repo, wrapper } = await setup()
170
+ const { repoCreator, wrapper } = await setupPairedRepos()
171
+ const handleA = repoCreator.create({ foo: "A" })
137
172
  const onHandle = vi.fn()
138
173
 
139
- // Mock find to simulate slow network
140
- const originalFind = repo.find.bind(repo)
141
- repo.find = vi.fn().mockImplementation(async (...args) => {
142
- await new Promise(resolve => setTimeout(resolve, 100))
143
- return originalFind(...args)
144
- })
145
-
146
174
  render(
147
175
  <ErrorBoundary fallback={<div data-testid="error">Error</div>}>
148
176
  <Suspense fallback={<div data-testid="loading">Loading...</div>}>
@@ -163,25 +191,18 @@ describe("useDocHandle", () => {
163
191
  })
164
192
 
165
193
  // Verify callback was called with correct handle
166
- expect(onHandle).toHaveBeenCalledWith(handleA)
194
+ expect(onHandle).toHaveBeenCalledWith(
195
+ expect.objectContaining({ url: handleA.url })
196
+ )
167
197
 
168
198
  // Verify error boundary never rendered
169
199
  expect(screen.queryByTestId("error")).not.toBeInTheDocument()
170
200
  })
171
201
 
172
202
  it("suspends while loading a handle", async () => {
173
- const { handleA, wrapper } = await setup()
203
+ const { repoCreator, wrapper } = await setupPairedRepos()
204
+ const handleA = repoCreator.create({ foo: "A" })
174
205
  const onHandle = vi.fn()
175
- let promiseResolve: (value: DocHandle<ExampleDoc>) => void
176
-
177
- // Mock find to return a delayed promise
178
- const originalFind = repo.find.bind(repo)
179
- repo.find = vi.fn().mockImplementation(
180
- () =>
181
- new Promise(resolve => {
182
- promiseResolve = resolve
183
- })
184
- )
185
206
 
186
207
  render(
187
208
  <Suspense fallback={<div data-testid="loading">Loading...</div>}>
@@ -194,31 +215,19 @@ describe("useDocHandle", () => {
194
215
  expect(screen.getByTestId("loading")).toBeInTheDocument()
195
216
  expect(onHandle).not.toHaveBeenCalled()
196
217
 
197
- // Resolve the find
198
- promiseResolve!(await originalFind(handleA.url))
199
-
200
218
  // Should show content
201
219
  await waitFor(() => {
202
- expect(onHandle).toHaveBeenCalledWith(handleA)
203
- // return repo.find to its natural state
204
- repo.find = originalFind
220
+ expect(onHandle).toHaveBeenCalledWith(
221
+ expect.objectContaining({ url: handleA.url })
222
+ )
205
223
  })
206
224
  })
207
225
 
208
226
  it("handles rapid url changes during loading", async () => {
209
- const { handleA, handleB, wrapper } = await setup()
227
+ const { repoCreator, repoFinder, wrapper } = await setupPairedRepos()
228
+ const handleA = repoCreator.create({ foo: "A" })
229
+ const handleB = repoFinder.create({ foo: "B" })
210
230
  const onHandle = vi.fn()
211
- const delays: Record<string, number> = {
212
- [handleA.url]: 100,
213
- [handleB.url]: 50,
214
- }
215
-
216
- // Mock find to simulate different network delays
217
- const originalFind = repo.find.bind(repo)
218
- repo.find = vi.fn().mockImplementation(async (url: string) => {
219
- await new Promise(resolve => setTimeout(resolve, delays[url]))
220
- return originalFind(url)
221
- })
222
231
 
223
232
  const { rerender } = render(
224
233
  <Suspense fallback={<div data-testid="loading">Loading...</div>}>
@@ -237,21 +246,18 @@ describe("useDocHandle", () => {
237
246
  // Should eventually resolve with B, not A
238
247
  await waitFor(() => {
239
248
  expect(onHandle).toHaveBeenLastCalledWith(handleB)
240
- expect(onHandle).not.toHaveBeenCalledWith(handleA)
249
+ expect(onHandle).not.toHaveBeenCalledWith(
250
+ expect.objectContaining({ url: handleA.url })
251
+ )
241
252
  })
242
253
  })
243
254
 
244
255
  describe("useDocHandle with suspense: false", () => {
245
256
  it("returns undefined while loading then resolves to handle", async () => {
246
- const { handleA, repo, wrapper } = await setup()
247
- const onHandle = vi.fn()
257
+ const { repoCreator, wrapper } = await setupPairedRepos()
258
+ const handleA = repoCreator.create({ foo: "A" })
248
259
 
249
- // Mock find to simulate network delay
250
- const originalFind = repo.find.bind(repo)
251
- repo.find = vi.fn().mockImplementation(async (...args) => {
252
- await new Promise(resolve => setTimeout(resolve, 100))
253
- return originalFind(...args)
254
- })
260
+ const onHandle = vi.fn()
255
261
 
256
262
  const NonSuspenseComponent = ({
257
263
  url,
@@ -274,11 +280,10 @@ describe("useDocHandle", () => {
274
280
 
275
281
  // Wait for handle to load
276
282
  await waitFor(() => {
277
- expect(onHandle).toHaveBeenLastCalledWith(handleA)
283
+ expect(onHandle).toHaveBeenCalledWith(
284
+ expect.objectContaining({ url: handleA.url })
285
+ )
278
286
  })
279
-
280
- // Restore original find implementation
281
- repo.find = originalFind
282
287
  })
283
288
 
284
289
  it("handles unavailable documents by returning undefined", async () => {
@@ -338,9 +343,6 @@ describe("useDocHandle", () => {
338
343
  // Change URL
339
344
  rerender(<NonSuspenseComponent url={handleB.url} onHandle={onHandle} />)
340
345
 
341
- // Should temporarily return to undefined
342
- expect(onHandle).toHaveBeenCalledWith(undefined)
343
-
344
346
  // Then resolve to new handle
345
347
  await waitFor(() => expect(onHandle).toHaveBeenLastCalledWith(handleB))
346
348
  })
@@ -4,6 +4,8 @@ import {
4
4
  generateAutomergeUrl,
5
5
  PeerId,
6
6
  Repo,
7
+ NetworkAdapter,
8
+ Message,
7
9
  } from "@automerge/automerge-repo"
8
10
  import { render, screen, waitFor } from "@testing-library/react"
9
11
  import React, { Suspense } from "react"
@@ -13,7 +15,7 @@ import "@testing-library/jest-dom"
13
15
  import { useDocument } from "../src/useDocument"
14
16
  import { RepoContext } from "../src/useRepo"
15
17
  import { ErrorBoundary } from "react-error-boundary"
16
-
18
+ import { DummyNetworkAdapter, pause } from "../src/helpers/DummyNetworkAdapter"
17
19
  interface ExampleDoc {
18
20
  foo: string
19
21
  }
@@ -48,6 +50,37 @@ describe("useDocument", () => {
48
50
  }
49
51
  }
50
52
 
53
+ function setupPairedRepos(latency = 10) {
54
+ // Create two connected repos with network delay
55
+ const [adapterCreator, adapterFinder] =
56
+ DummyNetworkAdapter.createConnectedPair({
57
+ latency,
58
+ })
59
+
60
+ const repoCreator = new Repo({
61
+ peerId: "peer-creator" as PeerId,
62
+ network: [adapterCreator],
63
+ })
64
+ const repoFinder = new Repo({
65
+ peerId: "peer-finder" as PeerId,
66
+ network: [adapterFinder],
67
+ })
68
+
69
+ // TODO: dummynetwork adapter should probably take care of this
70
+ // Initialize the network.
71
+ adapterCreator.peerCandidate(`peer-finder` as PeerId)
72
+ adapterFinder.peerCandidate(`peer-creator` as PeerId)
73
+
74
+ const wrapper = ({ children }) => {
75
+ return (
76
+ <RepoContext.Provider value={repoFinder}>
77
+ {children}
78
+ </RepoContext.Provider>
79
+ )
80
+ }
81
+
82
+ return { repoCreator, repoFinder, wrapper }
83
+ }
51
84
  const Component = ({
52
85
  url,
53
86
  onDoc,
@@ -71,13 +104,9 @@ describe("useDocument", () => {
71
104
  { wrapper }
72
105
  )
73
106
 
74
- // First we should see the loading state
75
- expect(screen.getByTestId("loading")).toBeInTheDocument()
76
-
77
- // Wait for content to appear and check it's correct
78
- await waitFor(() => {
79
- expect(screen.getByTestId("content")).toHaveTextContent("A")
80
- })
107
+ // Because this document is already loaded locally (we made it)
108
+ // we should see results immediately.
109
+ expect(screen.getByTestId("content")).toHaveTextContent("A")
81
110
 
82
111
  // Now check our spy got called with the document
83
112
  expect(onDoc).toHaveBeenCalledWith({ foo: "A" })
@@ -95,9 +124,7 @@ describe("useDocument", () => {
95
124
  )
96
125
 
97
126
  // Wait for initial render
98
- await waitFor(() => {
99
- expect(screen.getByTestId("content")).toHaveTextContent("A")
100
- })
127
+ expect(screen.getByTestId("content")).toHaveTextContent("A")
101
128
  expect(onDoc).toHaveBeenCalledWith({ foo: "A" })
102
129
 
103
130
  // Change the document
@@ -156,9 +183,7 @@ describe("useDocument", () => {
156
183
  )
157
184
 
158
185
  // Wait for first document
159
- await waitFor(() => {
160
- expect(screen.getByTestId("content")).toHaveTextContent("A")
161
- })
186
+ expect(screen.getByTestId("content")).toHaveTextContent("A")
162
187
  expect(onDoc).toHaveBeenCalledWith({ foo: "A" })
163
188
 
164
189
  // Switch to second document
@@ -169,9 +194,7 @@ describe("useDocument", () => {
169
194
  )
170
195
 
171
196
  // Should show loading then new content
172
- await waitFor(() => {
173
- expect(screen.getByTestId("content")).toHaveTextContent("B")
174
- })
197
+ expect(screen.getByTestId("content")).toHaveTextContent("B")
175
198
  expect(onDoc).toHaveBeenCalledWith({ foo: "B" })
176
199
  })
177
200
 
@@ -202,28 +225,23 @@ describe("useDocument", () => {
202
225
 
203
226
  // Test slow-loading document
204
227
  it("should handle slow-loading documents", async () => {
205
- const { wrapper, repo } = setup()
206
- const onDoc = vi.fn()
228
+ const { repoCreator, wrapper } = setupPairedRepos()
207
229
 
208
- // Create handle but delay its availability
209
- const slowHandle = repo.create({ foo: "slow" })
210
- const originalFind = repo.find.bind(repo)
211
- repo.find = vi.fn().mockImplementation(async (...args) => {
212
- await new Promise(resolve => setTimeout(resolve, 100))
213
- return originalFind(...args)
214
- })
230
+ // Create document in first repo
231
+ const handle = repoCreator.create({ foo: "slow" })
232
+ const onDoc = vi.fn()
215
233
 
216
234
  render(
217
235
  <Suspense fallback={<div data-testid="loading">Loading...</div>}>
218
- <Component url={slowHandle.url} onDoc={onDoc} />
236
+ <Component url={handle.url} onDoc={onDoc} />
219
237
  </Suspense>,
220
238
  { wrapper }
221
239
  )
222
240
 
223
- // Should show loading state
241
+ // Should show loading state initially
224
242
  expect(screen.getByTestId("loading")).toBeInTheDocument()
225
243
 
226
- // Eventually shows content
244
+ // Eventually shows content after network delay
227
245
  await waitFor(() => {
228
246
  expect(screen.getByTestId("content")).toHaveTextContent("slow")
229
247
  })
@@ -263,17 +281,10 @@ describe("useDocument", () => {
263
281
 
264
282
  // Test document changes during loading
265
283
  it("should handle document changes while loading", async () => {
266
- const { wrapper, repo } = setup()
284
+ const { wrapper, repoCreator } = setupPairedRepos()
267
285
  const onDoc = vi.fn()
268
286
 
269
- const handle = repo.create({ foo: "initial" })
270
- let resolveFind: (value: any) => void
271
- const originalFind = repo.find.bind(repo)
272
- repo.find = vi.fn().mockImplementation(async (...args) => {
273
- return new Promise(resolve => {
274
- resolveFind = resolve
275
- })
276
- })
287
+ const handle = repoCreator.create({ foo: "initial" })
277
288
 
278
289
  render(
279
290
  <Suspense fallback={<div data-testid="loading">Loading...</div>}>
@@ -285,9 +296,6 @@ describe("useDocument", () => {
285
296
  // Modify document while it's still loading
286
297
  handle.change(doc => (doc.foo = "changed"))
287
298
 
288
- // Resolve the find
289
- resolveFind!(await originalFind(handle.url))
290
-
291
299
  // Should show final state
292
300
  await waitFor(() => {
293
301
  expect(screen.getByTestId("content")).toHaveTextContent("changed")