// Archiv-Ansicht: Baum mit Notizen (Ordner + Dokumente, Verschmelzung // gleichnamiger Paare), Notiz-Ansicht rechts (Titel, Inhalt, Kindliste), // Aktionen (bearbeiten, umbenennen, löschen, anlegen), Suchtreffer-Vormerkung, // Leerzustände. import { describe, expect, it, vi } from "vitest"; import { initWikiView } from "./wiki-view"; function setup(pending: string | null = null) { document.body.innerHTML = `
`; const readDoc = vi.fn(() => Promise.resolve("# Inhalt\n\nText mit [[neu]].")); const writeDoc = vi.fn(() => Promise.resolve()); const setTitle = vi.fn(); const openWiki = vi.fn(); const actions = { rename: vi.fn(), remove: vi.fn(), moveFolder: vi.fn(), createFolder: vi.fn(), createDoc: vi.fn(), }; let pendingSelect = pending; const view = initWikiView(document.getElementById("w")!, { autoStart: true, readDoc, writeDoc, setTitle, openWiki, takePending: () => { const p = pendingSelect; pendingSelect = null; return p; }, actions, }); return { view, readDoc, writeDoc, setTitle, openWiki, actions }; } const doc = (relpath: string, name: string, extra: object = {}) => ({ relpath, name, title: name.toUpperCase(), description: null, tags: [], date: "2026-07-19", backlinks: 0, modified: "2026-07-24", ...extra, }); const page = JSON.stringify({ kind: "page", home: "/tmp/archiv", tag: null, total: 3, tags: [], folders: [ { name: "", docs: [ doc("2026-07-19_1000-wurzel-doc.md", "wurzel-doc", { backlinks: 2, description: "Beschreibung", }), ], }, { name: "konzepte", docs: [doc("konzepte/2026-07-19_1005-neu.md", "neu")] }, { name: "konzepte/panel", docs: [doc("konzepte/panel/2026-07-19_1010-alt.md", "alt")], }, ], }); /// Ordner `konzepte` mit gleichnamigem Dokument daneben — eine Notiz mit /// Inhalt und Kindern. const mergedPage = JSON.stringify({ kind: "page", home: "/tmp/archiv", tag: null, total: 2, tags: [], folders: [ { name: "", docs: [doc("konzepte.md", "konzepte", { title: "Konzepte" })] }, { name: "konzepte", docs: [doc("konzepte/2026-07-19_1005-neu.md", "neu")] }, ], }); const flush = () => new Promise((r) => setTimeout(r)); describe("initWikiView — Baum", () => { it("Baum zeigt Ordner und Dokumente, Wurzel ist gewählt", () => { const { view } = setup(); view.set(page); expect(document.querySelector(".wiki-tree-root")!.className).toContain("active"); const rows = [...document.querySelectorAll(".wiki-tree .wiki-tree-name")].map( (e) => e.textContent, ); // Unter jedem Knoten erst Dokumente, dann Ordner (mit Titel), sortiert. expect(rows).toEqual(["WURZEL-DOC", "konzepte", "NEU", "panel", "ALT"]); // Wurzel-Ansicht rechts: Titel + Kindzähler. expect(document.querySelector(".wiki-note-title")!.textContent).toBe("Archiv"); expect(document.querySelector(".wiki-children-caption")!.textContent).toBe( "2 Dokumente", ); }); it("gleichnamiges Dokument verschmilzt mit dem Ordner zur Notiz", async () => { const { view, readDoc } = setup(); view.set(mergedPage); const rows = [...document.querySelectorAll(".wiki-tree .wiki-tree-name")].map( (e) => e.textContent, ); // Kein eigenes Blatt für konzepte.md — der Ordnerknoten trägt den Titel. expect(rows).toEqual(["Konzepte", "NEU"]); // Auswahl des Knotens zeigt Inhalt UND Kindliste. document.querySelector(".wiki-tree summary .wiki-tree-name")!.click(); await flush(); expect(readDoc).toHaveBeenCalledWith("konzepte.md"); expect(document.querySelector(".wiki-note-body h1")!.textContent).toBe("Inhalt"); expect(document.querySelector(".wiki-note-children .wiki-doc-title")!.textContent).toBe( "NEU", ); }); }); describe("initWikiView — Notiz-Ansicht", () => { it("Dokument-Klick lädt den Body in die Notiz-Ansicht", async () => { const { view, readDoc } = setup(); view.set(page); document.querySelector(".wiki-tree-doc")!.click(); // WURZEL-DOC await flush(); expect(readDoc).toHaveBeenCalledWith("2026-07-19_1000-wurzel-doc.md"); expect(document.querySelector(".wiki-note-title")!.textContent).toBe("WURZEL-DOC"); // Meta als Popup am Info-Knopf. const pop = document.querySelector(".wiki-note-info-pop")!; expect(pop.hidden).toBe(true); document.querySelector('.wiki-note-actions [title^="Details"]')!.click(); expect(pop.hidden).toBe(false); expect(pop.textContent).toContain("↩ 2"); expect(document.querySelector(".wiki-note-body h1")!.textContent).toBe("Inhalt"); }); it("Wikilink im Inhalt wählt die Ziel-Notiz lokal aus", async () => { const { view, openWiki } = setup(); view.set(page); document.querySelector(".wiki-tree-doc")!.click(); // WURZEL-DOC await flush(); document.querySelector(".wiki-note-body a.wiki")!.click(); expect(openWiki).not.toHaveBeenCalled(); expect(document.querySelector(".wiki-note-title")!.textContent).toBe("NEU"); }); it("Stift schaltet in den Editor, Speichern schreibt zurück", async () => { const { view, writeDoc } = setup(); view.set(page); document.querySelector(".wiki-tree-doc")!.click(); await flush(); document.querySelector(".wiki-note-actions .panel-btn")!.click(); await flush(); const area = document.querySelector(".wiki-note-editor")!; expect(area.value).toContain("# Inhalt"); // Vorschau rechts zeigt den gerenderten Rohtext. expect(document.querySelector(".wiki-edit-preview h1")!.textContent).toBe("Inhalt"); area.value = "# Neu\n\nGeändert."; area.dispatchEvent(new Event("input")); expect(document.querySelector(".wiki-edit-preview h1")!.textContent).toBe("Neu"); document.querySelector(".wiki-form-submit")!.click(); await flush(); expect(writeDoc).toHaveBeenCalledWith( "2026-07-19_1000-wurzel-doc.md", "# Neu\n\nGeändert.", ); // Nach dem Speichern wieder die Anzeige. expect(document.querySelector(".wiki-note-editor")).toBeNull(); expect(document.querySelector(".wiki-note-body")).not.toBeNull(); }); it("Abbrechen verwirft und zeigt wieder an", async () => { const { view, writeDoc } = setup(); view.set(page); document.querySelector(".wiki-tree-doc")!.click(); await flush(); document.querySelector(".wiki-note-actions .panel-btn")!.click(); await flush(); document.querySelector(".wiki-form-cancel")!.click(); expect(writeDoc).not.toHaveBeenCalled(); expect(document.querySelector(".wiki-note-body")).not.toBeNull(); }); it("Auswahl übersteht ein Puffer-Update, Verschwundenes fällt zur Wurzel", () => { const { view } = setup(); view.set(page); document.querySelector(".wiki-tree summary .wiki-tree-name")!.click(); view.set(page); // Update: Auswahl bleibt expect(document.querySelector(".wiki-note-title")!.textContent).toBe("konzepte"); view.set( JSON.stringify({ kind: "page", home: "/tmp/archiv", tag: null, total: 1, tags: [], folders: [{ name: "", docs: [doc("2026-07-19_1000-wurzel-doc.md", "wurzel-doc")] }], }), ); expect(document.querySelector(".wiki-note-title")!.textContent).toBe("Archiv"); }); it("vorgemerkter Suchtreffer wählt die Notiz beim set() aus", async () => { const { view, readDoc } = setup("konzepte/2026-07-19_1005-neu.md"); view.set(page); await flush(); expect(document.querySelector(".wiki-note-title")!.textContent).toBe("NEU"); expect(readDoc).toHaveBeenCalledWith("konzepte/2026-07-19_1005-neu.md"); }); }); describe("initWikiView — Zurück", () => { it("Zurück-Knopf führt zur vorherigen Auswahl", async () => { const { view } = setup(); view.set(page); document.querySelector(".wiki-tree-doc")!.click(); // WURZEL-DOC await flush(); document.querySelector(".wiki-tree summary .wiki-tree-name")!.click(); // konzepte expect(document.querySelector(".wiki-note-title")!.textContent).toBe("konzepte"); document.querySelector(".wiki-note-back")!.click(); await flush(); expect(document.querySelector(".wiki-note-title")!.textContent).toBe("WURZEL-DOC"); document.querySelector(".wiki-note-back")!.click(); expect(document.querySelector(".wiki-note-title")!.textContent).toBe("Archiv"); // Verlauf leer: Knopf deaktiviert. expect( document.querySelector(".wiki-note-back")!.disabled, ).toBe(true); }); }); describe("initWikiView — Aktionen", () => { it("Klick auf den Titel bearbeitet ihn, Enter setzt den Frontmatter-Titel", async () => { const { view, setTitle } = setup(); view.set(page); document.querySelector(".wiki-tree-doc")!.click(); await flush(); document.querySelector(".wiki-note-title.editable")!.click(); const input = document.querySelector(".wiki-note-title-input")!; expect(input.value).toBe("WURZEL-DOC"); input.value = "Neuer Titel"; input.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true })); expect(setTitle).toHaveBeenCalledWith("2026-07-19_1000-wurzel-doc.md", "Neuer Titel"); }); it("Escape verwirft die Titel-Bearbeitung", async () => { const { view, setTitle } = setup(); view.set(page); document.querySelector(".wiki-tree-doc")!.click(); await flush(); document.querySelector(".wiki-note-title.editable")!.click(); const input = document.querySelector(".wiki-note-title-input")!; input.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true })); expect(document.querySelector(".wiki-note-title")!.textContent).toBe("WURZEL-DOC"); expect(setTitle).not.toHaveBeenCalled(); }); it("Löschen meldet mit relpath", async () => { const { view, actions } = setup(); view.set(page); document.querySelector(".wiki-tree-doc")!.click(); await flush(); document.querySelector(".wiki-note-actions .cmd-del")!.click(); expect(actions.remove).toHaveBeenCalledWith("2026-07-19_1000-wurzel-doc.md"); }); it("Plus im Notiz-Kopf: Formular legt Dokument im gewählten Ordner an", () => { const { view, actions } = setup(); view.set(page); document.querySelector(".wiki-tree summary .wiki-tree-name")!.click(); document.querySelector(".wiki-note-actions .wiki-add")!.click(); const form = document.querySelector(".wiki-form")!; expect(form.querySelector(".wiki-form-title")!.textContent).toBe("Neues Dokument"); const input = form.querySelector("input")!; input.value = "Deploy Notiz"; form.querySelector(".wiki-form-submit")!.click(); expect(actions.createDoc).toHaveBeenCalledWith("konzepte", "Deploy Notiz"); expect(document.querySelector(".wiki-form")).toBeNull(); }); it("Abbrechen und Escape schließen das Formular ohne Aktion", () => { const { view, actions } = setup(); view.set(page); document.querySelector(".wiki-note-actions .wiki-add")!.click(); document.querySelector(".wiki-form-cancel")!.click(); expect(document.querySelector(".wiki-form")).toBeNull(); document.querySelector(".wiki-note-actions .wiki-add")!.click(); document .querySelector(".wiki-form")! .dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true })); expect(document.querySelector(".wiki-form")).toBeNull(); expect(actions.createDoc).not.toHaveBeenCalled(); }); it("Rechtsklick auf Ordner: Menü legt Ordner darunter an", () => { const { view, actions } = setup(); view.set(page); const summary = document.querySelector(".wiki-tree summary")!; summary.dispatchEvent(new MouseEvent("contextmenu", { bubbles: true })); const items = [...document.querySelectorAll(".wiki-menu-item")]; items.find((i) => i.textContent === "Neuer Ordner")!.click(); const input = document.querySelector(".wiki-form input")!; expect(input.value).toBe("konzepte/"); input.value = "konzepte/neu"; input.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true })); expect(actions.createFolder).toHaveBeenCalledWith("konzepte/neu"); }); it("Rechtsklick auf Ordner: Dialog ändert den Ordnerpfad", () => { const { view, actions } = setup(); view.set(page); const summary = document.querySelector(".wiki-tree summary")!; summary.dispatchEvent(new MouseEvent("contextmenu", { bubbles: true })); const items = [...document.querySelectorAll(".wiki-menu-item")]; items.find((i) => i.textContent === "Ordnerpfad ändern")!.click(); const input = document.querySelector(".wiki-form input")!; expect(input.value).toBe("konzepte"); input.value = "notizen/2026"; input.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true })); expect(actions.moveFolder).toHaveBeenCalledWith("konzepte", "notizen/2026"); }); it("Rechtsklick auf Dokument: Menü löscht mit relpath", () => { const { view, actions } = setup(); view.set(page); document .querySelector(".wiki-tree-doc")! .dispatchEvent(new MouseEvent("contextmenu", { bubbles: true })); const items = [...document.querySelectorAll(".wiki-menu-item")]; items.find((i) => i.textContent === "Dokument löschen")!.click(); expect(actions.remove).toHaveBeenCalledWith("2026-07-19_1000-wurzel-doc.md"); expect(document.querySelector(".wiki-menu")).toBeNull(); }); }); describe("initWikiView — Leerzustände", () => { it("leeres Archiv erklärt das Archivieren", () => { const { view } = setup(); view.set( JSON.stringify({ kind: "page", home: "/a", tag: null, total: 0, tags: [], folders: [], }), ); expect(document.querySelector(".wiki-empty")!.textContent).toContain( "Das Archiv ist leer.", ); }); it("leerer Puffer fordert die Übersicht von selbst an", () => { const { view, openWiki } = setup(); view.set(""); expect(view.empty()).toBe(true); expect(openWiki).toHaveBeenCalledWith("tag:"); // Nur einmal — das Update kommt über den Puffer zurück. view.set(""); expect(openWiki).toHaveBeenCalledTimes(1); view.set(page); expect(view.empty()).toBe(false); }); });