import * as assert from "node:assert/strict"; import test from "node:test"; import type { EventRef, TAbstractFile, TFile, WorkspaceLeaf } from "obsidian"; import { PluginEventRouter } from "../src/app/pluginEventRouter"; type WorkspaceEventName = "file-open" | "active-leaf-change" | "editor-change"; type VaultEventName = "rename" | "delete" | "modify"; function createFile(path: string): TFile { return { path, basename: path.split("/").pop()?.replace(/\.[^.]+$/, "") ?? path, extension: path.split(".").pop() ?? "", } as TFile; } function createFolder(path: string): TAbstractFile { return { path, name: path.split("/").pop() ?? path, } as TAbstractFile; } function createHarness(options: { layoutReady?: boolean } = {}) { const calls: string[] = []; const registeredEvents: EventRef[] = []; const workspaceHandlers = new Map void>(); const vaultHandlers = new Map void>(); let layoutReadyHandler: (() => void | Promise) | null = null; const router = new PluginEventRouter({ app: { workspace: { layoutReady: options.layoutReady ?? false, on( eventName: WorkspaceEventName, handler: | ((file: TFile | null) => void) | ((leaf: WorkspaceLeaf | null) => void) | ((editor: unknown, info?: { file?: TFile | null }) => void), ): EventRef { workspaceHandlers.set(eventName, handler); return { name: `workspace:${eventName}` } as unknown as EventRef; }, onLayoutReady(handler: () => void | Promise): void { layoutReadyHandler = handler; }, }, vault: { on( eventName: VaultEventName, handler: | ((file: unknown, oldPath: string) => void | Promise) | ((file: unknown) => void | Promise), ): EventRef { vaultHandlers.set(eventName, handler); return { name: `vault:${eventName}` } as unknown as EventRef; }, }, }, registerEvent: (eventRef) => { registeredEvents.push(eventRef); }, isTFile: (value): value is TFile => !!value && typeof (value as TFile).path === "string" && typeof (value as TFile).extension === "string", handleLayoutReady: async () => { calls.push("layout-ready"); }, handleFileOpen: (file) => { calls.push(`file-open:${file?.path ?? "null"}`); }, handleActiveLeafChange: (leaf) => { calls.push(`active-leaf-change:${leaf ? "leaf" : "null"}`); }, handleFileRename: async (file, oldPath) => { calls.push(`rename:${oldPath}->${file?.path ?? "null"}`); }, handleFileDelete: async (file) => { calls.push(`delete:${file?.path ?? "null"}`); }, handleFileModify: async (file) => { calls.push(`modify:${file?.path ?? "null"}`); }, handleEditorChange: (filePath) => { calls.push(`editor-change:${filePath ?? "null"}`); }, }); return { calls, registeredEvents, router, workspaceHandlers, vaultHandlers, getLayoutReadyHandler: () => layoutReadyHandler, }; } test("plugin event router exposes Obsidian event flow in one module", async () => { const harness = createHarness(); const note = createFile("docs/a.md"); await harness.router.register(); assert.deepEqual( Array.from(harness.workspaceHandlers.keys()), ["file-open", "active-leaf-change", "editor-change"], ); assert.deepEqual( Array.from(harness.vaultHandlers.keys()), ["rename", "delete", "modify"], ); assert.equal(harness.registeredEvents.length, 6); harness.workspaceHandlers.get("file-open")?.(note); harness.workspaceHandlers.get("active-leaf-change")?.({} as WorkspaceLeaf); harness.workspaceHandlers.get("editor-change")?.({}, { file: note }); harness.vaultHandlers.get("rename")?.(note, "docs/old.md"); harness.vaultHandlers.get("delete")?.(note); harness.vaultHandlers.get("modify")?.(note); await Promise.resolve(); assert.deepEqual(harness.calls, [ "file-open:docs/a.md", "active-leaf-change:leaf", "editor-change:docs/a.md", "rename:docs/old.md->docs/a.md", "delete:docs/a.md", "modify:docs/a.md", ]); }); test("plugin event router preserves deleted folder paths", async () => { const harness = createHarness(); const folder = createFolder("Deleted"); await harness.router.register(); harness.vaultHandlers.get("delete")?.(folder); await Promise.resolve(); assert.deepEqual(harness.calls, ["delete:Deleted"]); }); test("plugin event router preserves immediate and deferred layout-ready handling", async () => { const readyHarness = createHarness({ layoutReady: true }); await readyHarness.router.register(); assert.deepEqual(readyHarness.calls, ["layout-ready"]); assert.equal(readyHarness.getLayoutReadyHandler(), null); const deferredHarness = createHarness({ layoutReady: false }); await deferredHarness.router.register(); assert.deepEqual(deferredHarness.calls, []); await deferredHarness.getLayoutReadyHandler()?.(); assert.deepEqual(deferredHarness.calls, ["layout-ready"]); });