/** * Frontmatter Store Tests * * Exercises the Yjs-backed frontmatter store: * - scalar get/set with defaults * - authors / affiliations / links arrays (order preservation, concurrent-safe) * - setAll / getAll round-trip * - observe callback * * Uses an in-memory Y.Doc, no network. */ import { describe, it, expect, beforeEach } from "vitest"; import * as Y from "yjs"; import { createFrontmatterStore, type FrontmatterStore } from "../src/editor/frontmatter/frontmatter-store"; function makeStore(): { ydoc: Y.Doc; store: FrontmatterStore } { const ydoc = new Y.Doc(); const store = createFrontmatterStore(ydoc); return { ydoc, store }; } describe("frontmatter-store — scalars", () => { let store: FrontmatterStore; beforeEach(() => { store = makeStore().store; }); it("returns default values when unset", () => { expect(store.get("title")).toBe(""); expect(store.get("template")).toBe("article"); expect(store.get("showPdf")).toBe(true); expect(store.get("tableOfContentsAutoCollapse")).toBe(false); }); it("persists scalar set/get", () => { store.set("title", "Hello world"); store.set("doi", "10.1000/xyz"); store.set("showPdf", false); expect(store.get("title")).toBe("Hello world"); expect(store.get("doi")).toBe("10.1000/xyz"); expect(store.get("showPdf")).toBe(false); }); it("changing template between 'article' and 'paper' round-trips", () => { store.set("template", "paper"); expect(store.get("template")).toBe("paper"); store.set("template", "article"); expect(store.get("template")).toBe("article"); }); }); describe("frontmatter-store — arrays", () => { let store: FrontmatterStore; beforeEach(() => { store = makeStore().store; }); it("adds authors in insertion order", () => { store.addAuthor({ name: "Alice", affiliations: [1] }); store.addAuthor({ name: "Bob", affiliations: [1, 2] }); const authors = store.get("authors"); expect(authors.map((a) => a.name)).toEqual(["Alice", "Bob"]); }); it("updateAuthor mutates the right index only", () => { store.addAuthor({ name: "Alice", affiliations: [1] }); store.addAuthor({ name: "Bob", affiliations: [1] }); store.updateAuthor(1, { name: "Bobby", affiliations: [2] }); const authors = store.get("authors"); expect(authors[0].name).toBe("Alice"); expect(authors[1].name).toBe("Bobby"); expect(authors[1].affiliations).toEqual([2]); }); it("removeAuthor removes at index and is a no-op for out-of-range", () => { store.addAuthor({ name: "Alice", affiliations: [] }); store.addAuthor({ name: "Bob", affiliations: [] }); store.removeAuthor(99); expect(store.get("authors")).toHaveLength(2); store.removeAuthor(0); expect(store.get("authors").map((a) => a.name)).toEqual(["Bob"]); }); it("addAffiliation returns a 1-based index", () => { const i1 = store.addAffiliation({ name: "HF" }); const i2 = store.addAffiliation({ name: "MIT" }); expect(i1).toBe(1); expect(i2).toBe(2); }); }); describe("frontmatter-store — setAll / getAll", () => { it("round-trips a full snapshot", () => { const { store } = makeStore(); store.setAll({ title: "Paper", subtitle: "Sub", authors: [{ name: "Alice", affiliations: [1] }], affiliations: [{ name: "HF" }], links: [{ label: "repo", url: "https://example.com" }], doi: "10.1/2", template: "paper", }); const snap = store.getAll(); expect(snap.title).toBe("Paper"); expect(snap.subtitle).toBe("Sub"); expect(snap.authors).toEqual([{ name: "Alice", affiliations: [1] }]); expect(snap.affiliations).toEqual([{ name: "HF" }]); expect(snap.links).toEqual([{ label: "repo", url: "https://example.com" }]); expect(snap.doi).toBe("10.1/2"); expect(snap.template).toBe("paper"); }); it("setAll skips undefined values", () => { const { store } = makeStore(); store.set("title", "original"); store.setAll({ title: undefined, doi: "new-doi" }); expect(store.get("title")).toBe("original"); expect(store.get("doi")).toBe("new-doi"); }); }); describe("frontmatter-store — observe", () => { it("calls back when scalars change", () => { const { store } = makeStore(); let count = 0; const unsub = store.observe(() => { count++; }); store.set("title", "a"); store.set("title", "b"); expect(count).toBeGreaterThanOrEqual(2); unsub(); store.set("title", "c"); const after = count; expect(after).toBe(count); }); it("calls back when arrays change", () => { const { store } = makeStore(); let count = 0; store.observe(() => { count++; }); store.addAuthor({ name: "A", affiliations: [] }); store.addAffiliation({ name: "HF" }); expect(count).toBeGreaterThanOrEqual(2); }); }); describe("frontmatter-store — collaboration (two docs)", () => { it("concurrent author inserts both survive after sync", () => { const docA = new Y.Doc(); const docB = new Y.Doc(); const storeA = createFrontmatterStore(docA); const storeB = createFrontmatterStore(docB); storeA.addAuthor({ name: "Alice", affiliations: [] }); storeB.addAuthor({ name: "Bob", affiliations: [] }); const updA = Y.encodeStateAsUpdate(docA); const updB = Y.encodeStateAsUpdate(docB); Y.applyUpdate(docA, updB); Y.applyUpdate(docB, updA); const finalA = storeA.get("authors").map((a) => a.name).sort(); const finalB = storeB.get("authors").map((a) => a.name).sort(); expect(finalA).toEqual(["Alice", "Bob"]); expect(finalB).toEqual(["Alice", "Bob"]); }); });