UNPKG

insta-toc

Version:

Simultaneously generate, update, and maintain a table of contents for your notes in real time.

181 lines (149 loc) 6.76 kB
import type { CachedMetadata, Editor, HeadingCache, Pos, SectionCache } from "obsidian"; import { describe, expect, test, vi } from "vitest"; vi.mock("obsidian-dev-utils/obsidian/plugin/plugin-base", () => ({ PluginBase: class PluginBase { public app: unknown; public manifest: unknown; public constructor(app?: unknown, manifest?: unknown) { this.app = app; this.manifest = manifest; } public registerEvent(): void {} public registerMarkdownCodeBlockProcessor(): void {} public addCommand(): void {} public addRibbonIcon(): void {} public async onloadImpl(): Promise<void> {} public async onunloadImpl(): Promise<void> {} } })); vi.mock("obsidian-dev-utils/obsidian/plugin/plugin-context", () => ({ initPluginContext: vi.fn() })); vi.mock("../src/settings/PluginSettingManager", () => ({ PluginSettingsManager: class PluginSettingsManager {} })); vi.mock("../src/settings/SettingsTab", () => ({ SettingTab: class SettingTab {} })); vi.mock( "../src/svelte", () => ({ CodeBlockComponent: class CodeBlockComponent {}, LocalSettingsModal: class LocalSettingsModal { public async open(): Promise<void> {} }, MarkdownComponentMounter: class MarkdownComponentMounter {} }) ); import { ManageToc } from "../src/ManageToc"; import InstaTocPlugin from "../src/Plugin"; import type EditorService from "../src/editorService"; import type { InstaTocSettings } from "../src/settings/Settings"; import { Validator } from "../src/validator"; import { createEditor, createPluginMock } from "./mocks/pluginClassMocks"; type ReloadFixture = { filePath: string; editor: Editor; metadata: CachedMetadata; }; function createHeading(heading: string, level: number, line: number): HeadingCache { const position: Pos = { start: { line, col: 0, offset: 0 }, end: { line, col: heading.length, offset: heading.length } }; return { heading, level, position } as HeadingCache; } function createCodeSection(closingFenceLine: number): SectionCache { return { type: "code", position: { start: { line: 0, col: 0, offset: 0 }, end: { line: closingFenceLine, col: 3, offset: 0 } } } as SectionCache; } function createFixture(headings: HeadingCache[], filePath = "folder/file-a.md"): ReloadFixture { const lines = [ "```insta-toc", "---", "title:", " name: Table of Contents", "---", "", "# Table of Contents", "", "- Existing", "```", "", "# Heading 1", "## Heading 2" ]; const closingFenceLine = lines.indexOf("```", 1); return { filePath, editor: createEditor(lines), metadata: { sections: [ createCodeSection(closingFenceLine) ], headings } }; } function createPluginUnderTest( settings: InstaTocSettings, editorService: EditorService, validator?: Validator ): InstaTocPlugin { const plugin = Object.create(InstaTocPlugin.prototype) as InstaTocPlugin; Object.defineProperty(plugin, "settings", { configurable: true, get: (): InstaTocSettings => settings }); Reflect.set(plugin as object, "_editorService", editorService); Reflect.set(plugin as object, "_validator", validator); return plugin; } describe("InstaTocPlugin.reload", () => { test("returns undefined when editor service state is incomplete", async () => { // Arrange const pluginMock = createPluginMock(); const plugin = createPluginUnderTest(pluginMock.plugin.settings, pluginMock.plugin.editorService); const manageSpy = vi.spyOn(ManageToc, "run").mockResolvedValue(); const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => undefined); // Act const result = await plugin.reload(); // Assert expect(result).toBeUndefined(); expect(manageSpy).not.toHaveBeenCalled(); consoleSpy.mockRestore(); manageSpy.mockRestore(); }); test("creates validator state and runs ManageToc from the unified reload path", async () => { // Arrange const fixture = createFixture([ createHeading("Heading 1", 1, 11), createHeading("Heading 2", 2, 12) ]); const pluginMock = createPluginMock(undefined, fixture.editor); pluginMock.setActiveFilePath(fixture.filePath); pluginMock.setMetadataCache(fixture.metadata); const plugin = createPluginUnderTest(pluginMock.plugin.settings, pluginMock.plugin.editorService); const manageSpy = vi.spyOn(ManageToc, "run").mockResolvedValue(); // Act const result = await plugin.reload({ forceValidate: true }); // Assert expect(result?.activeFile.path).toBe(fixture.filePath); expect(result?.isValid).toBe(true); expect(plugin.validator).toBeInstanceOf(Validator); expect(manageSpy).toHaveBeenCalledWith(plugin.editorService, plugin.settings, plugin.validator); manageSpy.mockRestore(); }); test("updates the existing validator and honors cache overrides without managing the toc", async () => { // Arrange const initialFixture = createFixture([ createHeading("Heading 1", 1, 11), createHeading("Heading 2", 2, 12) ]); const nextCache: CachedMetadata = { sections: initialFixture.metadata.sections, headings: [ createHeading("Heading 1", 1, 11), createHeading("Heading 3", 2, 12) ] }; const pluginMock = createPluginMock(undefined, initialFixture.editor); pluginMock.setActiveFilePath(initialFixture.filePath); pluginMock.setMetadataCache(initialFixture.metadata); const plugin = createPluginUnderTest(pluginMock.plugin.settings, pluginMock.plugin.editorService); const manageSpy = vi.spyOn(ManageToc, "run").mockResolvedValue(); await plugin.reload({ forceValidate: true, manageToc: false }); const validator = plugin.validator; const updateSpy = vi.spyOn(validator, "update"); // Act const result = await plugin.reload({ cache: nextCache, forceValidate: true, manageToc: false }); // Assert expect(pluginMock.plugin.editorService.syncState).toHaveBeenLastCalledWith(nextCache); expect(updateSpy).toHaveBeenCalledWith( plugin.editorService, plugin.settings, nextCache, initialFixture.filePath ); expect(result?.validator).toBe(validator); expect(result?.isValid).toBe(true); expect(manageSpy).not.toHaveBeenCalled(); updateSpy.mockRestore(); manageSpy.mockRestore(); }); });