@real1ty-obsidian-plugins/utils 2.3.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,26 @@
1
+ export const capitalize = (str: string): string => {
2
+ if (!str) return str;
3
+ return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
4
+ };
5
+
6
+ export const generateDuplicatedTitle = (originalTitle: string): string => {
7
+ // Check if title already has a counter pattern like " (2)", " - Copy", etc.
8
+ const counterMatch = originalTitle.match(/^(.*?)(?:\s*\((\d+)\)|\s*-?\s*Copy(?:\s*(\d+))?)$/);
9
+
10
+ if (counterMatch) {
11
+ const baseName = counterMatch[1];
12
+ const existingCounter = counterMatch[2] || counterMatch[3];
13
+ const nextCounter = existingCounter ? parseInt(existingCounter, 10) + 1 : 2;
14
+ return `${baseName} (${nextCounter})`;
15
+ } else {
16
+ return `${originalTitle} (2)`;
17
+ }
18
+ };
19
+
20
+ export const pluralize = (count: number): string => {
21
+ return count === 1 ? "" : "s";
22
+ };
23
+
24
+ export const getWeekDirection = (weeks: number): "next" | "previous" => {
25
+ return weeks > 0 ? "next" : "previous";
26
+ };
@@ -0,0 +1,23 @@
1
+ export * from "./mocks/obsidian";
2
+ // Re-export commonly used combinations
3
+ export {
4
+ createMockApp,
5
+ createMockFile,
6
+ createMockFileCache,
7
+ Modal,
8
+ Notice,
9
+ Plugin,
10
+ PluginSettingTab,
11
+ Setting,
12
+ TFile,
13
+ } from "./mocks/obsidian";
14
+ export * from "./mocks/utils";
15
+ export {
16
+ mockFileOperations,
17
+ mockLinkParser,
18
+ resetAllMocks,
19
+ setupDefaultMockImplementations,
20
+ setupMockImplementation,
21
+ setupMockReturnValue,
22
+ verifyMockCalls,
23
+ } from "./mocks/utils";
@@ -0,0 +1,331 @@
1
+ import { vi } from "vitest";
2
+
3
+ // Base Plugin class mock
4
+ export class Plugin {
5
+ app: any;
6
+ manifest: any;
7
+ settings: any;
8
+
9
+ constructor(app: any, manifest: any) {
10
+ this.app = app;
11
+ this.manifest = manifest;
12
+ }
13
+
14
+ // Core plugin methods
15
+ addSettingTab = vi.fn();
16
+ registerEvent = vi.fn();
17
+ loadData = vi.fn().mockResolvedValue({});
18
+ saveData = vi.fn().mockResolvedValue(undefined);
19
+ onload = vi.fn();
20
+ onunload = vi.fn();
21
+
22
+ // UI methods
23
+ addRibbonIcon = vi.fn();
24
+ addStatusBarItem = vi.fn();
25
+ addCommand = vi.fn();
26
+ removeCommand = vi.fn();
27
+
28
+ // Event methods
29
+ registerDomEvent = vi.fn();
30
+ registerCodeMirror = vi.fn();
31
+ registerEditorExtension = vi.fn();
32
+ registerMarkdownPostProcessor = vi.fn();
33
+ registerMarkdownCodeBlockProcessor = vi.fn();
34
+ registerObsidianProtocolHandler = vi.fn();
35
+ registerEditorSuggest = vi.fn();
36
+ registerHoverLinkSource = vi.fn();
37
+
38
+ // Interval methods
39
+ registerInterval = vi.fn();
40
+
41
+ // View and extension methods
42
+ registerView = vi.fn();
43
+ registerExtensions = vi.fn();
44
+
45
+ // Lifecycle methods
46
+ onUserEnable = vi.fn();
47
+ load = vi.fn();
48
+ unload = vi.fn();
49
+
50
+ // Other methods
51
+ addChild = vi.fn();
52
+ removeChild = vi.fn();
53
+ register = vi.fn();
54
+ }
55
+
56
+ // PluginSettingTab mock
57
+ export class PluginSettingTab {
58
+ app: any;
59
+ plugin: any;
60
+ containerEl: HTMLElement;
61
+
62
+ constructor(app: any, plugin: any) {
63
+ this.app = app;
64
+ this.plugin = plugin;
65
+ this.containerEl = document.createElement("div");
66
+ }
67
+
68
+ display = vi.fn();
69
+ }
70
+
71
+ // ItemView mock
72
+ export class ItemView {
73
+ app: any;
74
+ leaf: any;
75
+ containerEl: HTMLElement;
76
+
77
+ constructor(leaf: any) {
78
+ this.leaf = leaf;
79
+ this.app = leaf?.app;
80
+ this.containerEl = document.createElement("div");
81
+ }
82
+
83
+ // Don't override onOpen/onClose - let subclasses implement them
84
+ // These methods are implemented by MountableView mixin
85
+
86
+ // Don't provide default implementations for these methods
87
+ // Let subclasses implement them
88
+ getViewType(): string {
89
+ return "mock-view";
90
+ }
91
+
92
+ getDisplayText(): string {
93
+ return "Mock View";
94
+ }
95
+
96
+ getIcon(): string {
97
+ return "mock-icon";
98
+ }
99
+
100
+ getState = vi.fn().mockReturnValue({});
101
+ setState = vi.fn().mockResolvedValue(undefined);
102
+ }
103
+
104
+ // Setting component mock
105
+ export class Setting {
106
+ settingEl: HTMLElement;
107
+ nameEl: HTMLElement;
108
+ descEl: HTMLElement;
109
+ controlEl: HTMLElement;
110
+
111
+ constructor(_containerEl: HTMLElement) {
112
+ this.settingEl = document.createElement("div");
113
+ this.nameEl = document.createElement("div");
114
+ this.descEl = document.createElement("div");
115
+ this.controlEl = document.createElement("div");
116
+ }
117
+
118
+ setName = vi.fn().mockReturnThis();
119
+ setDesc = vi.fn().mockReturnThis();
120
+ addText = vi.fn().mockReturnThis();
121
+ addTextArea = vi.fn().mockReturnThis();
122
+ }
123
+
124
+ // TFolder mock
125
+ export class TFolder {
126
+ path: string;
127
+ name: string;
128
+ children: any[];
129
+ vault: any;
130
+ parent: TFolder | null;
131
+
132
+ constructor(path: string) {
133
+ this.path = path;
134
+ this.name = path.split("/").pop() || "";
135
+ this.children = [];
136
+ this.vault = {};
137
+ this.parent = null;
138
+ }
139
+
140
+ isRoot(): boolean {
141
+ return this.path === "" || this.path === "/";
142
+ }
143
+ }
144
+
145
+ // TFile mock with full interface
146
+ export class TFile {
147
+ path: string;
148
+ name: string;
149
+ basename: string;
150
+ extension: string;
151
+ stat: any;
152
+ vault: any;
153
+ parent: TFolder | null;
154
+
155
+ constructor(path: string, parentPath?: string) {
156
+ this.path = path;
157
+ this.name = path.split("/").pop() || "";
158
+ this.basename = this.name.replace(/\.[^/.]+$/, ""); // Remove extension
159
+ this.extension = path.split(".").pop() || "md";
160
+ this.stat = {};
161
+ this.vault = {};
162
+
163
+ // Set parent based on path or explicit parentPath
164
+ if (parentPath !== undefined) {
165
+ this.parent = parentPath ? new TFolder(parentPath) : null;
166
+ } else {
167
+ // Derive parent from path
168
+ const lastSlash = path.lastIndexOf("/");
169
+ if (lastSlash > 0) {
170
+ this.parent = new TFolder(path.substring(0, lastSlash));
171
+ } else {
172
+ this.parent = null;
173
+ }
174
+ }
175
+ }
176
+ }
177
+
178
+ // Modal mock
179
+ export class Modal {
180
+ app: any;
181
+ containerEl: HTMLElement;
182
+ titleEl: HTMLElement;
183
+ contentEl: HTMLElement;
184
+
185
+ constructor(app: any) {
186
+ this.app = app;
187
+ this.containerEl = document.createElement("div");
188
+ this.titleEl = document.createElement("div");
189
+ this.contentEl = document.createElement("div");
190
+ }
191
+
192
+ open = vi.fn();
193
+ close = vi.fn();
194
+ onOpen = vi.fn();
195
+ onClose = vi.fn();
196
+ }
197
+
198
+ // Notice mock
199
+ export class Notice {
200
+ constructor(message: string) {
201
+ console.log(`Notice: ${message}`);
202
+ }
203
+ }
204
+
205
+ // MarkdownRenderer mock
206
+ export const MarkdownRenderer = {
207
+ render: vi.fn().mockResolvedValue(undefined),
208
+ };
209
+
210
+ // Debounce function mock
211
+ export function debounce<T extends (...args: any[]) => any>(
212
+ func: T,
213
+ wait: number,
214
+ immediate?: boolean
215
+ ): T {
216
+ let timeout: ReturnType<typeof setTimeout> | null = null;
217
+
218
+ return ((...args: Parameters<T>) => {
219
+ const later = () => {
220
+ timeout = null;
221
+ if (!immediate) func(...args);
222
+ };
223
+
224
+ const callNow = immediate && !timeout;
225
+
226
+ if (timeout !== null) {
227
+ clearTimeout(timeout);
228
+ }
229
+
230
+ timeout = setTimeout(later, wait);
231
+
232
+ if (callNow) func(...args);
233
+ }) as T;
234
+ }
235
+
236
+ // normalizePath mock - simple path normalization for tests
237
+ export function normalizePath(path: string): string {
238
+ // Basic normalization: replace backslashes with forward slashes
239
+ // and remove duplicate slashes
240
+ return path.replace(/\\/g, "/").replace(/\/+/g, "/");
241
+ }
242
+
243
+ // App mock
244
+ export const App = vi.fn();
245
+
246
+ // Mock interfaces for TypeScript
247
+ export interface MockApp {
248
+ fileManager: {
249
+ processFrontMatter: ReturnType<typeof vi.fn>;
250
+ };
251
+ metadataCache: {
252
+ getFileCache: ReturnType<typeof vi.fn>;
253
+ };
254
+ vault: {
255
+ getAbstractFileByPath: ReturnType<typeof vi.fn>;
256
+ on: ReturnType<typeof vi.fn>;
257
+ read: ReturnType<typeof vi.fn>;
258
+ modify: ReturnType<typeof vi.fn>;
259
+ create: ReturnType<typeof vi.fn>;
260
+ delete: ReturnType<typeof vi.fn>;
261
+ rename: ReturnType<typeof vi.fn>;
262
+ getFiles: ReturnType<typeof vi.fn>;
263
+ getMarkdownFiles: ReturnType<typeof vi.fn>;
264
+ getFolderByPath: ReturnType<typeof vi.fn>;
265
+ };
266
+ workspace: {
267
+ getActiveFile: ReturnType<typeof vi.fn>;
268
+ on: ReturnType<typeof vi.fn>;
269
+ };
270
+ }
271
+
272
+ // Helper function to create a fully mocked app
273
+ export function createMockApp(): MockApp {
274
+ return {
275
+ fileManager: {
276
+ processFrontMatter: vi.fn(),
277
+ },
278
+ metadataCache: {
279
+ getFileCache: vi.fn(),
280
+ },
281
+ vault: {
282
+ getAbstractFileByPath: vi.fn(),
283
+ on: vi.fn(),
284
+ read: vi.fn(),
285
+ modify: vi.fn(),
286
+ create: vi.fn(),
287
+ delete: vi.fn(),
288
+ rename: vi.fn(),
289
+ getFiles: vi.fn().mockReturnValue([]),
290
+ getMarkdownFiles: vi.fn().mockReturnValue([]),
291
+ getFolderByPath: vi.fn(),
292
+ },
293
+ workspace: {
294
+ getActiveFile: vi.fn(),
295
+ on: vi.fn(),
296
+ },
297
+ };
298
+ }
299
+
300
+ // Helper to create mock TFile instances
301
+ export function createMockFile(
302
+ path: string,
303
+ options?: {
304
+ basename?: string;
305
+ parentPath?: string;
306
+ extension?: string;
307
+ }
308
+ ): TFile {
309
+ const file = new TFile(path, options?.parentPath);
310
+ if (options?.basename) {
311
+ file.basename = options.basename;
312
+ }
313
+ if (options?.extension) {
314
+ file.extension = options.extension;
315
+ }
316
+ return file;
317
+ }
318
+
319
+ // Helper to create mock file cache
320
+ export function createMockFileCache(frontmatter?: Record<string, any>) {
321
+ return {
322
+ frontmatter: frontmatter || {},
323
+ frontmatterPosition: frontmatter ? { start: { line: 0 }, end: { line: 3 } } : null,
324
+ sections: [],
325
+ headings: [],
326
+ links: [],
327
+ embeds: [],
328
+ tags: [],
329
+ listItems: [],
330
+ };
331
+ }
@@ -0,0 +1,113 @@
1
+ import { expect, vi } from "vitest";
2
+
3
+ // File operations mocks
4
+ export const mockFileOperations = {
5
+ arraysEqual: vi.fn(),
6
+ normalizeArray: vi.fn(),
7
+ createFileLink: vi.fn(),
8
+ };
9
+
10
+ // Link parser mocks
11
+ export const mockLinkParser = {
12
+ extractFilePathFromLink: vi.fn(),
13
+ };
14
+
15
+ // Default mock implementations that match the actual behavior
16
+ export function setupDefaultMockImplementations() {
17
+ // Set up file operations mocks
18
+ mockFileOperations.normalizeArray.mockImplementation((arr) =>
19
+ Array.isArray(arr) ? arr : arr ? [arr] : []
20
+ );
21
+
22
+ mockFileOperations.arraysEqual.mockImplementation(
23
+ (a, b) => JSON.stringify(a) === JSON.stringify(b)
24
+ );
25
+
26
+ mockFileOperations.createFileLink.mockImplementation((file) => {
27
+ if (!file) return "[[Unknown File]]";
28
+
29
+ const basename =
30
+ file.basename ||
31
+ file.path
32
+ ?.split("/")
33
+ .pop()
34
+ ?.replace(/\.[^/.]+$/, "") ||
35
+ "";
36
+ const parentPath = file.parent?.path;
37
+
38
+ if (!parentPath || parentPath === "/" || parentPath === "") {
39
+ return `[[${basename}]]`;
40
+ }
41
+
42
+ return `[[${parentPath}/${basename}|${basename}]]`;
43
+ });
44
+
45
+ // Set up link parser mocks
46
+ mockLinkParser.extractFilePathFromLink.mockImplementation((link) => {
47
+ if (!link || typeof link !== "string") return null;
48
+
49
+ // Handle text that contains a link
50
+ const linkMatch = link.match(/\[\[([^\]]+)\]\]/);
51
+ if (linkMatch) {
52
+ const content = linkMatch[1];
53
+ // Remove display name if present
54
+ const filePart = content.split("|")[0].trim();
55
+ if (!filePart) return null;
56
+
57
+ // Add .md extension if not present
58
+ return filePart.endsWith(".md") ? filePart : `${filePart}.md`;
59
+ }
60
+
61
+ return null;
62
+ });
63
+ }
64
+
65
+ // Reset all mocks
66
+ export function resetAllMocks() {
67
+ Object.values(mockFileOperations).forEach((mock) => {
68
+ mock.mockReset();
69
+ });
70
+ Object.values(mockLinkParser).forEach((mock) => {
71
+ mock.mockReset();
72
+ });
73
+ }
74
+
75
+ // Helper to setup mocks with specific implementations
76
+ export function setupMockImplementation(
77
+ mockName: keyof typeof mockFileOperations | keyof typeof mockLinkParser,
78
+ implementation: (...args: any[]) => any
79
+ ) {
80
+ if (mockName in mockFileOperations) {
81
+ (mockFileOperations as any)[mockName].mockImplementation(implementation);
82
+ } else if (mockName in mockLinkParser) {
83
+ (mockLinkParser as any)[mockName].mockImplementation(implementation);
84
+ }
85
+ }
86
+
87
+ // Helper to setup mock return values
88
+ export function setupMockReturnValue(
89
+ mockName: keyof typeof mockFileOperations | keyof typeof mockLinkParser,
90
+ value: any
91
+ ) {
92
+ if (mockName in mockFileOperations) {
93
+ (mockFileOperations as any)[mockName].mockReturnValue(value);
94
+ } else if (mockName in mockLinkParser) {
95
+ (mockLinkParser as any)[mockName].mockReturnValue(value);
96
+ }
97
+ }
98
+
99
+ // Helper to verify mock calls
100
+ export function verifyMockCalls(
101
+ mockName: keyof typeof mockFileOperations | keyof typeof mockLinkParser,
102
+ expectedCalls: any[][]
103
+ ) {
104
+ const mock =
105
+ mockName in mockFileOperations
106
+ ? (mockFileOperations as any)[mockName]
107
+ : (mockLinkParser as any)[mockName];
108
+
109
+ expect(mock).toHaveBeenCalledTimes(expectedCalls.length);
110
+ expectedCalls.forEach((args, index) => {
111
+ expect(mock).toHaveBeenNthCalledWith(index + 1, ...args);
112
+ });
113
+ }
@@ -0,0 +1,19 @@
1
+ import { vi } from "vitest";
2
+ import { mockFileOperations, mockLinkParser } from "./mocks/utils";
3
+
4
+ // Global test setup that can be imported once per test file
5
+ export function setupTestEnvironment() {
6
+ // Mock the utils modules
7
+ vi.mock("@obsidian-plugins/utils/file-operations", () => mockFileOperations);
8
+ vi.mock("@obsidian-plugins/utils/link-parser", () => mockLinkParser);
9
+
10
+ // Mock any plugin-specific components
11
+ vi.mock("../src/components/settings-tab", () => ({
12
+ TreePropertiesManagerSettingTab: class MockSettingTab {},
13
+ }));
14
+
15
+ // Return cleanup function
16
+ return () => {
17
+ vi.clearAllMocks();
18
+ };
19
+ }