mirror of
https://github.com/mnaoumov/obsidian-backlink-full-path.git
synced 2026-07-22 05:41:39 +00:00
test: restore component tests to real dev-utils base classes
Rewrite backlink-full-path-component.test.ts to drive the real LayoutReadyComponent lifecycle and add real-MonkeyAroundComponent tests for the extracted patch components, replacing the stale, mock-based suite left behind by the monkey-around refactor. Restores 100% coverage.
This commit is contained in:
parent
ee1b609a54
commit
b3f3c329bf
3 changed files with 530 additions and 462 deletions
|
|
@ -1,19 +1,17 @@
|
|||
import type {
|
||||
BacklinkView,
|
||||
ResultDomItem,
|
||||
ResultDomResult
|
||||
} from '@obsidian-typings/obsidian-public-latest';
|
||||
import type { BacklinkView } from '@obsidian-typings/obsidian-public-latest';
|
||||
import type {
|
||||
App,
|
||||
TFile,
|
||||
WorkspaceLeaf
|
||||
} from 'obsidian';
|
||||
import type { Mock } from 'vitest';
|
||||
|
||||
import { ViewType } from '@obsidian-typings/obsidian-public-latest/implementations';
|
||||
import { MarkdownView } from 'obsidian';
|
||||
import { castTo } from 'obsidian-dev-utils/object-utils';
|
||||
import { strictProxy } from 'obsidian-dev-utils/strict-proxy';
|
||||
import {
|
||||
afterEach,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
|
|
@ -24,17 +22,11 @@ import {
|
|||
import type { PluginSettingsComponent } from './plugin-settings-component.ts';
|
||||
|
||||
import { BacklinkFullPathComponent } from './backlink-full-path-component.ts';
|
||||
import { ResultDomAddResultPatchComponent } from './patches/result-dom-add-result-patch-component.ts';
|
||||
import { PluginSettings } from './plugin-settings.ts';
|
||||
|
||||
interface BacklinkDom {
|
||||
addResult(file: TFile, result: ResultDomResult, content: string, shouldShowTitle: boolean): ResultDomItem;
|
||||
}
|
||||
|
||||
interface ComponentInternals {
|
||||
generateBacklinkTitle(file: TFile): HTMLDivElement;
|
||||
getBacklinkView(): Promise<BacklinkView | null>;
|
||||
onBacklinksCorePluginEnable(): void;
|
||||
onLayoutReady(): Promise<void>;
|
||||
patchBacklinksPane(): Promise<void>;
|
||||
refreshBacklinkPanels(): Promise<void>;
|
||||
reloadBacklinksView(): Promise<void>;
|
||||
|
|
@ -45,86 +37,232 @@ interface CorePlugin {
|
|||
instance: object;
|
||||
}
|
||||
|
||||
interface PatchableTarget {
|
||||
addResult?(...args: never[]): unknown;
|
||||
onUserEnable?(...args: never[]): unknown;
|
||||
}
|
||||
|
||||
interface PatchHandlerParams {
|
||||
fallback(): unknown;
|
||||
readonly originalArgs: readonly unknown[];
|
||||
}
|
||||
|
||||
interface RegisteredCallbacksHolder {
|
||||
registeredCallbacks: (() => void)[];
|
||||
}
|
||||
|
||||
interface RegisterMethodPatchParams {
|
||||
readonly methodName: 'addResult' | 'onUserEnable';
|
||||
readonly obj: PatchableTarget;
|
||||
patchHandler(params: PatchHandlerParams): unknown;
|
||||
interface PushBacklinkViewParams {
|
||||
readonly file: null | TFile;
|
||||
readonly recomputeBacklink: Mock<(backlinkFile: null | TFile) => void>;
|
||||
}
|
||||
|
||||
interface TestContext {
|
||||
app: App;
|
||||
backlinkLeaves: WorkspaceLeaf[];
|
||||
component: BacklinkFullPathComponent;
|
||||
getPluginById: ReturnType<typeof vi.fn>;
|
||||
markdownLeaves: WorkspaceLeaf[];
|
||||
on: ReturnType<typeof vi.fn>;
|
||||
registeredCallbacks: (() => void)[];
|
||||
settings: PluginSettings;
|
||||
}
|
||||
|
||||
vi.mock('obsidian-dev-utils/async', () => ({
|
||||
invokeAsyncSafely: vi.fn((fn: () => Promise<void>) => fn())
|
||||
}));
|
||||
describe('BacklinkFullPathComponent', () => {
|
||||
let context: TestContext;
|
||||
|
||||
vi.mock('obsidian-dev-utils/obsidian/components/layout-ready-component', () => ({
|
||||
LayoutReadyComponent: class MockLayoutReadyComponent {
|
||||
public app: App;
|
||||
public readonly registeredCallbacks: (() => void)[] = [];
|
||||
beforeEach(() => {
|
||||
context = createTestContext();
|
||||
});
|
||||
|
||||
public constructor(app: App) {
|
||||
this.app = app;
|
||||
}
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
public addChild(child: unknown): unknown {
|
||||
return child;
|
||||
}
|
||||
describe('onLayoutReady', () => {
|
||||
it('should register a saveSettings handler that refreshes panels', async () => {
|
||||
context.getPluginById.mockReturnValue(undefined);
|
||||
const refreshSpy = vi.spyOn(internals(context.component), 'refreshBacklinkPanels').mockResolvedValue(undefined);
|
||||
|
||||
public register(callback: () => void): void {
|
||||
this.registeredCallbacks.push(callback);
|
||||
}
|
||||
await triggerLayoutReady();
|
||||
|
||||
expect(context.on).toHaveBeenCalledWith('saveSettings', expect.any(Function));
|
||||
const handler = castTo<() => Promise<void>>(context.on.mock.calls[0]?.[1]);
|
||||
await handler();
|
||||
expect(refreshSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not patch the pane when the backlinks core plugin is not found', async () => {
|
||||
context.getPluginById.mockReturnValue(undefined);
|
||||
const patchSpy = vi.spyOn(internals(context.component), 'patchBacklinksPane').mockResolvedValue(undefined);
|
||||
|
||||
await triggerLayoutReady();
|
||||
|
||||
expect(patchSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should add the onUserEnable patch child when the core plugin is found', async () => {
|
||||
context.getPluginById.mockReturnValue(createCorePlugin(false));
|
||||
vi.spyOn(internals(context.component), 'patchBacklinksPane').mockResolvedValue(undefined);
|
||||
const addChildSpy = vi.spyOn(context.component, 'addChild');
|
||||
|
||||
await triggerLayoutReady();
|
||||
|
||||
expect(addChildSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should patch the pane and refresh panels when the plugin is enabled', async () => {
|
||||
context.getPluginById.mockReturnValue(createCorePlugin(true));
|
||||
const patchSpy = vi.spyOn(internals(context.component), 'patchBacklinksPane').mockResolvedValue(undefined);
|
||||
const refreshSpy = vi.spyOn(internals(context.component), 'refreshBacklinkPanels').mockResolvedValue(undefined);
|
||||
|
||||
await triggerLayoutReady();
|
||||
|
||||
expect(patchSpy).toHaveBeenCalled();
|
||||
expect(refreshSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not patch the pane when the plugin is disabled', async () => {
|
||||
context.getPluginById.mockReturnValue(createCorePlugin(false));
|
||||
const patchSpy = vi.spyOn(internals(context.component), 'patchBacklinksPane').mockResolvedValue(undefined);
|
||||
|
||||
await triggerLayoutReady();
|
||||
|
||||
expect(patchSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should register an unload callback that refreshes panels', async () => {
|
||||
context.getPluginById.mockReturnValue(createCorePlugin(false));
|
||||
const refreshSpy = vi.spyOn(internals(context.component), 'refreshBacklinkPanels').mockResolvedValue(undefined);
|
||||
const registerSpy = vi.spyOn(context.component, 'register');
|
||||
|
||||
await triggerLayoutReady();
|
||||
|
||||
const unloadCallback = registerSpy.mock.calls.at(-1)?.[0];
|
||||
unloadCallback?.();
|
||||
|
||||
expect(refreshSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('onBacklinksCorePluginEnable', () => {
|
||||
it('should patch the backlinks pane', () => {
|
||||
const patchSpy = vi.spyOn(internals(context.component), 'patchBacklinksPane').mockResolvedValue(undefined);
|
||||
|
||||
context.component.onBacklinksCorePluginEnable();
|
||||
|
||||
expect(patchSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBacklinkView', () => {
|
||||
it('should return null when no backlink leaf exists', async () => {
|
||||
const view = await internals(context.component).getBacklinkView();
|
||||
expect(view).toBeNull();
|
||||
});
|
||||
|
||||
it('should load the deferred leaf and return its view', async () => {
|
||||
const backlinkView = strictProxy<BacklinkView>({});
|
||||
const loadIfDeferred = vi.fn().mockResolvedValue(undefined);
|
||||
context.backlinkLeaves.push(castTo<WorkspaceLeaf>({
|
||||
loadIfDeferred,
|
||||
view: backlinkView
|
||||
}));
|
||||
|
||||
const view = await internals(context.component).getBacklinkView();
|
||||
|
||||
expect(loadIfDeferred).toHaveBeenCalled();
|
||||
expect(view).toBe(backlinkView);
|
||||
});
|
||||
});
|
||||
|
||||
describe('patchBacklinksPane', () => {
|
||||
it('should do nothing when no backlink view exists', async () => {
|
||||
const addChildSpy = vi.spyOn(context.component, 'addChild');
|
||||
|
||||
await internals(context.component).patchBacklinksPane();
|
||||
|
||||
expect(addChildSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should add a result-dom patch child when a backlink view exists', async () => {
|
||||
const backlinkDomProto = { addResult: vi.fn() };
|
||||
const backlinkView = strictProxy<BacklinkView>({
|
||||
backlink: strictProxy({
|
||||
backlinkDom: Object.create(backlinkDomProto)
|
||||
})
|
||||
});
|
||||
context.backlinkLeaves.push(castTo<WorkspaceLeaf>({
|
||||
loadIfDeferred: vi.fn().mockResolvedValue(undefined),
|
||||
view: backlinkView
|
||||
}));
|
||||
const addChildSpy = vi.spyOn(context.component, 'addChild');
|
||||
|
||||
await internals(context.component).patchBacklinksPane();
|
||||
|
||||
expect(addChildSpy).toHaveBeenCalledWith(expect.any(ResultDomAddResultPatchComponent));
|
||||
});
|
||||
});
|
||||
|
||||
describe('reloadBacklinksView', () => {
|
||||
it('should do nothing when no backlink view exists', async () => {
|
||||
await expect(internals(context.component).reloadBacklinksView()).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not recompute when the backlink view has no file', async () => {
|
||||
const recomputeBacklink = vi.fn<(backlinkFile: null | TFile) => void>();
|
||||
pushBacklinkView({ file: null, recomputeBacklink });
|
||||
|
||||
await internals(context.component).reloadBacklinksView();
|
||||
|
||||
expect(recomputeBacklink).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should recompute when the backlink view has a file', async () => {
|
||||
const file = createMockFile('note.md');
|
||||
const recomputeBacklink = vi.fn<(backlinkFile: null | TFile) => void>();
|
||||
pushBacklinkView({ file, recomputeBacklink });
|
||||
|
||||
await internals(context.component).reloadBacklinksView();
|
||||
|
||||
expect(recomputeBacklink).toHaveBeenCalledWith(file);
|
||||
});
|
||||
});
|
||||
|
||||
describe('refreshBacklinkPanels', () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(internals(context.component), 'reloadBacklinksView').mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it('should skip leaves that are not MarkdownView instances', async () => {
|
||||
context.markdownLeaves.push(castTo<WorkspaceLeaf>({ view: {} }));
|
||||
|
||||
await expect(internals(context.component).refreshBacklinkPanels()).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('should skip MarkdownView leaves without backlinks', async () => {
|
||||
context.markdownLeaves.push(createMarkdownLeaf(undefined));
|
||||
|
||||
await expect(internals(context.component).refreshBacklinkPanels()).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('should recompute backlinks for MarkdownView leaves with backlinks', async () => {
|
||||
const file = createMockFile('note.md');
|
||||
const recomputeBacklink = vi.fn();
|
||||
context.markdownLeaves.push(createMarkdownLeaf({ file, recomputeBacklink }));
|
||||
|
||||
await internals(context.component).refreshBacklinkPanels();
|
||||
|
||||
expect(recomputeBacklink).toHaveBeenCalledWith(file);
|
||||
});
|
||||
});
|
||||
|
||||
function pushBacklinkView(backlink: PushBacklinkViewParams): void {
|
||||
const backlinkView = strictProxy<BacklinkView>({
|
||||
backlink: strictProxy({ recomputeBacklink: backlink.recomputeBacklink }),
|
||||
file: backlink.file
|
||||
});
|
||||
context.backlinkLeaves.push(castTo<WorkspaceLeaf>({
|
||||
loadIfDeferred: vi.fn().mockResolvedValue(undefined),
|
||||
view: backlinkView
|
||||
}));
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock('obsidian-dev-utils/obsidian/components/monkey-around-component', () => ({
|
||||
MonkeyAroundComponent: class MockMonkeyAroundComponent {
|
||||
public registerMethodPatch(params: RegisterMethodPatchParams): void {
|
||||
const {
|
||||
methodName,
|
||||
obj,
|
||||
patchHandler
|
||||
} = params;
|
||||
const originalMethod = obj[methodName];
|
||||
obj[methodName] = function patched(this: unknown, ...originalArgs: never[]): unknown {
|
||||
return patchHandler({
|
||||
fallback: () => originalMethod?.apply(this, originalArgs),
|
||||
originalArgs
|
||||
});
|
||||
};
|
||||
}
|
||||
async function triggerLayoutReady(): Promise<void> {
|
||||
vi.useFakeTimers();
|
||||
context.component.load();
|
||||
await vi.runAllTimersAsync();
|
||||
vi.useRealTimers();
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
function asInternals(component: BacklinkFullPathComponent): ComponentInternals {
|
||||
return castTo<ComponentInternals>(component);
|
||||
}
|
||||
|
||||
function createCorePlugin(enabled: boolean, instanceProto: object = {}): CorePlugin {
|
||||
function createCorePlugin(enabled: boolean): CorePlugin {
|
||||
return {
|
||||
enabled,
|
||||
instance: Object.create(instanceProto)
|
||||
instance: Object.create({ onUserEnable: vi.fn() })
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -135,32 +273,11 @@ function createMarkdownLeaf(backlinks: unknown): WorkspaceLeaf {
|
|||
}
|
||||
|
||||
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 strictProxy<TFile>({
|
||||
basename,
|
||||
extension,
|
||||
name,
|
||||
path
|
||||
});
|
||||
}
|
||||
|
||||
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 strictProxy<ResultDomItem>({ el });
|
||||
return strictProxy<TFile>({ path });
|
||||
}
|
||||
|
||||
function createTestContext(): TestContext {
|
||||
const settings = new PluginSettings();
|
||||
const backlinkLeaves: WorkspaceLeaf[] = [];
|
||||
const markdownLeaves: WorkspaceLeaf[] = [];
|
||||
const getPluginById = vi.fn();
|
||||
const on = vi.fn();
|
||||
|
|
@ -171,17 +288,23 @@ function createTestContext(): TestContext {
|
|||
},
|
||||
workspace: {
|
||||
getLeavesOfType: vi.fn().mockImplementation((type: string) => {
|
||||
if (type === ViewType.Backlink) {
|
||||
return backlinkLeaves;
|
||||
}
|
||||
if (type === ViewType.Markdown) {
|
||||
return markdownLeaves;
|
||||
}
|
||||
return [];
|
||||
}),
|
||||
onLayoutReady: vi.fn().mockImplementation((callback: () => void) => {
|
||||
callback();
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
const pluginSettingsComponent = strictProxy<PluginSettingsComponent>({
|
||||
on,
|
||||
settings
|
||||
settings: new PluginSettings()
|
||||
});
|
||||
|
||||
const component = new BacklinkFullPathComponent({
|
||||
|
|
@ -189,377 +312,16 @@ function createTestContext(): TestContext {
|
|||
pluginSettingsComponent
|
||||
});
|
||||
|
||||
const registeredCallbacks = castTo<RegisteredCallbacksHolder>(component).registeredCallbacks;
|
||||
|
||||
return {
|
||||
app,
|
||||
backlinkLeaves,
|
||||
component,
|
||||
getPluginById,
|
||||
markdownLeaves,
|
||||
on,
|
||||
registeredCallbacks,
|
||||
settings
|
||||
on
|
||||
};
|
||||
}
|
||||
|
||||
describe('BacklinkFullPathComponent', () => {
|
||||
let context: TestContext;
|
||||
|
||||
beforeEach(() => {
|
||||
context = createTestContext();
|
||||
});
|
||||
|
||||
describe('onLayoutReady', () => {
|
||||
it('should register a saveSettings handler that refreshes panels', async () => {
|
||||
context.getPluginById.mockReturnValue(undefined);
|
||||
const refreshSpy = vi.spyOn(asInternals(context.component), 'refreshBacklinkPanels').mockResolvedValue(undefined);
|
||||
|
||||
await asInternals(context.component).onLayoutReady();
|
||||
|
||||
expect(context.on).toHaveBeenCalledWith('saveSettings', expect.any(Function));
|
||||
const handler = context.on.mock.calls[0]?.[1] as () => Promise<void>;
|
||||
await handler();
|
||||
expect(refreshSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return early when backlinks core plugin is not found', async () => {
|
||||
context.getPluginById.mockReturnValue(undefined);
|
||||
const patchSpy = vi.spyOn(asInternals(context.component), 'patchBacklinksPane').mockResolvedValue(undefined);
|
||||
|
||||
await asInternals(context.component).onLayoutReady();
|
||||
|
||||
expect(patchSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should patch onUserEnable to call onBacklinksCorePluginEnable', async () => {
|
||||
const originalOnUserEnable = vi.fn();
|
||||
const instanceProto = { onUserEnable: originalOnUserEnable };
|
||||
const corePlugin = createCorePlugin(false, instanceProto);
|
||||
context.getPluginById.mockReturnValue(corePlugin);
|
||||
|
||||
const enableSpy = vi.spyOn(asInternals(context.component), 'onBacklinksCorePluginEnable').mockReturnValue(undefined);
|
||||
|
||||
await asInternals(context.component).onLayoutReady();
|
||||
|
||||
instanceProto.onUserEnable.call(corePlugin.instance);
|
||||
|
||||
expect(originalOnUserEnable).toHaveBeenCalled();
|
||||
expect(enableSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should patch pane and refresh panels when plugin is enabled', async () => {
|
||||
context.getPluginById.mockReturnValue(createCorePlugin(true));
|
||||
const patchSpy = vi.spyOn(asInternals(context.component), 'patchBacklinksPane').mockResolvedValue(undefined);
|
||||
const refreshSpy = vi.spyOn(asInternals(context.component), 'refreshBacklinkPanels').mockResolvedValue(undefined);
|
||||
|
||||
await asInternals(context.component).onLayoutReady();
|
||||
|
||||
expect(patchSpy).toHaveBeenCalled();
|
||||
expect(refreshSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not patch pane when plugin is disabled', async () => {
|
||||
context.getPluginById.mockReturnValue(createCorePlugin(false));
|
||||
const patchSpy = vi.spyOn(asInternals(context.component), 'patchBacklinksPane').mockResolvedValue(undefined);
|
||||
|
||||
await asInternals(context.component).onLayoutReady();
|
||||
|
||||
expect(patchSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should register an unload callback that refreshes panels', async () => {
|
||||
context.getPluginById.mockReturnValue(createCorePlugin(false));
|
||||
const refreshSpy = vi.spyOn(asInternals(context.component), 'refreshBacklinkPanels').mockResolvedValue(undefined);
|
||||
|
||||
await asInternals(context.component).onLayoutReady();
|
||||
|
||||
expect(context.registeredCallbacks.length).toBeGreaterThan(0);
|
||||
const callback = context.registeredCallbacks[0];
|
||||
callback?.();
|
||||
|
||||
expect(refreshSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateBacklinkTitle', () => {
|
||||
function callGenerateBacklinkTitle(file: TFile): HTMLDivElement {
|
||||
return asInternals(context.component).generateBacklinkTitle.call(context.component, file);
|
||||
}
|
||||
|
||||
it('should include extension when shouldIncludeExtension is true', () => {
|
||||
context.settings.shouldIncludeExtension = true;
|
||||
const container = callGenerateBacklinkTitle(createMockFile('folder/note.md'));
|
||||
const fileNameSpan = container.shadowRoot?.querySelector('[part="file-name"]');
|
||||
expect(fileNameSpan?.textContent).toBe('note.md');
|
||||
});
|
||||
|
||||
it('should exclude extension when shouldIncludeExtension is false', () => {
|
||||
context.settings.shouldIncludeExtension = false;
|
||||
const container = callGenerateBacklinkTitle(createMockFile('folder/note.md'));
|
||||
const fileNameSpan = container.shadowRoot?.querySelector('[part="file-name"]');
|
||||
expect(fileNameSpan?.textContent).toBe('note');
|
||||
});
|
||||
|
||||
it('should show file at root with no parent path', () => {
|
||||
const container = callGenerateBacklinkTitle(createMockFile('note.md'));
|
||||
const parentPathSpan = container.shadowRoot?.querySelector('[part="parent-path"]');
|
||||
expect(parentPathSpan).toBeNull();
|
||||
});
|
||||
|
||||
it('should show parent path with trailing separator', () => {
|
||||
context.settings.shouldReversePathParts = false;
|
||||
context.settings.shouldDisplayParentPathOnSeparateLine = false;
|
||||
const container = callGenerateBacklinkTitle(createMockFile('folder/note.md'));
|
||||
const parentPathSpan = container.shadowRoot?.querySelector('[part="parent-path"]');
|
||||
expect(parentPathSpan?.textContent).toBe('folder/');
|
||||
});
|
||||
|
||||
it('should strip root path prefix', () => {
|
||||
context.settings.rootPaths = ['folder'];
|
||||
const container = callGenerateBacklinkTitle(createMockFile('folder/subfolder/note.md'));
|
||||
const parentPathSpan = container.shadowRoot?.querySelector('[part="parent-path"]');
|
||||
expect(parentPathSpan?.textContent).toBe('subfolder/');
|
||||
});
|
||||
|
||||
it('should strip nested root path prefix', () => {
|
||||
context.settings.rootPaths = ['folder/subfolder'];
|
||||
const container = callGenerateBacklinkTitle(createMockFile('folder/subfolder/deep/note.md'));
|
||||
const parentPathSpan = container.shadowRoot?.querySelector('[part="parent-path"]');
|
||||
expect(parentPathSpan?.textContent).toBe('deep/');
|
||||
});
|
||||
|
||||
it('should limit depth with pathDepth setting', () => {
|
||||
context.settings.pathDepth = 2;
|
||||
context.settings.shouldShowEllipsisForSkippedPathParts = false;
|
||||
const container = callGenerateBacklinkTitle(createMockFile('a/b/c/note.md'));
|
||||
const parentPathSpan = container.shadowRoot?.querySelector('[part="parent-path"]');
|
||||
expect(parentPathSpan?.textContent).toBe('c/');
|
||||
});
|
||||
|
||||
it('should add ellipsis for skipped parts when enabled', () => {
|
||||
context.settings.pathDepth = 2;
|
||||
context.settings.shouldShowEllipsisForSkippedPathParts = true;
|
||||
const container = callGenerateBacklinkTitle(createMockFile('a/b/c/note.md'));
|
||||
const parentPathSpan = container.shadowRoot?.querySelector('[part="parent-path"]');
|
||||
expect(parentPathSpan?.textContent).toBe('.../c/');
|
||||
});
|
||||
|
||||
it('should not add ellipsis when pathDepth does not truncate', () => {
|
||||
context.settings.pathDepth = 5;
|
||||
context.settings.shouldShowEllipsisForSkippedPathParts = true;
|
||||
const container = callGenerateBacklinkTitle(createMockFile('a/b/note.md'));
|
||||
const parentPathSpan = container.shadowRoot?.querySelector('[part="parent-path"]');
|
||||
expect(parentPathSpan?.textContent).toBe('a/b/');
|
||||
});
|
||||
|
||||
it('should reverse path parts when shouldReversePathParts is true', () => {
|
||||
context.settings.shouldReversePathParts = true;
|
||||
context.settings.shouldDisplayParentPathOnSeparateLine = false;
|
||||
const container = callGenerateBacklinkTitle(createMockFile('a/b/note.md'));
|
||||
const parentPathSpan = container.shadowRoot?.querySelector('[part="parent-path"]');
|
||||
expect(parentPathSpan?.textContent).toBe(' ← b ← a');
|
||||
});
|
||||
|
||||
it('should display parent path on separate line without separator', () => {
|
||||
context.settings.shouldDisplayParentPathOnSeparateLine = true;
|
||||
context.settings.shouldReversePathParts = false;
|
||||
const container = callGenerateBacklinkTitle(createMockFile('folder/note.md'));
|
||||
const parentPathSpan = container.shadowRoot?.querySelector('[part="parent-path"]');
|
||||
expect(parentPathSpan?.textContent).toBe('folder');
|
||||
});
|
||||
|
||||
it('should set dataset attributes for highlighting', () => {
|
||||
context.settings.shouldHighlightFileName = true;
|
||||
context.settings.shouldDisplayParentPathOnSeparateLine = false;
|
||||
const container = callGenerateBacklinkTitle(createMockFile('note.md'));
|
||||
expect(container.dataset['shouldHighlightFileName']).toBe('true');
|
||||
expect(container.dataset['shouldDisplayParentPathOnSeparateLine']).toBe('false');
|
||||
});
|
||||
|
||||
it('should include full-path span with file path', () => {
|
||||
const container = callGenerateBacklinkTitle(createMockFile('folder/note.md'));
|
||||
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', () => {
|
||||
context.settings.shouldReversePathParts = false;
|
||||
context.settings.shouldDisplayParentPathOnSeparateLine = false;
|
||||
const container = callGenerateBacklinkTitle(createMockFile('folder/note.md'));
|
||||
const firstChild = container.shadowRoot?.firstElementChild;
|
||||
expect(firstChild?.getAttribute('part')).toBe('parent-path');
|
||||
});
|
||||
|
||||
it('should append parent path when reversed and not on separate line', () => {
|
||||
context.settings.shouldReversePathParts = true;
|
||||
context.settings.shouldDisplayParentPathOnSeparateLine = false;
|
||||
const container = callGenerateBacklinkTitle(createMockFile('folder/note.md'));
|
||||
const lastChild = container.shadowRoot?.lastElementChild;
|
||||
expect(lastChild?.getAttribute('part')).toBe('parent-path');
|
||||
});
|
||||
|
||||
it('should not prepend when displaying on separate line', () => {
|
||||
context.settings.shouldReversePathParts = false;
|
||||
context.settings.shouldDisplayParentPathOnSeparateLine = true;
|
||||
const container = callGenerateBacklinkTitle(createMockFile('folder/note.md'));
|
||||
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 view = await asInternals(context.component).getBacklinkView();
|
||||
expect(view).toBeNull();
|
||||
});
|
||||
|
||||
it('should return the view when a backlink leaf exists', async () => {
|
||||
const mockView = strictProxy<BacklinkView>({});
|
||||
const loadIfDeferred = vi.fn().mockResolvedValue(undefined);
|
||||
const mockLeaf = castTo<WorkspaceLeaf>({
|
||||
loadIfDeferred,
|
||||
view: mockView
|
||||
});
|
||||
|
||||
vi.mocked(context.app.workspace.getLeavesOfType).mockImplementation((type: string) => {
|
||||
if (type === ViewType.Backlink) {
|
||||
return [mockLeaf];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
const view = await asInternals(context.component).getBacklinkView();
|
||||
|
||||
expect(loadIfDeferred).toHaveBeenCalled();
|
||||
expect(view).toBe(mockView);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onBacklinksCorePluginEnable', () => {
|
||||
it('should invoke patchBacklinksPane', () => {
|
||||
const patchSpy = vi.spyOn(asInternals(context.component), 'patchBacklinksPane').mockResolvedValue(undefined);
|
||||
|
||||
asInternals(context.component).onBacklinksCorePluginEnable();
|
||||
|
||||
expect(patchSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('patchBacklinksPane', () => {
|
||||
it('should return early when no backlink view exists', async () => {
|
||||
const getViewSpy = vi.spyOn(asInternals(context.component), 'getBacklinkView').mockResolvedValue(null);
|
||||
|
||||
await asInternals(context.component).patchBacklinksPane();
|
||||
|
||||
expect(getViewSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should patch addResult to replace tree-item-inner with generated title', async () => {
|
||||
const originalAddResult = vi.fn().mockImplementation(() => createMockResultDomItem(true));
|
||||
const backlinkDomProto: BacklinkDom = { addResult: originalAddResult };
|
||||
const backlinkView = strictProxy<BacklinkView>({
|
||||
backlink: strictProxy({
|
||||
backlinkDom: Object.create(backlinkDomProto)
|
||||
})
|
||||
});
|
||||
vi.spyOn(asInternals(context.component), 'getBacklinkView').mockResolvedValue(backlinkView);
|
||||
|
||||
await asInternals(context.component).patchBacklinksPane();
|
||||
|
||||
const file = createMockFile('folder/note.md');
|
||||
const result = strictProxy<ResultDomResult>({});
|
||||
const item = backlinkDomProto.addResult(file, result, 'content', true);
|
||||
|
||||
const inner = item.el.querySelector('.tree-item-inner');
|
||||
expect(inner?.textContent).not.toBe('Original');
|
||||
});
|
||||
|
||||
it('should leave result unchanged when tree-item-inner is absent', async () => {
|
||||
const originalAddResult = vi.fn().mockImplementation(() => createMockResultDomItem(false));
|
||||
const backlinkDomProto: BacklinkDom = { addResult: originalAddResult };
|
||||
const backlinkView = strictProxy<BacklinkView>({
|
||||
backlink: strictProxy({
|
||||
backlinkDom: Object.create(backlinkDomProto)
|
||||
})
|
||||
});
|
||||
vi.spyOn(asInternals(context.component), 'getBacklinkView').mockResolvedValue(backlinkView);
|
||||
|
||||
await asInternals(context.component).patchBacklinksPane();
|
||||
|
||||
const file = createMockFile('note.md');
|
||||
const result = strictProxy<ResultDomResult>({});
|
||||
const item = backlinkDomProto.addResult(file, result, 'content', true);
|
||||
|
||||
expect(item.el.querySelector('.tree-item-inner')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('reloadBacklinksView', () => {
|
||||
it('should do nothing when no backlink view exists', async () => {
|
||||
const getViewSpy = vi.spyOn(asInternals(context.component), 'getBacklinkView').mockResolvedValue(null);
|
||||
|
||||
await asInternals(context.component).reloadBacklinksView();
|
||||
|
||||
expect(getViewSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not recompute when backlink view has no file', async () => {
|
||||
const recomputeBacklink = vi.fn();
|
||||
const backlinkView = strictProxy<BacklinkView>({
|
||||
backlink: strictProxy({ recomputeBacklink }),
|
||||
file: null
|
||||
});
|
||||
vi.spyOn(asInternals(context.component), 'getBacklinkView').mockResolvedValue(backlinkView);
|
||||
|
||||
await asInternals(context.component).reloadBacklinksView();
|
||||
|
||||
expect(recomputeBacklink).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should recompute when backlink view has a file', async () => {
|
||||
const mockFile = createMockFile('note.md');
|
||||
const recomputeBacklink = vi.fn();
|
||||
const backlinkView = strictProxy<BacklinkView>({
|
||||
backlink: strictProxy({ recomputeBacklink }),
|
||||
file: mockFile
|
||||
});
|
||||
vi.spyOn(asInternals(context.component), 'getBacklinkView').mockResolvedValue(backlinkView);
|
||||
|
||||
await asInternals(context.component).reloadBacklinksView();
|
||||
|
||||
expect(recomputeBacklink).toHaveBeenCalledWith(mockFile);
|
||||
});
|
||||
});
|
||||
|
||||
describe('refreshBacklinkPanels', () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(asInternals(context.component), 'reloadBacklinksView').mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it('should skip leaves that are not MarkdownView instances', async () => {
|
||||
context.markdownLeaves.push(castTo<WorkspaceLeaf>({ view: {} }));
|
||||
|
||||
await asInternals(context.component).refreshBacklinkPanels();
|
||||
|
||||
expect(asInternals(context.component).reloadBacklinksView).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should skip MarkdownView leaves without backlinks', async () => {
|
||||
context.markdownLeaves.push(createMarkdownLeaf(undefined));
|
||||
|
||||
await asInternals(context.component).refreshBacklinkPanels();
|
||||
|
||||
expect(asInternals(context.component).reloadBacklinksView).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should recompute backlinks for MarkdownView leaves with backlinks', async () => {
|
||||
const mockFile = createMockFile('test2.md');
|
||||
const recomputeBacklink = vi.fn();
|
||||
context.markdownLeaves.push(createMarkdownLeaf({ file: mockFile, recomputeBacklink }));
|
||||
|
||||
await asInternals(context.component).refreshBacklinkPanels();
|
||||
|
||||
expect(recomputeBacklink).toHaveBeenCalledWith(mockFile);
|
||||
});
|
||||
});
|
||||
});
|
||||
function internals(component: BacklinkFullPathComponent): ComponentInternals {
|
||||
return castTo<ComponentInternals>(component);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
import type { BacklinkPluginInstance } from '@obsidian-typings/obsidian-public-latest';
|
||||
import type { Mock } from 'vitest';
|
||||
|
||||
import { castTo } from 'obsidian-dev-utils/object-utils';
|
||||
import { strictProxy } from 'obsidian-dev-utils/strict-proxy';
|
||||
import {
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
vi
|
||||
} from 'vitest';
|
||||
|
||||
import type { BacklinkFullPathComponent } from '../backlink-full-path-component.ts';
|
||||
|
||||
import { BacklinkPluginInstanceOnUserEnablePatchComponent } from './backlink-plugin-instance-on-user-enable-patch-component.ts';
|
||||
|
||||
interface BacklinkPluginInstanceProto {
|
||||
onUserEnable(): void;
|
||||
}
|
||||
|
||||
describe('BacklinkPluginInstanceOnUserEnablePatchComponent', () => {
|
||||
let onBacklinksCorePluginEnable: Mock<() => void>;
|
||||
let backlinkFullPathComponent: BacklinkFullPathComponent;
|
||||
let originalOnUserEnable: Mock<() => void>;
|
||||
let instanceProto: BacklinkPluginInstanceProto;
|
||||
let backlinkPluginInstance: BacklinkPluginInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
onBacklinksCorePluginEnable = vi.fn<() => void>();
|
||||
backlinkFullPathComponent = strictProxy<BacklinkFullPathComponent>({
|
||||
onBacklinksCorePluginEnable
|
||||
});
|
||||
originalOnUserEnable = vi.fn<() => void>();
|
||||
instanceProto = { onUserEnable: originalOnUserEnable };
|
||||
backlinkPluginInstance = castTo<BacklinkPluginInstance>(Object.create(instanceProto));
|
||||
});
|
||||
|
||||
function createComponent(): BacklinkPluginInstanceOnUserEnablePatchComponent {
|
||||
return new BacklinkPluginInstanceOnUserEnablePatchComponent({
|
||||
backlinkFullPathComponent,
|
||||
backlinkPluginInstance
|
||||
});
|
||||
}
|
||||
|
||||
it('should register a single method patch on load', () => {
|
||||
const component = createComponent();
|
||||
const registerMethodPatchSpy = vi.spyOn(component, 'registerMethodPatch');
|
||||
|
||||
component.load();
|
||||
|
||||
expect(registerMethodPatchSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should call the original onUserEnable and notify the component when the patched method runs', () => {
|
||||
const component = createComponent();
|
||||
component.load();
|
||||
|
||||
instanceProto.onUserEnable.call(backlinkPluginInstance);
|
||||
|
||||
expect(originalOnUserEnable).toHaveBeenCalled();
|
||||
expect(onBacklinksCorePluginEnable).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should remove the patch on unload', () => {
|
||||
const component = createComponent();
|
||||
component.load();
|
||||
component.unload();
|
||||
|
||||
instanceProto.onUserEnable.call(backlinkPluginInstance);
|
||||
|
||||
expect(originalOnUserEnable).toHaveBeenCalled();
|
||||
expect(onBacklinksCorePluginEnable).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
231
src/patches/result-dom-add-result-patch-component.test.ts
Normal file
231
src/patches/result-dom-add-result-patch-component.test.ts
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
import type {
|
||||
ResultDom,
|
||||
ResultDomItem,
|
||||
ResultDomResult
|
||||
} from '@obsidian-typings/obsidian-public-latest';
|
||||
import type { TFile } from 'obsidian';
|
||||
|
||||
import { castTo } from 'obsidian-dev-utils/object-utils';
|
||||
import { strictProxy } from 'obsidian-dev-utils/strict-proxy';
|
||||
import {
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
vi
|
||||
} from 'vitest';
|
||||
|
||||
import type { PluginSettingsComponent } from '../plugin-settings-component.ts';
|
||||
|
||||
import { PluginSettings } from '../plugin-settings.ts';
|
||||
import { ResultDomAddResultPatchComponent } from './result-dom-add-result-patch-component.ts';
|
||||
|
||||
interface ResultDomProto {
|
||||
addResult(file: TFile, result: ResultDomResult, content: string, shouldShowTitle: boolean): ResultDomItem;
|
||||
}
|
||||
|
||||
describe('ResultDomAddResultPatchComponent', () => {
|
||||
let settings: PluginSettings;
|
||||
let pluginSettingsComponent: PluginSettingsComponent;
|
||||
let resultDomProto: ResultDomProto;
|
||||
let resultDom: ResultDom;
|
||||
|
||||
beforeEach(() => {
|
||||
settings = new PluginSettings();
|
||||
pluginSettingsComponent = strictProxy<PluginSettingsComponent>({
|
||||
settings
|
||||
});
|
||||
resultDomProto = {
|
||||
addResult: vi.fn().mockImplementation(() => createOriginalItem(true))
|
||||
};
|
||||
resultDom = castTo<ResultDom>(Object.create(resultDomProto));
|
||||
});
|
||||
|
||||
it('should register a single method patch on load', () => {
|
||||
const component = createComponent();
|
||||
const registerMethodPatchSpy = vi.spyOn(component, 'registerMethodPatch');
|
||||
|
||||
component.load();
|
||||
|
||||
expect(registerMethodPatchSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should leave the result untouched when there is no tree-item-inner', () => {
|
||||
resultDomProto.addResult = vi.fn().mockImplementation(() => createOriginalItem(false));
|
||||
const component = createComponent();
|
||||
component.load();
|
||||
|
||||
const item = resultDomProto.addResult(createMockFile('note.md'), strictProxy<ResultDomResult>({}), 'content', true);
|
||||
|
||||
expect(item.el.querySelector('.tree-item-inner')).toBeNull();
|
||||
});
|
||||
|
||||
it('should remove the patch on unload', () => {
|
||||
const component = createComponent();
|
||||
component.load();
|
||||
component.unload();
|
||||
|
||||
const item = resultDomProto.addResult(createMockFile('note.md'), strictProxy<ResultDomResult>({}), 'content', true);
|
||||
|
||||
expect(item.el.querySelector('.tree-item-inner')?.textContent).toBe('Original');
|
||||
});
|
||||
|
||||
describe('generated backlink title', () => {
|
||||
it('should replace the tree-item-inner content with the generated title', () => {
|
||||
const container = generateContainer('folder/note.md');
|
||||
expect(container.classList.contains('backlink-full-path')).toBe(true);
|
||||
});
|
||||
|
||||
it('should include the extension when shouldIncludeExtension is true', () => {
|
||||
settings.shouldIncludeExtension = true;
|
||||
const container = generateContainer('folder/note.md');
|
||||
expect(getPart(container, 'file-name')?.textContent).toBe('note.md');
|
||||
});
|
||||
|
||||
it('should exclude the extension when shouldIncludeExtension is false', () => {
|
||||
settings.shouldIncludeExtension = false;
|
||||
const container = generateContainer('folder/note.md');
|
||||
expect(getPart(container, 'file-name')?.textContent).toBe('note');
|
||||
});
|
||||
|
||||
it('should show no parent path for a file at the root', () => {
|
||||
const container = generateContainer('note.md');
|
||||
expect(getPart(container, 'parent-path')).toBeNull();
|
||||
});
|
||||
|
||||
it('should show the parent path with a trailing separator', () => {
|
||||
settings.shouldReversePathParts = false;
|
||||
settings.shouldDisplayParentPathOnSeparateLine = false;
|
||||
const container = generateContainer('folder/note.md');
|
||||
expect(getPart(container, 'parent-path')?.textContent).toBe('folder/');
|
||||
});
|
||||
|
||||
it('should strip a root path prefix', () => {
|
||||
settings.rootPaths = ['folder'];
|
||||
const container = generateContainer('folder/subfolder/note.md');
|
||||
expect(getPart(container, 'parent-path')?.textContent).toBe('subfolder/');
|
||||
});
|
||||
|
||||
it('should strip a nested root path prefix', () => {
|
||||
settings.rootPaths = ['folder/subfolder'];
|
||||
const container = generateContainer('folder/subfolder/deep/note.md');
|
||||
expect(getPart(container, 'parent-path')?.textContent).toBe('deep/');
|
||||
});
|
||||
|
||||
it('should limit depth with the pathDepth setting', () => {
|
||||
settings.pathDepth = 2;
|
||||
settings.shouldShowEllipsisForSkippedPathParts = false;
|
||||
const container = generateContainer('a/b/c/note.md');
|
||||
expect(getPart(container, 'parent-path')?.textContent).toBe('c/');
|
||||
});
|
||||
|
||||
it('should add an ellipsis for skipped parts when enabled', () => {
|
||||
settings.pathDepth = 2;
|
||||
settings.shouldShowEllipsisForSkippedPathParts = true;
|
||||
const container = generateContainer('a/b/c/note.md');
|
||||
expect(getPart(container, 'parent-path')?.textContent).toBe('.../c/');
|
||||
});
|
||||
|
||||
it('should not add an ellipsis when pathDepth does not truncate', () => {
|
||||
settings.pathDepth = 5;
|
||||
settings.shouldShowEllipsisForSkippedPathParts = true;
|
||||
const container = generateContainer('a/b/note.md');
|
||||
expect(getPart(container, 'parent-path')?.textContent).toBe('a/b/');
|
||||
});
|
||||
|
||||
it('should reverse the path parts when shouldReversePathParts is true', () => {
|
||||
settings.shouldReversePathParts = true;
|
||||
settings.shouldDisplayParentPathOnSeparateLine = false;
|
||||
const container = generateContainer('a/b/note.md');
|
||||
expect(getPart(container, 'parent-path')?.textContent).toBe(' ← b ← a');
|
||||
});
|
||||
|
||||
it('should display the parent path on a separate line without a separator', () => {
|
||||
settings.shouldDisplayParentPathOnSeparateLine = true;
|
||||
settings.shouldReversePathParts = false;
|
||||
const container = generateContainer('folder/note.md');
|
||||
expect(getPart(container, 'parent-path')?.textContent).toBe('folder');
|
||||
});
|
||||
|
||||
it('should set dataset attributes for highlighting', () => {
|
||||
settings.shouldHighlightFileName = true;
|
||||
settings.shouldDisplayParentPathOnSeparateLine = false;
|
||||
const container = generateContainer('note.md');
|
||||
expect(container.dataset['shouldHighlightFileName']).toBe('true');
|
||||
expect(container.dataset['shouldDisplayParentPathOnSeparateLine']).toBe('false');
|
||||
});
|
||||
|
||||
it('should include a full-path span with the file path', () => {
|
||||
const container = generateContainer('folder/note.md');
|
||||
expect(container.querySelector('.full-path')?.textContent).toBe('folder/note.md');
|
||||
});
|
||||
|
||||
it('should prepend the parent path when not reversed and not on a separate line', () => {
|
||||
settings.shouldReversePathParts = false;
|
||||
settings.shouldDisplayParentPathOnSeparateLine = false;
|
||||
const container = generateContainer('folder/note.md');
|
||||
expect(container.shadowRoot?.firstElementChild?.getAttribute('part')).toBe('parent-path');
|
||||
});
|
||||
|
||||
it('should append the parent path when reversed and not on a separate line', () => {
|
||||
settings.shouldReversePathParts = true;
|
||||
settings.shouldDisplayParentPathOnSeparateLine = false;
|
||||
const container = generateContainer('folder/note.md');
|
||||
expect(container.shadowRoot?.lastElementChild?.getAttribute('part')).toBe('parent-path');
|
||||
});
|
||||
|
||||
it('should append the parent path when displaying on a separate line', () => {
|
||||
settings.shouldReversePathParts = false;
|
||||
settings.shouldDisplayParentPathOnSeparateLine = true;
|
||||
const container = generateContainer('folder/note.md');
|
||||
expect(container.shadowRoot?.lastElementChild?.getAttribute('part')).toBe('parent-path');
|
||||
});
|
||||
});
|
||||
|
||||
function createComponent(): ResultDomAddResultPatchComponent {
|
||||
return new ResultDomAddResultPatchComponent({
|
||||
pluginSettingsComponent,
|
||||
resultDom
|
||||
});
|
||||
}
|
||||
|
||||
function generateContainer(filePath: string): HTMLDivElement {
|
||||
const component = createComponent();
|
||||
component.load();
|
||||
|
||||
const item = resultDomProto.addResult(createMockFile(filePath), strictProxy<ResultDomResult>({}), 'content', true);
|
||||
const inner = item.el.querySelector('.tree-item-inner');
|
||||
const container = inner?.firstElementChild;
|
||||
if (!(container instanceof HTMLDivElement)) {
|
||||
throw new Error('Expected a generated backlink-full-path container');
|
||||
}
|
||||
return container;
|
||||
}
|
||||
});
|
||||
|
||||
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 strictProxy<TFile>({
|
||||
basename,
|
||||
extension,
|
||||
name,
|
||||
path
|
||||
});
|
||||
}
|
||||
|
||||
function createOriginalItem(hasTreeItemInner: boolean): ResultDomItem {
|
||||
const el = createDiv();
|
||||
if (hasTreeItemInner) {
|
||||
const inner = el.createDiv({ cls: 'tree-item-inner' });
|
||||
inner.textContent = 'Original';
|
||||
}
|
||||
return strictProxy<ResultDomItem>({ el });
|
||||
}
|
||||
|
||||
function getPart(container: HTMLDivElement, part: string): Element | null {
|
||||
return container.shadowRoot?.querySelector(`[part="${part}"]`) ?? null;
|
||||
}
|
||||
Loading…
Reference in a new issue