firstsun-dev_git-files-sync/tests/setup.ts
ClaudiaFang f1420f0b44 test: add SyncStatusView coverage for the delete-path and folder-collision fixes
Adds minimal ItemView/WorkspaceLeaf mocks to tests/setup.ts (the shared
'obsidian' module mock) -- SyncStatusView had no test file before this.
New tests/ui/SyncStatusView.test.ts covers:
- performRemoteDeletion strips the vaultFolder prefix before calling
  gitService.deleteFile (with and without vaultFolder configured), and
  records the real error message instead of swallowing it.
- identifyExtraFiles classifies a local folder colliding with a stale
  remote record as remote-only rather than a readable file, while still
  treating a genuine local file as checkable.
2026-07-14 09:25:43 +00:00

296 lines
6.9 KiB
TypeScript

import { vi } from 'vitest';
// Mock global browser environment if not in JSDOM
if (typeof document === 'undefined') {
(globalThis as unknown as { document: unknown }).document = {
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
createElement: vi.fn(() => ({
classList: { add: vi.fn(), contains: vi.fn(), toggle: vi.fn() },
appendChild: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
setAttribute: vi.fn(),
textContent: '',
type: 'text',
empty() {},
})),
};
}
if (typeof window === 'undefined') {
(globalThis as unknown as { window: unknown }).window = {
setInterval: vi.fn(),
clearInterval: vi.fn(),
};
}
function createButtonElement(): HTMLButtonElement {
return document.createElement('button');
}
export const Plugin = class {};
export const PluginSettingTab = class {
app: unknown;
plugin: unknown;
containerEl: HTMLElement;
constructor(app?: unknown, plugin?: unknown) {
this.app = app;
this.plugin = plugin;
this.containerEl = document.createElement('div') as HTMLElement;
}
};
class BaseTextComponent<T extends HTMLInputElement | HTMLTextAreaElement> {
inputEl: T;
protected changeHandler?: (value: string) => void;
constructor(inputEl: T) {
this.inputEl = inputEl;
}
setPlaceholder(value: string) {
this.inputEl.placeholder = value;
return this;
}
setValue(value: string) {
this.inputEl.value = value;
return this;
}
onChange(handler: (value: string) => void) {
this.changeHandler = handler;
return this;
}
triggerChange(value: string) {
this.inputEl.value = value;
this.changeHandler?.(value);
}
}
export const TextComponent = class extends BaseTextComponent<HTMLInputElement> {
constructor(containerEl?: HTMLElement) {
const inputEl = document.createElement('input');
containerEl?.appendChild(inputEl);
super(inputEl);
}
};
export const TextAreaComponent = class extends BaseTextComponent<HTMLTextAreaElement> {
constructor(containerEl?: HTMLElement) {
const inputEl = document.createElement('textarea');
containerEl?.appendChild(inputEl);
super(inputEl);
}
};
export const DropdownComponent = class {
private changeHandler?: (value: string) => void;
addOption() { return this; }
setValue() { return this; }
onChange(handler: (value: string) => void) {
this.changeHandler = handler;
return this;
}
triggerChange(value: string) {
this.changeHandler?.(value);
}
};
export const ButtonComponent = class {
buttonEl: HTMLButtonElement;
constructor(containerEl?: HTMLElement) {
this.buttonEl = createButtonElement();
containerEl?.appendChild(this.buttonEl);
}
setButtonText(text: string) {
this.buttonEl.textContent = text;
return this;
}
setTooltip(text: string) {
this.buttonEl.title = text;
return this;
}
setCta() {
this.buttonEl.classList.add('mod-cta');
return this;
}
setWarning() {
this.buttonEl.classList.add('mod-warning');
return this;
}
setDestructive() {
this.buttonEl.classList.add('mod-destructive');
return this;
}
setIcon(icon: string) {
this.buttonEl.dataset.icon = icon;
return this;
}
onClick(handler: () => void) {
this.buttonEl.addEventListener('click', handler);
return this;
}
};
export const ExtraButtonComponent = class extends ButtonComponent {};
export const Setting = class {
containerEl?: HTMLElement;
constructor(containerEl?: HTMLElement) {
this.containerEl = containerEl;
}
setName() { return this; }
setDesc() { return this; }
setHeading() { return this; }
addToggle() { return this; }
addText(callback?: (component: InstanceType<typeof TextComponent>) => void) {
if (callback) callback(new TextComponent(this.containerEl));
return this;
}
addTextArea(callback?: (component: InstanceType<typeof TextAreaComponent>) => void) {
if (callback) callback(new TextAreaComponent(this.containerEl));
return this;
}
addButton(callback?: (component: InstanceType<typeof ButtonComponent>) => void) {
if (callback) callback(new ButtonComponent(this.containerEl));
return this;
}
addExtraButton(callback?: (component: InstanceType<typeof ExtraButtonComponent>) => void) {
if (callback) callback(new ExtraButtonComponent(this.containerEl));
return this;
}
addDropdown(callback?: (component: InstanceType<typeof DropdownComponent>) => void) {
if (callback) callback(new DropdownComponent());
return this;
}
};
export const Notice = class {
constructor() {}
setMessage() {}
hide() {}
};
export const Modal = class {
app: unknown;
contentEl: HTMLElement;
constructor(app?: unknown) {
this.app = app;
this.contentEl = document.createElement('div') as HTMLElement;
}
open() {
const withOnOpen = this as unknown as { onOpen?: () => void };
if (typeof withOnOpen.onOpen === 'function') {
withOnOpen.onOpen();
}
}
close() {
const withOnClose = this as unknown as { onClose?: () => void };
if (typeof withOnClose.onClose === 'function') {
withOnClose.onClose();
}
}
};
export const MarkdownView = class {};
export const Editor = class {};
export const WorkspaceLeaf = class {};
export const ItemView = class {
app: unknown;
leaf: unknown;
containerEl: HTMLElement;
constructor(leaf?: { app?: unknown }) {
this.leaf = leaf;
this.app = leaf?.app;
// Real ItemView's containerEl has a header at children[0] and the
// content root at children[1]; views render into the latter.
this.containerEl = document.createElement('div') as HTMLElement;
this.containerEl.appendChild(document.createElement('div'));
this.containerEl.appendChild(document.createElement('div'));
}
registerEvent() {}
register() {}
registerDomEvent() {}
registerInterval() {}
};
export const App = class {
workspace = {
getActiveViewOfType: vi.fn(),
on: vi.fn(),
};
vault = {
read: vi.fn(),
modify: vi.fn(),
getFileByPath: vi.fn(),
on: vi.fn(),
configDir: 'mock-config-dir',
adapter: {
getBasePath: vi.fn().mockReturnValue('/mock/path'),
},
};
};
export const TFile = class {};
export const TFolder = class {};
export const AbstractInputSuggest = class {
app: unknown;
constructor(app: unknown) {
this.app = app;
}
setValue() {}
getValue() { return ''; }
close() {}
open() {}
};
export const requestUrl = vi.fn();
export const setTooltip = vi.fn();
export const setIcon = vi.fn();
export const Platform = { isDesktopApp: true };
export const FileSystemAdapter = class {
getBasePath() { return '/mock/path'; }
};
vi.mock('obsidian', () => ({
Plugin,
PluginSettingTab,
Setting,
Notice,
Modal,
MarkdownView,
Editor,
WorkspaceLeaf,
ItemView,
App,
TFile,
TFolder,
AbstractInputSuggest,
TextComponent,
TextAreaComponent,
DropdownComponent,
ButtonComponent,
ExtraButtonComponent,
requestUrl,
setTooltip,
setIcon,
Platform,
FileSystemAdapter,
}));