import {PeerId, Repo, type DocHandle} from "@automerge/automerge-repo" import {renderHook, testEffect} from "@solidjs/testing-library" import {describe, expect, it, vi} from "vitest" import { createEffect, createRoot, createSignal, type ParentComponent, } from "solid-js" import makeDocumentProjection from "../src/makeDocumentProjection.js" import {RepoContext} from "../src/context.js" describe("makeDocumentProjection", () => { function setup() { const repo = new Repo({ peerId: "bob" as PeerId, }) const create = () => repo.create({ key: "value", array: [1, 2, 3], hellos: [{hello: "world"}, {hello: "hedgehog"}], projects: [ {title: "one", items: [{title: "go shopping"}]}, {title: "two", items: []}, ], }) const handle = create() const wrapper: ParentComponent = props => { return ( {props.children} ) } return { repo, handle, wrapper, create, } } it("should notify on a property change", async () => { const {handle} = setup() const {result: doc, owner} = renderHook( makeDocumentProjection as (handle: DocHandle) => ExampleDoc, { initialProps: [handle], } ) const done = testEffect(done => { createEffect((run: number = 0) => { if (run == 0) { expect(doc.key).toBe("value") handle.change(doc => (doc.key = "hello world!")) } else if (run == 1) { expect(doc.key).toBe("hello world!") handle.change(doc => (doc.key = "friday night!")) } else if (run == 2) { expect(doc.key).toBe("friday night!") done() } return run + 1 }) }, owner!) return done }) it("should not apply patches multiple times just because there are multiple projections of the same handle", async () => { const {handle} = setup() const {result: one, owner: owner1} = renderHook( makeDocumentProjection as (handle: DocHandle) => ExampleDoc, { initialProps: [handle], } ) const {result: two, owner: owner2} = renderHook( makeDocumentProjection as (handle: DocHandle) => ExampleDoc, { initialProps: [handle], } ) const done2 = testEffect(done => { createEffect((run: number = 0) => { if (run == 0) { expect(two.array).toEqual([1, 2, 3]) } else if (run == 1) { expect(two.array).toEqual([1, 2, 3, 4]) } else if (run == 2) { expect(two.array).toEqual([1, 2, 3, 4, 5]) done() } return run + 1 }) }, owner2!) const done1 = testEffect(done => { createEffect((run: number = 0) => { if (run == 0) { expect(one.array).toEqual([1, 2, 3]) handle.change(doc => doc.array.push(4)) } else if (run == 1) { expect(one.array).toEqual([1, 2, 3, 4]) handle.change(doc => doc.array.push(5)) } else if (run == 2) { expect(one.array).toEqual([1, 2, 3, 4, 5]) done() } return run + 1 }) }, owner1!) return Promise.allSettled([done1, done2]) }) it("should notify on a deep property change", async () => { const {handle} = setup() return createRoot(() => { const doc = makeDocumentProjection(handle) return testEffect(done => { createEffect((run: number = 0) => { if (run == 0) { expect(doc.projects[0].title).toBe("one") handle.change(doc => (doc.projects[0].title = "hello world!")) } else if (run == 1) { expect(doc.projects[0].title).toBe("hello world!") handle.change(doc => (doc.projects[0].title = "friday night!")) } else if (run == 2) { expect(doc.projects[0].title).toBe("friday night!") done() } return run + 1 }) }) }) }) it("should not clean up when it should not clean up", async () => { const {handle} = setup() return createRoot(() => { const [one, clean1] = createRoot(c => [makeDocumentProjection(handle), c]) const [two, clean2] = createRoot(c => [makeDocumentProjection(handle), c]) const [three, clean3] = createRoot(c => [ makeDocumentProjection(handle), c, ]) const [signal, setSignal] = createSignal(0) return testEffect(done => { createEffect((run: number = 0) => { signal() expect(one.projects[0].title).not.toBeUndefined() expect(two.projects[0].title).not.toBeUndefined() expect(three.projects[0].title).not.toBeUndefined() if (run == 0) { // immediately clean up the first projection. updates should // carry on because there is still another reference clean1() expect(one.projects[0].title).toBe("one") expect(two.projects[0].title).toBe("one") expect(three.projects[0].title).toBe("one") handle.change(doc => (doc.projects[0].title = "hello world!")) } else if (run == 1) { // clean up another projection. updates should carry on // because there is still one left clean3() expect(one.projects[0].title).toBe("hello world!") expect(two.projects[0].title).toBe("hello world!") expect(three.projects[0].title).toBe("hello world!") setSignal(1) } else if (run == 2) { // now all the stores are cleaned up so further updates // should not show in the store clean2() setSignal(2) } else if (run == 3) { handle.change(doc => (doc.projects[0].title = "friday night!")) // force the test to run again setSignal(3) } else if (run == 4) { expect(one.projects[0].title).toBe("hello world!") expect(two.projects[0].title).toBe("hello world!") expect(three.projects[0].title).toBe("hello world!") done() } return run + 1 }) }) }) }) it("should not notify on properties nobody cares about", async () => { const {handle} = setup() let fn = vi.fn() const {result: doc, owner} = renderHook( makeDocumentProjection as (handle: DocHandle) => ExampleDoc, { initialProps: [handle], } ) testEffect(() => { createEffect(() => { fn(doc?.projects[1].title) }) }) const arrayDotThree = testEffect(done => { createEffect((run: number = 0) => { if (run == 0) { expect(doc.array[3]).toBeUndefined() handle.change(doc => (doc.array[2] = 22)) handle.change(doc => (doc.key = "hello world!")) handle.change(doc => (doc.array[1] = 11)) handle.change(doc => (doc.array[3] = 145)) } else if (run == 1) { expect(doc?.array[3]).toBe(145) handle.change(doc => (doc.projects[0].title = "hello world!")) handle.change( doc => (doc.projects[0].items[0].title = "hello world!") ) handle.change(doc => (doc.array[3] = 147)) } else if (run == 2) { expect(doc?.array[3]).toBe(147) done() } return run + 1 }) }, owner!) const projectZeroItemZeroTitle = testEffect(done => { createEffect((run: number = 0) => { if (run == 0) { expect(doc?.projects[0].items[0].title).toBe("hello world!") done() } return run + 1 }) }, owner!) expect(fn).toHaveBeenCalledOnce() expect(fn).toHaveBeenCalledWith("two") return Promise.all([arrayDotThree, projectZeroItemZeroTitle]) }) }) interface ExampleDoc { key: string array: number[] hellos: {hello: string}[] projects: { title: string items: {title: string; complete?: number}[] }[] }