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
text/typescript
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();
});
});