| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| 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"]); |
| }); |
| }); |
|
|