UNPKG

insta-toc

Version:

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

268 lines (212 loc) 10.1 kB
import type { CachedMetadata, Editor, HeadingCache, Pos, SectionCache } from "obsidian"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { injectGlobals } from "../src/globals/globalFuncs"; import { // getLocalSettingsBulletTypeSuggestions, getLocalSettingsOmitSuggestions } from "../src/settings/localSettingsCompletionOptions"; import { Validator } from "../src/validator"; import { createEditor, createPluginMock } from "./mocks/pluginClassMocks"; injectGlobals(); 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; } type ValidatorFixture = { metadata: CachedMetadata; editor: Editor; filePath: string; }; function createFixture( localYamlLines: string[], headings?: HeadingCache[], filePath = "folder/file-a.md" ): ValidatorFixture { const lines: string[] = [ "```insta-toc", "---", ...localYamlLines, "---", "", "# Table of Contents", "", "- Existing", "```", "", "# Heading 1", "## Heading 2" ]; const closingFenceLine = lines.indexOf("```", 1); const section = createCodeSection(closingFenceLine); const metadata: CachedMetadata = { sections: [ section ], headings }; return { metadata, editor: createEditor(lines), filePath }; } function getHeadingExcludePattern(validator: Validator): RegExp | undefined { const getter = Reflect.get(Validator.prototype as object, "getHeadingExcludePattern") as ( this: Validator ) => RegExp | undefined; return getter.call(validator); } describe("Validator local settings behavior", () => { beforeEach(() => { vi.stubGlobal("window", { app: { plugins: { getPlugin: vi.fn().mockReturnValue({ settings: { tocTitle: "Table of Contents", bulletType: "dash" } }) } } }); }); afterEach(() => { vi.unstubAllGlobals(); }); test("re-validates when local settings change but headings do not", () => { const stableHeadings: HeadingCache[] = [ createHeading("Heading 1", 1, 10), createHeading("Heading 2", 2, 11) ]; const initial = createFixture([ "title:", " name: Table of Contents" ], stableHeadings); const { plugin, setEditor } = createPluginMock(undefined, initial.editor); const validator = new Validator(plugin.editorService, plugin.settings, initial.metadata, initial.filePath); expect(validator.isValid()).toBe(true); expect(validator.fileHeadings.map((h) => h.heading)).toEqual([ "Heading 1", "Heading 2" ]); const updatedLocalConfigOnly = createFixture( [ "levels:", " min: 2", " max: 6", "omit:", " - Heading 2" ], stableHeadings ); setEditor(updatedLocalConfigOnly.editor); validator.update( plugin.editorService, plugin.settings, updatedLocalConfigOnly.metadata, updatedLocalConfigOnly.filePath ); expect(validator.isValid()).toBe(true); expect(validator.localTocSettings.levels.min).toBe(2); expect(validator.fileHeadings).toEqual([]); }); test("returns false when neither headings nor local config changed", () => { const headings: HeadingCache[] = [ createHeading("Heading 1", 1, 10), createHeading("Heading 2", 2, 11) ]; const fixture = createFixture([ "title:", " name: Table of Contents" ], headings); const { plugin } = createPluginMock(undefined, fixture.editor); const validator = new Validator(plugin.editorService, plugin.settings, fixture.metadata, fixture.filePath); expect(validator.isValid()).toBe(true); validator.update(plugin.editorService, plugin.settings, fixture.metadata, fixture.filePath); expect(validator.isValid()).toBe(false); }); test("re-validates when forceRefresh is true even without content changes", () => { const fixture = createFixture([ "title:", " name: Table of Contents" ], [ createHeading("Heading 1", 1, 10), createHeading("Heading 2", 2, 11) ]); const { plugin } = createPluginMock(undefined, fixture.editor); const validator = new Validator(plugin.editorService, plugin.settings, fixture.metadata, fixture.filePath); expect(validator.isValid()).toBe(true); expect(validator.isValid(true)).toBe(true); }); test("handles missing heading cache safely", () => { const noHeadingsFixture = createFixture([ "title:", " name: Table of Contents" ], undefined); const { plugin } = createPluginMock(undefined, noHeadingsFixture.editor); const validator = new Validator( plugin.editorService, plugin.settings, noHeadingsFixture.metadata, noHeadingsFixture.filePath ); expect(validator.isValid()).toBe(true); expect(validator.fileHeadings).toEqual([]); }); test("resets local settings to global defaults when switching files", () => { const headings: HeadingCache[] = [ createHeading("Heading 1", 1, 10), createHeading("Heading 2", 2, 11) ]; const firstFile = createFixture( [ "levels:", " min: 2", " max: 6", "omit:", " - Heading 2" ], headings, "folder/file-a.md" ); const { plugin, setEditor } = createPluginMock(undefined, firstFile.editor); const validator = new Validator(plugin.editorService, plugin.settings, firstFile.metadata, firstFile.filePath); expect(validator.isValid()).toBe(true); expect(validator.localTocSettings.levels.min).toBe(2); expect(validator.fileHeadings).toEqual([]); const secondFile = createFixture([], headings, "folder/file-b.md"); setEditor(secondFile.editor); validator.update(plugin.editorService, plugin.settings, secondFile.metadata, secondFile.filePath); expect(validator.isValid()).toBe(true); expect(validator.localTocSettings.levels.min).toBeNull(); expect(validator.localTocSettings.omit).toBeNull(); expect(validator.fileHeadings.map((heading) => heading.heading)).toEqual([ "Heading 1", "Heading 2" ]); }); test("restores previous local settings when applying invalid yaml", () => { const fixture = createFixture([ "levels:", " min: 2", " max: 6" ], [ createHeading("Heading 1", 1, 10), createHeading("Heading 2", 2, 11) ]); const { plugin } = createPluginMock(undefined, fixture.editor); const validator = new Validator(plugin.editorService, plugin.settings, fixture.metadata, fixture.filePath); expect(validator.isValid()).toBe(true); expect(validator.localTocSettings.levels.min).toBe(2); const didApply = validator.applyLocalSettingsYaml("levels:\n min: 9"); expect(didApply).toBe(false); expect(validator.localTocSettings.levels.min).toBe(2); expect(validator.localTocSettings.levels.max).toBe(6); }); test("reuses the compiled heading exclusion regex until exclusion inputs change", () => { // Arrange const fixture = createFixture([], [ createHeading("Heading 1", 1, 10), createHeading("Heading 2", 2, 11) ]); const { plugin } = createPluginMock({ excludedChars: [ "*" ] }, fixture.editor); const validator = new Validator(plugin.editorService, plugin.settings, fixture.metadata, fixture.filePath); expect(validator.isValid()).toBe(true); // Act const firstPattern = getHeadingExcludePattern(validator); const secondPattern = getHeadingExcludePattern(validator); validator.applyLocalSettingsYaml("exclude: _"); const localChangePattern = getHeadingExcludePattern(validator); validator.update( plugin.editorService, { ...plugin.settings, excludedChars: [ "*", "#" ] }, fixture.metadata, fixture.filePath ); const globalChangePattern = getHeadingExcludePattern(validator); // Assert expect(firstPattern).toBeInstanceOf(RegExp); expect(secondPattern).toBe(firstPattern); expect(localChangePattern).toBeInstanceOf(RegExp); expect(localChangePattern).not.toBe(firstPattern); expect(globalChangePattern).toBeInstanceOf(RegExp); expect(globalChangePattern).not.toBe(localChangePattern); }); // test("provides list type suggestions for local settings completions", () => { // expect(getLocalSettingsBulletTypeSuggestions()).toEqual([ // "none", // "armenian", // "georgian", // "lower-greek", // "lower-latin", // "upper-latin", // "disc", // "circle", // "square", // "decimal", // "decimal-leading-zero", // "lower-alpha", // "upper-alpha", // "lower-roman", // "upper-roman" // ]); // }); test("builds omit suggestions from heading cache entries", () => { const headings: HeadingCache[] = [ createHeading(" Heading 1 ", 1, 10), createHeading("Heading 1", 2, 11), createHeading("", 3, 12), createHeading("Heading 2 <!-- omit -->", 4, 13), createHeading("Heading 3", 5, 14) ]; expect(getLocalSettingsOmitSuggestions(headings)).toEqual([ "Heading 1", "Heading 3" ]); }); });