mirror of
https://github.com/mnaoumov/obsidian-backlink-full-path.git
synced 2026-07-22 05:41:39 +00:00
chore: update template
This commit is contained in:
parent
cc483ff1a1
commit
fe25a77d33
12 changed files with 1317 additions and 631 deletions
|
|
@ -17,7 +17,8 @@
|
|||
"mnaoumov",
|
||||
"Naumov",
|
||||
"noopAsync",
|
||||
"tsbuildinfo"
|
||||
"tsbuildinfo",
|
||||
"unsanitized"
|
||||
],
|
||||
"ignoreWords": [],
|
||||
"import": [],
|
||||
|
|
|
|||
861
package-lock.json
generated
861
package-lock.json
generated
File diff suppressed because it is too large
Load diff
21
package.json
21
package.json
|
|
@ -27,9 +27,15 @@
|
|||
"spellcheck": "jiti scripts/spellcheck.ts",
|
||||
"test": "jiti scripts/test.ts",
|
||||
"test:coverage": "jiti scripts/test-coverage.ts",
|
||||
"test:integration": "jiti scripts/test-integration.ts",
|
||||
"test:integration:android": "jiti scripts/test-integration-android.ts",
|
||||
"test:integration:desktop": "jiti scripts/test-integration-desktop.ts",
|
||||
"test:integration:no-app": "jiti scripts/test-integration-no-app.ts",
|
||||
"test:watch": "jiti scripts/test-watch.ts",
|
||||
"version": "jiti scripts/version.ts"
|
||||
},
|
||||
"overrides": {
|
||||
"@vitest/coverage-v8": "$@vitest/coverage-v8",
|
||||
"eslint": "^10.4.0",
|
||||
"type-fest": "^5.6.0",
|
||||
"typescript": "^6.0.3"
|
||||
|
|
@ -38,11 +44,11 @@
|
|||
"@commitlint/cli": "^21.0.1",
|
||||
"@commitlint/config-conventional": "^21.0.1",
|
||||
"@commitlint/types": "^21.0.1",
|
||||
"@obsidian-typings/obsidian-public-latest": "^6.4.0",
|
||||
"@obsidian-typings/obsidian-public-latest": "^6.11.0",
|
||||
"@total-typescript/ts-reset": "^0.6.1",
|
||||
"@tsconfig/strictest": "^2.0.8",
|
||||
"@types/node": "^25.8.0",
|
||||
"@vitest/coverage-v8": "^4.1.6",
|
||||
"@types/node": "^25.9.1",
|
||||
"@vitest/coverage-v8": "^4.1.7",
|
||||
"better-typescript-lib": "^2.12.0",
|
||||
"czg": "^1.13.1",
|
||||
"eslint": "^10.4.0",
|
||||
|
|
@ -51,9 +57,10 @@
|
|||
"jsdom": "^29.1.1",
|
||||
"nano-staged": "^1.0.2",
|
||||
"obsidian": "^1.12.3",
|
||||
"obsidian-dev-utils": "^66.0.1",
|
||||
"obsidian-test-mocks": "^2.0.3",
|
||||
"sass-embedded": "^1.99.0",
|
||||
"vitest": "^4.1.6"
|
||||
"obsidian-dev-utils": "^69.1.1",
|
||||
"obsidian-integration-testing": "^4.1.5",
|
||||
"obsidian-test-mocks": "^3.0.0",
|
||||
"sass-embedded": "^1.100.0",
|
||||
"vitest": "^4.1.7"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,9 @@ export const config = defineConfig({
|
|||
inline: ['@obsidian-typings', 'obsidian-dev-utils']
|
||||
}
|
||||
},
|
||||
setupFiles: ['obsidian-test-mocks/setup']
|
||||
setupFiles: [
|
||||
'obsidian-test-mocks/vitest-setup',
|
||||
'obsidian-test-mocks/obsidian-typings/vitest-setup'
|
||||
]
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,11 +10,12 @@ import {
|
|||
it
|
||||
} from 'vitest';
|
||||
|
||||
// eslint-disable-next-line import-x/no-rename-default -- Renamed to avoid collision with the named `Plugin` import.
|
||||
import mainExport from './main.ts';
|
||||
import { Plugin } from './plugin.ts';
|
||||
|
||||
describe('main', () => {
|
||||
it('should export Plugin as default export', async () => {
|
||||
const mainModule = await import('./main.ts') as { default: typeof Plugin };
|
||||
expect(mainModule.default).toBe(Plugin);
|
||||
it('should export Plugin as default export', () => {
|
||||
expect(mainExport).toBe(Plugin);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,42 +0,0 @@
|
|||
/**
|
||||
* @file
|
||||
*
|
||||
* Tests for PluginSettingsComponent.
|
||||
*/
|
||||
|
||||
import {
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
vi
|
||||
} from 'vitest';
|
||||
|
||||
import { PluginSettings } from './plugin-settings.ts';
|
||||
import { PluginSettingsComponent } from './plugin-settings-component.ts';
|
||||
|
||||
describe('PluginSettingsComponent', () => {
|
||||
it('should create default settings as PluginSettings instance', () => {
|
||||
const component = new PluginSettingsComponent({
|
||||
loadData: vi.fn().mockResolvedValue(null),
|
||||
saveData: vi.fn().mockResolvedValue(undefined)
|
||||
});
|
||||
|
||||
expect(component.defaultSettings).toBeInstanceOf(PluginSettings);
|
||||
});
|
||||
|
||||
it('should have all 7 default setting properties', () => {
|
||||
const component = new PluginSettingsComponent({
|
||||
loadData: vi.fn().mockResolvedValue(null),
|
||||
saveData: vi.fn().mockResolvedValue(undefined)
|
||||
});
|
||||
|
||||
const defaults = component.defaultSettings;
|
||||
expect(defaults).toHaveProperty('pathDepth');
|
||||
expect(defaults).toHaveProperty('rootPaths');
|
||||
expect(defaults).toHaveProperty('shouldDisplayParentPathOnSeparateLine');
|
||||
expect(defaults).toHaveProperty('shouldHighlightFileName');
|
||||
expect(defaults).toHaveProperty('shouldIncludeExtension');
|
||||
expect(defaults).toHaveProperty('shouldReversePathParts');
|
||||
expect(defaults).toHaveProperty('shouldShowEllipsisForSkippedPathParts');
|
||||
});
|
||||
});
|
||||
|
|
@ -4,20 +4,23 @@
|
|||
* Settings component that manages plugin settings persistence.
|
||||
*/
|
||||
|
||||
import type { DataHandler } from 'obsidian-dev-utils/obsidian/data-handler';
|
||||
import type { PluginEventSource } from 'obsidian-dev-utils/obsidian/plugin/plugin-event-source';
|
||||
|
||||
import { PluginSettingsComponentBase } from 'obsidian-dev-utils/obsidian/components/plugin-settings-component';
|
||||
|
||||
import { PluginSettings } from './plugin-settings.ts';
|
||||
|
||||
/**
|
||||
* Manages persistence and lifecycle for {@link PluginSettings}.
|
||||
*/
|
||||
interface PluginSettingsComponentConstructorParams {
|
||||
readonly dataHandler: DataHandler;
|
||||
readonly pluginEventSource: PluginEventSource;
|
||||
}
|
||||
|
||||
export class PluginSettingsComponent extends PluginSettingsComponentBase<PluginSettings> {
|
||||
/**
|
||||
* Creates the default settings instance.
|
||||
*
|
||||
* @returns A new {@link PluginSettings} with default values.
|
||||
*/
|
||||
protected override createDefaultSettings(): PluginSettings {
|
||||
return new PluginSettings();
|
||||
public constructor(params: PluginSettingsComponentConstructorParams) {
|
||||
super({
|
||||
...params,
|
||||
pluginSettingsClass: PluginSettings
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,67 +4,159 @@
|
|||
* Tests for the PluginSettingsTab class.
|
||||
*/
|
||||
|
||||
import type { Plugin as ObsidianPlugin } from 'obsidian';
|
||||
import type { PluginManifest } from 'obsidian';
|
||||
|
||||
import { App } from 'obsidian-test-mocks/obsidian';
|
||||
import {
|
||||
App,
|
||||
ToggleComponent
|
||||
} from 'obsidian-test-mocks/obsidian';
|
||||
import {
|
||||
beforeAll,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
vi
|
||||
it
|
||||
} from 'vitest';
|
||||
|
||||
import { PluginSettingsComponent } from './plugin-settings-component.ts';
|
||||
import { PluginSettingsTab } from './plugin-settings-tab.ts';
|
||||
import { Plugin } from './plugin.ts';
|
||||
|
||||
interface DisplayedTabResult {
|
||||
names: string[];
|
||||
tab: PluginSettingsTab;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a mock plugin object for testing.
|
||||
* Creates a PluginSettingsTab attached to a freshly created Plugin and calls display().
|
||||
*
|
||||
* @returns A mock plugin.
|
||||
* @returns An object with the tab and the extracted setting names.
|
||||
*/
|
||||
function createMockPlugin(): ObsidianPlugin {
|
||||
const app = App.createConfigured__();
|
||||
return {
|
||||
app: app.asOriginalType__(),
|
||||
manifest: {
|
||||
author: 'test',
|
||||
description: 'test',
|
||||
id: 'backlink-full-path',
|
||||
minAppVersion: '0.0.0',
|
||||
name: 'Backlink Full Path',
|
||||
version: '1.0.0'
|
||||
}
|
||||
} as unknown as ObsidianPlugin;
|
||||
function createDisplayedTab(): DisplayedTabResult {
|
||||
const plugin = createPlugin();
|
||||
const pluginSettingsComponent = Reflect.get(plugin, 'pluginSettingsComponent') as InstanceType<
|
||||
typeof import('./plugin-settings-component.ts').PluginSettingsComponent
|
||||
>;
|
||||
|
||||
const tab = new PluginSettingsTab({
|
||||
plugin,
|
||||
pluginSettingsComponent
|
||||
});
|
||||
|
||||
tab.display();
|
||||
const names = getSettingNames(tab.containerEl);
|
||||
return { names, tab };
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Plugin instance with mocked dependencies.
|
||||
*
|
||||
* @returns The created Plugin.
|
||||
*/
|
||||
function createPlugin(): Plugin {
|
||||
const app = App.createConfigured__();
|
||||
const manifest: PluginManifest = {
|
||||
author: 'test',
|
||||
description: 'test',
|
||||
id: 'backlink-full-path',
|
||||
minAppVersion: '0.0.0',
|
||||
name: 'Backlink Full Path',
|
||||
version: '1.0.0'
|
||||
};
|
||||
return new Plugin(app.asOriginalType__(), manifest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts setting names from the tab's containerEl after calling display().
|
||||
*
|
||||
* Each Setting mock creates a settingEl > [controlEl, infoEl > [nameEl, descEl]].
|
||||
* The nameEl is the second-level div that contains the setting name text.
|
||||
*
|
||||
* @param containerEl - The container element.
|
||||
* @returns The array of setting name strings.
|
||||
*/
|
||||
function getSettingNames(containerEl: HTMLElement): string[] {
|
||||
const names: string[] = [];
|
||||
for (const settingEl of containerEl.children) {
|
||||
// SettingEl contains [controlEl, infoEl].
|
||||
// InfoEl contains [nameEl, descEl].
|
||||
const infoEl = settingEl.children[1];
|
||||
if (infoEl) {
|
||||
const nameEl = infoEl.children[0];
|
||||
if (nameEl?.textContent) {
|
||||
names.push(nameEl.textContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
/**
|
||||
* Patches missing mock properties on obsidian-test-mocks components
|
||||
* that obsidian-dev-utils's `bind()` method probes via strict proxy.
|
||||
*/
|
||||
function patchMissingMockProperties(): void {
|
||||
// Obsidian-dev-utils checks for setPlaceholderValue to detect text-based components.
|
||||
if (!('setPlaceholderValue' in ToggleComponent.prototype)) {
|
||||
Object.defineProperty(ToggleComponent.prototype, 'setPlaceholderValue', { value: undefined });
|
||||
}
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
patchMissingMockProperties();
|
||||
});
|
||||
|
||||
describe('PluginSettingsTab', () => {
|
||||
it('should be constructable', () => {
|
||||
const plugin = createMockPlugin();
|
||||
const settingsComponent = new PluginSettingsComponent({
|
||||
loadData: vi.fn().mockResolvedValue(null),
|
||||
saveData: vi.fn().mockResolvedValue(undefined)
|
||||
});
|
||||
|
||||
const plugin = createPlugin();
|
||||
const pluginSettingsComponent = Reflect.get(plugin, 'pluginSettingsComponent') as InstanceType<
|
||||
typeof import('./plugin-settings-component.ts').PluginSettingsComponent
|
||||
>;
|
||||
const tab = new PluginSettingsTab({
|
||||
plugin,
|
||||
settingsComponent
|
||||
pluginSettingsComponent
|
||||
});
|
||||
|
||||
expect(tab).toBeInstanceOf(PluginSettingsTab);
|
||||
});
|
||||
|
||||
it('should have a containerEl after construction', () => {
|
||||
const plugin = createMockPlugin();
|
||||
const settingsComponent = new PluginSettingsComponent({
|
||||
loadData: vi.fn().mockResolvedValue(null),
|
||||
saveData: vi.fn().mockResolvedValue(undefined)
|
||||
describe('display', () => {
|
||||
it('should create all 7 settings', () => {
|
||||
const { names } = createDisplayedTab();
|
||||
const EXPECTED_SETTING_COUNT = 7;
|
||||
expect(names.length).toBe(EXPECTED_SETTING_COUNT);
|
||||
});
|
||||
|
||||
const tab = new PluginSettingsTab({
|
||||
plugin,
|
||||
settingsComponent
|
||||
it('should create Include extension setting', () => {
|
||||
const { names } = createDisplayedTab();
|
||||
expect(names).toContain('Include extension');
|
||||
});
|
||||
|
||||
expect(tab.containerEl).toBeDefined();
|
||||
it('should create Path depth setting', () => {
|
||||
const { names } = createDisplayedTab();
|
||||
expect(names).toContain('Path depth');
|
||||
});
|
||||
|
||||
it('should create Show ellipsis for skipped path parts setting', () => {
|
||||
const { names } = createDisplayedTab();
|
||||
expect(names).toContain('Show ellipsis for skipped path parts');
|
||||
});
|
||||
|
||||
it('should create Highlight file name setting', () => {
|
||||
const { names } = createDisplayedTab();
|
||||
expect(names).toContain('Highlight file name');
|
||||
});
|
||||
|
||||
it('should create Reverse path parts setting', () => {
|
||||
const { names } = createDisplayedTab();
|
||||
expect(names).toContain('Reverse path parts');
|
||||
});
|
||||
|
||||
it('should create Display parent path on separate line setting', () => {
|
||||
const { names } = createDisplayedTab();
|
||||
expect(names).toContain('Display parent path on separate line');
|
||||
});
|
||||
|
||||
it('should create Root paths setting', () => {
|
||||
const { names } = createDisplayedTab();
|
||||
expect(names).toContain('Root paths');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,11 +1,3 @@
|
|||
/**
|
||||
* @file
|
||||
*
|
||||
* Settings tab UI for the Backlink Full Path plugin.
|
||||
*/
|
||||
|
||||
import type { PluginSettingsTabBaseParams } from 'obsidian-dev-utils/obsidian/plugin/plugin-settings-tab';
|
||||
|
||||
import { Setting } from 'obsidian';
|
||||
import { appendCodeBlock } from 'obsidian-dev-utils/html-element';
|
||||
import { PluginSettingsTabBase } from 'obsidian-dev-utils/obsidian/plugin/plugin-settings-tab';
|
||||
|
|
@ -13,22 +5,7 @@ import { SettingEx } from 'obsidian-dev-utils/obsidian/setting-ex';
|
|||
|
||||
import type { PluginSettings } from './plugin-settings.ts';
|
||||
|
||||
/**
|
||||
* Settings tab for the Backlink Full Path plugin.
|
||||
*/
|
||||
export class PluginSettingsTab extends PluginSettingsTabBase<PluginSettings> {
|
||||
/**
|
||||
* Creates a new settings tab.
|
||||
*
|
||||
* @param params - The settings tab params.
|
||||
*/
|
||||
public constructor(params: PluginSettingsTabBaseParams<PluginSettings>) {
|
||||
super(params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the settings tab UI.
|
||||
*/
|
||||
public override display(): void {
|
||||
super.display();
|
||||
|
||||
|
|
|
|||
|
|
@ -4,17 +4,91 @@
|
|||
* Tests for the Plugin class.
|
||||
*/
|
||||
|
||||
import type { PluginManifest } from 'obsidian';
|
||||
import type {
|
||||
BacklinkView,
|
||||
ResultDom,
|
||||
ResultDomItem,
|
||||
ResultDomResult
|
||||
} from '@obsidian-typings/obsidian-public-latest';
|
||||
import type {
|
||||
PluginManifest,
|
||||
TFile
|
||||
} from 'obsidian';
|
||||
import type { PartialDeep } from 'type-fest';
|
||||
|
||||
import { App } from 'obsidian-test-mocks/obsidian';
|
||||
import {
|
||||
bypassStrictProxy,
|
||||
strictProxy
|
||||
} from 'obsidian-dev-utils/strict-proxy';
|
||||
import {
|
||||
App,
|
||||
MarkdownView,
|
||||
WorkspaceLeaf
|
||||
} from 'obsidian-test-mocks/obsidian';
|
||||
import {
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it
|
||||
it,
|
||||
vi
|
||||
} from 'vitest';
|
||||
|
||||
import type { PluginSettings } from './plugin-settings.ts';
|
||||
|
||||
import { Plugin } from './plugin.ts';
|
||||
|
||||
interface MockCorePlugin {
|
||||
enabled: boolean;
|
||||
instance: object;
|
||||
}
|
||||
|
||||
interface SettingsComponentProxy {
|
||||
on(...args: never[]): void;
|
||||
settings: PluginSettings;
|
||||
}
|
||||
|
||||
interface TestPluginResult {
|
||||
app: ReturnType<typeof App.createConfigured__>;
|
||||
plugin: Plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a mock TFile for testing.
|
||||
*
|
||||
* @param path - The file path.
|
||||
* @returns A mock TFile.
|
||||
*/
|
||||
function createMockFile(path: string): TFile {
|
||||
const parts = path.split('/');
|
||||
const name = parts.at(-1) ?? '';
|
||||
const dotIndex = name.lastIndexOf('.');
|
||||
const basename = dotIndex >= 0 ? name.slice(0, dotIndex) : name;
|
||||
const extension = dotIndex >= 0 ? name.slice(dotIndex + 1) : '';
|
||||
return mockProxy<TFile>({
|
||||
basename,
|
||||
extension,
|
||||
name,
|
||||
path
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a mock ResultDomItem with an element that has .tree-item-inner.
|
||||
*
|
||||
* @param hasTreeItemInner - Whether the result element should have a .tree-item-inner child.
|
||||
* @returns The mock ResultDomItem.
|
||||
*/
|
||||
function createMockResultDomItem(hasTreeItemInner: boolean): ResultDomItem {
|
||||
const el = activeDocument.createElement('div');
|
||||
if (hasTreeItemInner) {
|
||||
const inner = activeDocument.createElement('div');
|
||||
inner.classList.add('tree-item-inner');
|
||||
inner.textContent = 'Original';
|
||||
el.appendChild(inner);
|
||||
}
|
||||
return mockProxy<ResultDomItem>({ el });
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a minimal PluginManifest for testing.
|
||||
*
|
||||
|
|
@ -31,18 +105,659 @@ function createTestManifest(): PluginManifest {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a test Plugin instance with a configured App.
|
||||
*
|
||||
* @returns The plugin and app.
|
||||
*/
|
||||
function createTestPlugin(): TestPluginResult {
|
||||
const app = App.createConfigured__();
|
||||
const manifest = createTestManifest();
|
||||
const plugin = new Plugin(app.asOriginalType__(), manifest);
|
||||
return { app, plugin };
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the plugin's private pluginSettingsComponent via Reflect.
|
||||
*
|
||||
* @param plugin - The plugin instance.
|
||||
* @returns The pluginSettingsComponent.
|
||||
*/
|
||||
function getSettingsComponent(plugin: Plugin): SettingsComponentProxy {
|
||||
return Reflect.get(plugin, 'pluginSettingsComponent') as SettingsComponentProxy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets `_loaded` to true on the plugin (bypassing strict proxy).
|
||||
*
|
||||
* @param plugin - The plugin to mark as loaded.
|
||||
*/
|
||||
function markPluginLoaded(plugin: Plugin): void {
|
||||
Reflect.set(bypassStrictProxy(plugin), '_loaded', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up internalPlugins mock on the plugin's app.
|
||||
*
|
||||
* @param plugin - The plugin.
|
||||
* @param returnValue - The value to return from getPluginById.
|
||||
* @returns The mocked getPluginById function.
|
||||
*/
|
||||
function mockInternalPlugins(plugin: Plugin, returnValue: unknown): ReturnType<typeof vi.fn> {
|
||||
const getPluginById = vi.fn().mockReturnValue(returnValue);
|
||||
Reflect.set(bypassStrictProxy(plugin.app), 'internalPlugins', strictProxy({ getPluginById }));
|
||||
return getPluginById;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a strict proxy, working around `PartialDeep<T>` incompatibility
|
||||
* with complex DOM types by casting the partial before passing to strictProxy.
|
||||
*
|
||||
* @param partial - A partial object with the mocked members.
|
||||
* @returns A strict proxy typed as T.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- T is used in the body for strictProxy<T>.
|
||||
function mockProxy<T>(partial: object): T {
|
||||
return strictProxy<T>(partial as PartialDeep<T>);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls a private/protected method on an object via Reflect and binds it.
|
||||
*
|
||||
* @param obj - The object.
|
||||
* @param name - The method name.
|
||||
* @returns The bound method.
|
||||
*/
|
||||
function privateMethod(obj: object, name: string): (...args: never[]) => unknown {
|
||||
const fn = Reflect.get(obj, name) as (...args: never[]) => unknown;
|
||||
return fn.bind(obj);
|
||||
}
|
||||
|
||||
describe('Plugin', () => {
|
||||
it('should be constructable', () => {
|
||||
const app = App.createConfigured__();
|
||||
const manifest = createTestManifest();
|
||||
const plugin = new Plugin(app.asOriginalType__(), manifest);
|
||||
const { plugin } = createTestPlugin();
|
||||
expect(plugin).toBeInstanceOf(Plugin);
|
||||
});
|
||||
|
||||
it('should have the correct manifest id', () => {
|
||||
const app = App.createConfigured__();
|
||||
const manifest = createTestManifest();
|
||||
const plugin = new Plugin(app.asOriginalType__(), manifest);
|
||||
const { plugin } = createTestPlugin();
|
||||
expect(plugin.manifest.id).toBe('backlink-full-path');
|
||||
});
|
||||
|
||||
describe('onload', () => {
|
||||
it('should register saveSettings event listener', () => {
|
||||
const { plugin } = createTestPlugin();
|
||||
const settingsComponent = getSettingsComponent(plugin);
|
||||
const onSpy = vi.spyOn(settingsComponent, 'on');
|
||||
|
||||
plugin.onload();
|
||||
|
||||
expect(onSpy).toHaveBeenCalledWith('saveSettings', expect.any(Function));
|
||||
});
|
||||
|
||||
it('should call refreshBacklinkPanels when saveSettings is triggered', async () => {
|
||||
const { plugin } = createTestPlugin();
|
||||
|
||||
const refreshBacklinkPanels = vi.fn().mockResolvedValue(undefined);
|
||||
Reflect.set(plugin, 'refreshBacklinkPanels', refreshBacklinkPanels);
|
||||
|
||||
const settingsComponent = getSettingsComponent(plugin);
|
||||
let savedCallback: (() => Promise<void>) | undefined;
|
||||
vi.spyOn(settingsComponent, 'on').mockImplementation(
|
||||
((_event: never, callback: never) => {
|
||||
savedCallback = callback;
|
||||
}) as (...args: never[]) => void
|
||||
);
|
||||
|
||||
plugin.onload();
|
||||
|
||||
expect(savedCallback).toBeDefined();
|
||||
if (savedCallback) {
|
||||
await savedCallback();
|
||||
}
|
||||
|
||||
expect(refreshBacklinkPanels).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('onLayoutReady', () => {
|
||||
it('should return early when backlinks core plugin is not found', async () => {
|
||||
const { plugin } = createTestPlugin();
|
||||
|
||||
const getPluginById = mockInternalPlugins(plugin, undefined);
|
||||
|
||||
const onLayoutReady = privateMethod(plugin, 'onLayoutReady');
|
||||
await onLayoutReady();
|
||||
|
||||
expect(getPluginById).toHaveBeenCalledWith('backlink');
|
||||
});
|
||||
|
||||
it('should patch but not call patchBacklinksPane when backlinks plugin is disabled', async () => {
|
||||
const { plugin } = createTestPlugin();
|
||||
markPluginLoaded(plugin);
|
||||
|
||||
const instanceProto = {};
|
||||
const backlinksCorePlugin = mockProxy<MockCorePlugin>({
|
||||
enabled: false,
|
||||
instance: Object.create(instanceProto)
|
||||
});
|
||||
|
||||
mockInternalPlugins(plugin, backlinksCorePlugin);
|
||||
|
||||
const patchBacklinksPane = vi.fn().mockResolvedValue(undefined);
|
||||
Reflect.set(plugin, 'patchBacklinksPane', patchBacklinksPane);
|
||||
|
||||
const onLayoutReady = privateMethod(plugin, 'onLayoutReady');
|
||||
await onLayoutReady();
|
||||
|
||||
expect(patchBacklinksPane).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call patchBacklinksPane and refreshBacklinkPanels when backlinks plugin is enabled', async () => {
|
||||
const { plugin } = createTestPlugin();
|
||||
markPluginLoaded(plugin);
|
||||
|
||||
const instanceProto = {};
|
||||
const backlinksCorePlugin = mockProxy<MockCorePlugin>({
|
||||
enabled: true,
|
||||
instance: Object.create(instanceProto)
|
||||
});
|
||||
|
||||
mockInternalPlugins(plugin, backlinksCorePlugin);
|
||||
|
||||
const patchBacklinksPane = vi.fn().mockResolvedValue(undefined);
|
||||
const refreshBacklinkPanels = vi.fn().mockResolvedValue(undefined);
|
||||
Reflect.set(plugin, 'patchBacklinksPane', patchBacklinksPane);
|
||||
Reflect.set(plugin, 'refreshBacklinkPanels', refreshBacklinkPanels);
|
||||
|
||||
const onLayoutReady = privateMethod(plugin, 'onLayoutReady');
|
||||
await onLayoutReady();
|
||||
|
||||
expect(patchBacklinksPane).toHaveBeenCalled();
|
||||
expect(refreshBacklinkPanels).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should invoke onUserEnable patch that calls onBacklinksCorePluginEnable', async () => {
|
||||
const { plugin } = createTestPlugin();
|
||||
markPluginLoaded(plugin);
|
||||
|
||||
const instanceProto = { onUserEnable: vi.fn() };
|
||||
const backlinksCorePlugin = mockProxy<MockCorePlugin>({
|
||||
enabled: false,
|
||||
instance: Object.create(instanceProto) as object
|
||||
});
|
||||
|
||||
mockInternalPlugins(plugin, backlinksCorePlugin);
|
||||
|
||||
const onBacklinksCorePluginEnable = vi.fn();
|
||||
Reflect.set(plugin, 'onBacklinksCorePluginEnable', onBacklinksCorePluginEnable);
|
||||
|
||||
const onLayoutReady = privateMethod(plugin, 'onLayoutReady');
|
||||
await onLayoutReady();
|
||||
|
||||
const patchedOnUserEnable = instanceProto.onUserEnable as () => void;
|
||||
patchedOnUserEnable.call(backlinksCorePlugin.instance);
|
||||
|
||||
expect(onBacklinksCorePluginEnable).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should register unload callback that calls refreshBacklinkPanels', async () => {
|
||||
const { plugin } = createTestPlugin();
|
||||
markPluginLoaded(plugin);
|
||||
|
||||
const instanceProto = {};
|
||||
const backlinksCorePlugin = mockProxy<MockCorePlugin>({
|
||||
enabled: false,
|
||||
instance: Object.create(instanceProto)
|
||||
});
|
||||
|
||||
mockInternalPlugins(plugin, backlinksCorePlugin);
|
||||
|
||||
const refreshBacklinkPanels = vi.fn().mockResolvedValue(undefined);
|
||||
Reflect.set(plugin, 'refreshBacklinkPanels', refreshBacklinkPanels);
|
||||
|
||||
const registerCallbacks: (() => void)[] = [];
|
||||
vi.spyOn(plugin, 'register').mockImplementation((cb: () => void) => {
|
||||
registerCallbacks.push(cb);
|
||||
});
|
||||
|
||||
const onLayoutReady = privateMethod(plugin, 'onLayoutReady');
|
||||
await onLayoutReady();
|
||||
|
||||
expect(registerCallbacks.length).toBeGreaterThan(0);
|
||||
const callback = registerCallbacks[0];
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
|
||||
expect(refreshBacklinkPanels).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('addResult', () => {
|
||||
let plugin: Plugin;
|
||||
let addResult: (...args: never[]) => unknown;
|
||||
|
||||
beforeEach(() => {
|
||||
const result = createTestPlugin();
|
||||
plugin = result.plugin;
|
||||
addResult = privateMethod(plugin, 'addResult');
|
||||
});
|
||||
|
||||
it('should replace tree-item-inner content with generated backlink title', () => {
|
||||
const file = createMockFile('folder/subfolder/note.md');
|
||||
const resultDomItem = createMockResultDomItem(true);
|
||||
const next = vi.fn().mockReturnValue(resultDomItem);
|
||||
const resultDom = mockProxy<ResultDom>({});
|
||||
const result = mockProxy<ResultDomResult>({});
|
||||
|
||||
const returnedItem = (addResult as (...args: unknown[]) => unknown)(next, resultDom, file, result, 'content', true) as ResultDomItem;
|
||||
|
||||
expect(next).toHaveBeenCalledWith(file, result, 'content', true);
|
||||
expect(returnedItem).toBe(resultDomItem);
|
||||
|
||||
const inner = resultDomItem.el.querySelector('.tree-item-inner');
|
||||
expect(inner?.textContent).not.toBe('Original');
|
||||
});
|
||||
|
||||
it('should not modify result when tree-item-inner is absent', () => {
|
||||
const file = createMockFile('note.md');
|
||||
const resultDomItem = createMockResultDomItem(false);
|
||||
const next = vi.fn().mockReturnValue(resultDomItem);
|
||||
const resultDom = mockProxy<ResultDom>({});
|
||||
const result = mockProxy<ResultDomResult>({});
|
||||
|
||||
const returnedItem = (addResult as (...args: unknown[]) => unknown)(next, resultDom, file, result, 'content') as ResultDomItem;
|
||||
expect(returnedItem).toBe(resultDomItem);
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateBacklinkTitle', () => {
|
||||
let plugin: Plugin;
|
||||
let settings: PluginSettings;
|
||||
|
||||
beforeEach(() => {
|
||||
const result = createTestPlugin();
|
||||
plugin = result.plugin;
|
||||
settings = getSettingsComponent(plugin).settings;
|
||||
});
|
||||
|
||||
/**
|
||||
* Calls the private generateBacklinkTitle method.
|
||||
*
|
||||
* @param file - The TFile to generate a title for.
|
||||
* @returns The generated HTMLDivElement.
|
||||
*/
|
||||
function callGenerateBacklinkTitle(file: TFile): HTMLDivElement {
|
||||
const fn = privateMethod(plugin, 'generateBacklinkTitle');
|
||||
return (fn as (...args: unknown[]) => unknown)(file) as HTMLDivElement;
|
||||
}
|
||||
|
||||
it('should include extension when shouldIncludeExtension is true', () => {
|
||||
settings.shouldIncludeExtension = true;
|
||||
const file = createMockFile('folder/note.md');
|
||||
const container = callGenerateBacklinkTitle(file);
|
||||
const fileNameSpan = container.shadowRoot?.querySelector('[part="file-name"]');
|
||||
expect(fileNameSpan?.textContent).toBe('note.md');
|
||||
});
|
||||
|
||||
it('should exclude extension when shouldIncludeExtension is false', () => {
|
||||
settings.shouldIncludeExtension = false;
|
||||
const file = createMockFile('folder/note.md');
|
||||
const container = callGenerateBacklinkTitle(file);
|
||||
const fileNameSpan = container.shadowRoot?.querySelector('[part="file-name"]');
|
||||
expect(fileNameSpan?.textContent).toBe('note');
|
||||
});
|
||||
|
||||
it('should show file at root with no parent path', () => {
|
||||
const file = createMockFile('note.md');
|
||||
const container = callGenerateBacklinkTitle(file);
|
||||
const parentPathSpan = container.shadowRoot?.querySelector('[part="parent-path"]');
|
||||
expect(parentPathSpan).toBeNull();
|
||||
});
|
||||
|
||||
it('should show parent path with trailing separator', () => {
|
||||
settings.shouldReversePathParts = false;
|
||||
settings.shouldDisplayParentPathOnSeparateLine = false;
|
||||
const file = createMockFile('folder/note.md');
|
||||
const container = callGenerateBacklinkTitle(file);
|
||||
const parentPathSpan = container.shadowRoot?.querySelector('[part="parent-path"]');
|
||||
expect(parentPathSpan?.textContent).toBe('folder/');
|
||||
});
|
||||
|
||||
it('should strip root path prefix', () => {
|
||||
settings.rootPaths = ['folder'];
|
||||
const file = createMockFile('folder/subfolder/note.md');
|
||||
const container = callGenerateBacklinkTitle(file);
|
||||
const parentPathSpan = container.shadowRoot?.querySelector('[part="parent-path"]');
|
||||
expect(parentPathSpan?.textContent).toBe('subfolder/');
|
||||
});
|
||||
|
||||
it('should strip nested root path prefix', () => {
|
||||
settings.rootPaths = ['folder/subfolder'];
|
||||
const file = createMockFile('folder/subfolder/deep/note.md');
|
||||
const container = callGenerateBacklinkTitle(file);
|
||||
const parentPathSpan = container.shadowRoot?.querySelector('[part="parent-path"]');
|
||||
expect(parentPathSpan?.textContent).toBe('deep/');
|
||||
});
|
||||
|
||||
it('should limit depth with pathDepth setting', () => {
|
||||
settings.pathDepth = 2;
|
||||
settings.shouldShowEllipsisForSkippedPathParts = false;
|
||||
const file = createMockFile('a/b/c/note.md');
|
||||
const container = callGenerateBacklinkTitle(file);
|
||||
const parentPathSpan = container.shadowRoot?.querySelector('[part="parent-path"]');
|
||||
expect(parentPathSpan?.textContent).toBe('c/');
|
||||
});
|
||||
|
||||
it('should add ellipsis for skipped parts when enabled', () => {
|
||||
settings.pathDepth = 2;
|
||||
settings.shouldShowEllipsisForSkippedPathParts = true;
|
||||
const file = createMockFile('a/b/c/note.md');
|
||||
const container = callGenerateBacklinkTitle(file);
|
||||
const parentPathSpan = container.shadowRoot?.querySelector('[part="parent-path"]');
|
||||
expect(parentPathSpan?.textContent).toBe('.../c/');
|
||||
});
|
||||
|
||||
it('should not add ellipsis when pathDepth does not truncate', () => {
|
||||
settings.pathDepth = 5;
|
||||
settings.shouldShowEllipsisForSkippedPathParts = true;
|
||||
const file = createMockFile('a/b/note.md');
|
||||
const container = callGenerateBacklinkTitle(file);
|
||||
const parentPathSpan = container.shadowRoot?.querySelector('[part="parent-path"]');
|
||||
expect(parentPathSpan?.textContent).toBe('a/b/');
|
||||
});
|
||||
|
||||
it('should reverse path parts when shouldReversePathParts is true', () => {
|
||||
settings.shouldReversePathParts = true;
|
||||
settings.shouldDisplayParentPathOnSeparateLine = false;
|
||||
const file = createMockFile('a/b/note.md');
|
||||
const container = callGenerateBacklinkTitle(file);
|
||||
const parentPathSpan = container.shadowRoot?.querySelector('[part="parent-path"]');
|
||||
expect(parentPathSpan?.textContent).toBe(' \u2190 b \u2190 a');
|
||||
});
|
||||
|
||||
it('should display parent path on separate line without separator', () => {
|
||||
settings.shouldDisplayParentPathOnSeparateLine = true;
|
||||
settings.shouldReversePathParts = false;
|
||||
const file = createMockFile('folder/note.md');
|
||||
const container = callGenerateBacklinkTitle(file);
|
||||
const parentPathSpan = container.shadowRoot?.querySelector('[part="parent-path"]');
|
||||
expect(parentPathSpan?.textContent).toBe('folder');
|
||||
});
|
||||
|
||||
it('should set dataset attributes for highlighting', () => {
|
||||
settings.shouldHighlightFileName = true;
|
||||
settings.shouldDisplayParentPathOnSeparateLine = false;
|
||||
const file = createMockFile('note.md');
|
||||
const container = callGenerateBacklinkTitle(file);
|
||||
expect(container.dataset['shouldHighlightFileName']).toBe('true');
|
||||
expect(container.dataset['shouldDisplayParentPathOnSeparateLine']).toBe('false');
|
||||
});
|
||||
|
||||
it('should include full-path span with file path', () => {
|
||||
const file = createMockFile('folder/note.md');
|
||||
const container = callGenerateBacklinkTitle(file);
|
||||
const fullPathSpan = container.querySelector('.full-path');
|
||||
expect(fullPathSpan?.textContent).toBe('folder/note.md');
|
||||
});
|
||||
|
||||
it('should prepend parent path when not reversed and not on separate line', () => {
|
||||
settings.shouldReversePathParts = false;
|
||||
settings.shouldDisplayParentPathOnSeparateLine = false;
|
||||
const file = createMockFile('folder/note.md');
|
||||
const container = callGenerateBacklinkTitle(file);
|
||||
const firstChild = container.shadowRoot?.firstElementChild;
|
||||
expect(firstChild?.getAttribute('part')).toBe('parent-path');
|
||||
});
|
||||
|
||||
it('should append parent path when reversed and not on separate line', () => {
|
||||
settings.shouldReversePathParts = true;
|
||||
settings.shouldDisplayParentPathOnSeparateLine = false;
|
||||
const file = createMockFile('folder/note.md');
|
||||
const container = callGenerateBacklinkTitle(file);
|
||||
const lastChild = container.shadowRoot?.lastElementChild;
|
||||
expect(lastChild?.getAttribute('part')).toBe('parent-path');
|
||||
});
|
||||
|
||||
it('should not prepend when displaying on separate line', () => {
|
||||
settings.shouldReversePathParts = false;
|
||||
settings.shouldDisplayParentPathOnSeparateLine = true;
|
||||
const file = createMockFile('folder/note.md');
|
||||
const container = callGenerateBacklinkTitle(file);
|
||||
const lastChild = container.shadowRoot?.lastElementChild;
|
||||
expect(lastChild?.getAttribute('part')).toBe('parent-path');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBacklinkView', () => {
|
||||
it('should return null when no backlink leaf exists', async () => {
|
||||
const { plugin } = createTestPlugin();
|
||||
|
||||
const getBacklinkView = privateMethod(plugin, 'getBacklinkView');
|
||||
const view = await getBacklinkView();
|
||||
expect(view).toBeNull();
|
||||
});
|
||||
|
||||
it('should return the view when a backlink leaf exists', async () => {
|
||||
const { plugin } = createTestPlugin();
|
||||
|
||||
const mockView = mockProxy<BacklinkView>({});
|
||||
const mockLeaf = mockProxy<WorkspaceLeaf>({
|
||||
loadIfDeferred: vi.fn().mockResolvedValue(undefined),
|
||||
view: mockView
|
||||
});
|
||||
|
||||
Reflect.set(plugin.app.workspace, 'getLeavesOfType', vi.fn().mockReturnValue([mockLeaf]));
|
||||
|
||||
const getBacklinkView = privateMethod(plugin, 'getBacklinkView');
|
||||
const view = await getBacklinkView();
|
||||
|
||||
expect(mockLeaf.loadIfDeferred).toHaveBeenCalled();
|
||||
expect(view).toBe(mockView);
|
||||
});
|
||||
});
|
||||
|
||||
describe('refreshBacklinkPanels', () => {
|
||||
it('should call reloadBacklinksView', async () => {
|
||||
const { plugin } = createTestPlugin();
|
||||
|
||||
const reloadBacklinksView = vi.fn().mockResolvedValue(undefined);
|
||||
Reflect.set(plugin, 'reloadBacklinksView', reloadBacklinksView);
|
||||
|
||||
const refreshBacklinkPanels = privateMethod(plugin, 'refreshBacklinkPanels');
|
||||
await refreshBacklinkPanels();
|
||||
|
||||
expect(reloadBacklinksView).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('reloadBacklinksView', () => {
|
||||
it('should do nothing when no backlink view exists', async () => {
|
||||
const { plugin } = createTestPlugin();
|
||||
|
||||
const getBacklinkView = vi.fn().mockResolvedValue(null);
|
||||
Reflect.set(plugin, 'getBacklinkView', getBacklinkView);
|
||||
|
||||
const reloadBacklinksView = privateMethod(plugin, 'reloadBacklinksView');
|
||||
await reloadBacklinksView();
|
||||
|
||||
expect(getBacklinkView).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not recompute when backlink view has no file', async () => {
|
||||
const { plugin } = createTestPlugin();
|
||||
|
||||
const recomputeBacklink = vi.fn();
|
||||
const backlinkView = mockProxy<BacklinkView>({
|
||||
backlink: mockProxy({ recomputeBacklink }),
|
||||
file: null
|
||||
});
|
||||
|
||||
Reflect.set(plugin, 'getBacklinkView', vi.fn().mockResolvedValue(backlinkView));
|
||||
|
||||
const reloadBacklinksView = privateMethod(plugin, 'reloadBacklinksView');
|
||||
await reloadBacklinksView();
|
||||
|
||||
expect(recomputeBacklink).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should recompute when backlink view has a file', async () => {
|
||||
const { plugin } = createTestPlugin();
|
||||
|
||||
const mockFile = createMockFile('note.md');
|
||||
const recomputeBacklink = vi.fn();
|
||||
const backlinkView = mockProxy<BacklinkView>({
|
||||
backlink: mockProxy({ recomputeBacklink }),
|
||||
file: mockFile
|
||||
});
|
||||
|
||||
Reflect.set(plugin, 'getBacklinkView', vi.fn().mockResolvedValue(backlinkView));
|
||||
|
||||
const reloadBacklinksView = privateMethod(plugin, 'reloadBacklinksView');
|
||||
await reloadBacklinksView();
|
||||
|
||||
expect(recomputeBacklink).toHaveBeenCalledWith(mockFile);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onBacklinksCorePluginEnable', () => {
|
||||
it('should invoke patchBacklinksPane asynchronously', () => {
|
||||
const { plugin } = createTestPlugin();
|
||||
|
||||
const patchBacklinksPane = vi.fn().mockResolvedValue(undefined);
|
||||
Reflect.set(plugin, 'patchBacklinksPane', patchBacklinksPane);
|
||||
|
||||
const onBacklinksCorePluginEnable = privateMethod(plugin, 'onBacklinksCorePluginEnable');
|
||||
onBacklinksCorePluginEnable();
|
||||
|
||||
expect(patchBacklinksPane).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('patchBacklinksPane', () => {
|
||||
it('should return early when no backlink view exists', async () => {
|
||||
const { plugin } = createTestPlugin();
|
||||
|
||||
const getBacklinkView = vi.fn().mockResolvedValue(null);
|
||||
Reflect.set(plugin, 'getBacklinkView', getBacklinkView);
|
||||
|
||||
const patchBacklinksPane = privateMethod(plugin, 'patchBacklinksPane');
|
||||
await patchBacklinksPane();
|
||||
|
||||
expect(getBacklinkView).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should patch addResult on backlinkDom when view exists', async () => {
|
||||
const { plugin } = createTestPlugin();
|
||||
markPluginLoaded(plugin);
|
||||
|
||||
const originalAddResult = vi.fn().mockImplementation(() => createMockResultDomItem(true));
|
||||
const backlinkDomProto = { addResult: originalAddResult };
|
||||
const backlinkView = mockProxy<BacklinkView>({
|
||||
backlink: mockProxy({
|
||||
backlinkDom: Object.create(backlinkDomProto) as object
|
||||
})
|
||||
});
|
||||
|
||||
Reflect.set(plugin, 'getBacklinkView', vi.fn().mockResolvedValue(backlinkView));
|
||||
|
||||
const patchBacklinksPane = privateMethod(plugin, 'patchBacklinksPane');
|
||||
await patchBacklinksPane();
|
||||
|
||||
// The patchedAddResult is now on the prototype; call it to exercise the callback
|
||||
const file = createMockFile('folder/note.md');
|
||||
const result = mockProxy<ResultDomResult>({});
|
||||
const mockResultDom = Object.create(backlinkDomProto) as object;
|
||||
const patchedAddResult = backlinkDomProto.addResult as (
|
||||
file: TFile,
|
||||
result: ResultDomResult,
|
||||
content: string,
|
||||
shouldShowTitle?: boolean
|
||||
) => ResultDomItem;
|
||||
const item = patchedAddResult.call(mockResultDom, file, result, 'content', true);
|
||||
|
||||
expect(item).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('refreshBacklinkPanels with markdown leaves', () => {
|
||||
it('should skip leaves that are not MarkdownView instances', async () => {
|
||||
const { plugin } = createTestPlugin();
|
||||
|
||||
Reflect.set(plugin, 'reloadBacklinksView', vi.fn().mockResolvedValue(undefined));
|
||||
|
||||
const nonMarkdownLeaf = mockProxy<WorkspaceLeaf>({
|
||||
view: mockProxy({})
|
||||
});
|
||||
|
||||
Reflect.set(
|
||||
plugin.app.workspace,
|
||||
'getLeavesOfType',
|
||||
vi.fn().mockImplementation((type: string) => {
|
||||
if (type === 'markdown') {
|
||||
return [nonMarkdownLeaf];
|
||||
}
|
||||
return [];
|
||||
})
|
||||
);
|
||||
|
||||
const refreshBacklinkPanels = privateMethod(plugin, 'refreshBacklinkPanels');
|
||||
await refreshBacklinkPanels();
|
||||
});
|
||||
|
||||
it('should skip MarkdownView leaves without backlinks', async () => {
|
||||
const { app, plugin } = createTestPlugin();
|
||||
|
||||
Reflect.set(plugin, 'reloadBacklinksView', vi.fn().mockResolvedValue(undefined));
|
||||
|
||||
const leaf = WorkspaceLeaf.create2__(app);
|
||||
const mdView = MarkdownView.create2__(leaf);
|
||||
Reflect.set(bypassStrictProxy(mdView), 'backlinks', undefined);
|
||||
Reflect.set(bypassStrictProxy(leaf), 'view', mdView);
|
||||
|
||||
Reflect.set(
|
||||
plugin.app.workspace,
|
||||
'getLeavesOfType',
|
||||
vi.fn().mockImplementation((type: string) => {
|
||||
if (type === 'markdown') {
|
||||
return [leaf.asOriginalType__()];
|
||||
}
|
||||
return [];
|
||||
})
|
||||
);
|
||||
|
||||
const refreshBacklinkPanels = privateMethod(plugin, 'refreshBacklinkPanels');
|
||||
await refreshBacklinkPanels();
|
||||
});
|
||||
|
||||
it('should recompute backlinks for MarkdownView leaves with backlinks', async () => {
|
||||
const { app, plugin } = createTestPlugin();
|
||||
|
||||
Reflect.set(plugin, 'reloadBacklinksView', vi.fn().mockResolvedValue(undefined));
|
||||
|
||||
const leaf = WorkspaceLeaf.create2__(app);
|
||||
const mdView = MarkdownView.create2__(leaf);
|
||||
const mockFile = createMockFile('test2.md');
|
||||
const recomputeBacklink = vi.fn();
|
||||
Reflect.set(bypassStrictProxy(mdView), 'backlinks', { file: mockFile, recomputeBacklink });
|
||||
Reflect.set(bypassStrictProxy(leaf), 'view', mdView);
|
||||
|
||||
Reflect.set(
|
||||
plugin.app.workspace,
|
||||
'getLeavesOfType',
|
||||
vi.fn().mockImplementation((type: string) => {
|
||||
if (type === 'markdown') {
|
||||
return [leaf.asOriginalType__()];
|
||||
}
|
||||
return [];
|
||||
})
|
||||
);
|
||||
|
||||
const refreshBacklinkPanels = privateMethod(plugin, 'refreshBacklinkPanels');
|
||||
await refreshBacklinkPanels();
|
||||
|
||||
expect(recomputeBacklink).toHaveBeenCalledWith(mockFile);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,11 +4,6 @@
|
|||
* Main plugin class for the Backlink Full Path plugin.
|
||||
*/
|
||||
|
||||
import type {
|
||||
App,
|
||||
PluginManifest,
|
||||
TFile
|
||||
} from 'obsidian';
|
||||
import type {
|
||||
BacklinkPlugin,
|
||||
BacklinkView,
|
||||
|
|
@ -16,21 +11,27 @@ import type {
|
|||
ResultDomItem,
|
||||
ResultDomResult
|
||||
} from '@obsidian-typings/obsidian-public-latest';
|
||||
import type {
|
||||
App,
|
||||
PluginManifest,
|
||||
TFile
|
||||
} from 'obsidian';
|
||||
|
||||
import {
|
||||
InternalPluginName,
|
||||
ViewType
|
||||
} from '@obsidian-typings/obsidian-public-latest/implementations';
|
||||
import {
|
||||
MarkdownView,
|
||||
setTooltip
|
||||
} from 'obsidian';
|
||||
import { invokeAsyncSafely } from 'obsidian-dev-utils/async';
|
||||
import { getPrototypeOf } from 'obsidian-dev-utils/object-utils';
|
||||
import { registerPatch } from 'obsidian-dev-utils/obsidian/monkey-around';
|
||||
import { MonkeyAroundComponent } from 'obsidian-dev-utils/obsidian/components/monkey-around-component';
|
||||
import { PluginSettingsTabComponent } from 'obsidian-dev-utils/obsidian/components/plugin-settings-tab-component';
|
||||
import { PluginBase } from 'obsidian-dev-utils/obsidian/plugin/plugin';
|
||||
import { PluginDataHandler } from 'obsidian-dev-utils/obsidian/data-handler';
|
||||
import {
|
||||
InternalPluginName,
|
||||
ViewType
|
||||
} from '@obsidian-typings/obsidian-public-latest/implementations';
|
||||
import { PluginBase } from 'obsidian-dev-utils/obsidian/plugin/plugin';
|
||||
import { PluginEventSourceImpl } from 'obsidian-dev-utils/obsidian/plugin/plugin-event-source';
|
||||
|
||||
import { PluginSettingsComponent } from './plugin-settings-component.ts';
|
||||
import { PluginSettingsTab } from './plugin-settings-tab.ts';
|
||||
|
|
@ -55,39 +56,45 @@ export class Plugin extends PluginBase {
|
|||
public constructor(app: App, manifest: PluginManifest) {
|
||||
super(app, manifest);
|
||||
|
||||
this.pluginSettingsComponent = this.addChild(new PluginSettingsComponent(new PluginDataHandler(this)));
|
||||
|
||||
this.addChild(new PluginSettingsTabComponent({
|
||||
plugin: this,
|
||||
pluginSettingsTab: new PluginSettingsTab({
|
||||
plugin: this,
|
||||
settingsComponent: this.pluginSettingsComponent
|
||||
this.pluginSettingsComponent = this.addChild(
|
||||
new PluginSettingsComponent({
|
||||
dataHandler: new PluginDataHandler(this),
|
||||
pluginEventSource: new PluginEventSourceImpl(this)
|
||||
})
|
||||
}));
|
||||
);
|
||||
|
||||
this.addChild(
|
||||
new PluginSettingsTabComponent({
|
||||
plugin: this,
|
||||
pluginSettingsTab: new PluginSettingsTab({
|
||||
plugin: this,
|
||||
pluginSettingsComponent: this.pluginSettingsComponent
|
||||
})
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after pre-loaded components are initialized.
|
||||
*/
|
||||
protected override async onloadImpl(): Promise<void> {
|
||||
await super.onloadImpl();
|
||||
|
||||
this.pluginSettingsComponent.on('saveSettings', () => {
|
||||
invokeAsyncSafely(() => this.refreshBacklinkPanels());
|
||||
public override onload(): void {
|
||||
this.pluginSettingsComponent.on('saveSettings', async () => {
|
||||
await this.refreshBacklinkPanels();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the workspace layout is ready.
|
||||
*/
|
||||
protected override async onLayoutReady(): Promise<void> {
|
||||
protected async onLayoutReady(): Promise<void> {
|
||||
const backlinksCorePlugin = this.app.internalPlugins.getPluginById(InternalPluginName.Backlink);
|
||||
if (!backlinksCorePlugin) {
|
||||
return;
|
||||
}
|
||||
|
||||
const that = this;
|
||||
registerPatch(this, getPrototypeOf(backlinksCorePlugin.instance), {
|
||||
const patch = this.addChild(new MonkeyAroundComponent());
|
||||
patch.registerPatch(getPrototypeOf(backlinksCorePlugin.instance), {
|
||||
onUserEnable: (next: () => void) => {
|
||||
return function onUserEnablePatched(this: BacklinkPlugin): void {
|
||||
next.call(this);
|
||||
|
|
@ -204,7 +211,8 @@ export class Plugin extends PluginBase {
|
|||
}
|
||||
|
||||
const that = this;
|
||||
registerPatch(this, getPrototypeOf(backlinkView.backlink.backlinkDom), {
|
||||
const patch = this.addChild(new MonkeyAroundComponent());
|
||||
patch.registerPatch(getPrototypeOf(backlinkView.backlink.backlinkDom), {
|
||||
addResult: (next: AddResultFn): AddResultFn => {
|
||||
return function addResultPatched(this: ResultDom, file: TFile, result: ResultDomResult, content: string, shouldShowTitle?: boolean): ResultDomItem {
|
||||
return that.addResult(next, this, file, result, content, shouldShowTitle);
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
"inlineSources": true,
|
||||
"lib": [
|
||||
"DOM",
|
||||
"DOM.Iterable",
|
||||
"DOM.Iterable",
|
||||
"ES2024"
|
||||
],
|
||||
"libReplacement": true,
|
||||
|
|
|
|||
Loading…
Reference in a new issue