Skip to content

Commit b15d457

Browse files
committed
feat: support time parsing service
1 parent 69b27ac commit b15d457

6 files changed

Lines changed: 1344 additions & 91 deletions

File tree

Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
import { QuickCaptureModal } from "../components/QuickCaptureModal";
2+
import { DEFAULT_TIME_PARSING_CONFIG } from "../utils/TimeParsingService";
3+
import { App } from "obsidian";
4+
5+
// Mock dependencies
6+
jest.mock("obsidian", () => ({
7+
App: jest.fn(),
8+
Modal: class MockModal {
9+
constructor(app: any, plugin: any) {}
10+
onOpen() {}
11+
onClose() {}
12+
close() {}
13+
modalEl = { toggleClass: jest.fn() };
14+
titleEl = { createDiv: jest.fn(), createEl: jest.fn() };
15+
contentEl = {
16+
empty: jest.fn(),
17+
createDiv: jest.fn(() => ({
18+
createDiv: jest.fn(),
19+
createEl: jest.fn(),
20+
createSpan: jest.fn(),
21+
addClass: jest.fn(),
22+
setAttribute: jest.fn(),
23+
addEventListener: jest.fn(),
24+
})),
25+
createEl: jest.fn(),
26+
};
27+
},
28+
Setting: class MockSetting {
29+
constructor(containerEl: any) {}
30+
setName(name: string) {
31+
return this;
32+
}
33+
setDesc(desc: string) {
34+
return this;
35+
}
36+
addToggle(cb: any) {
37+
return this;
38+
}
39+
addText(cb: any) {
40+
return this;
41+
}
42+
addTextArea(cb: any) {
43+
return this;
44+
}
45+
addDropdown(cb: any) {
46+
return this;
47+
}
48+
},
49+
Notice: jest.fn(),
50+
Platform: { isPhone: false },
51+
MarkdownRenderer: jest.fn(),
52+
moment: () => ({ format: jest.fn(() => "2025-01-04") }),
53+
}));
54+
55+
jest.mock("../editor-ext/markdownEditor", () => ({
56+
createEmbeddableMarkdownEditor: jest.fn(() => ({
57+
value: "",
58+
editor: { focus: jest.fn() },
59+
scope: { register: jest.fn() },
60+
destroy: jest.fn(),
61+
})),
62+
}));
63+
64+
jest.mock("../utils/fileUtils", () => ({
65+
saveCapture: jest.fn(),
66+
processDateTemplates: jest.fn(),
67+
}));
68+
69+
jest.mock("../components/AutoComplete", () => ({
70+
FileSuggest: jest.fn(),
71+
ContextSuggest: jest.fn(),
72+
ProjectSuggest: jest.fn(),
73+
}));
74+
75+
jest.mock("../translations/helper", () => ({
76+
t: (key: string) => key,
77+
}));
78+
79+
jest.mock("../components/MarkdownRenderer", () => ({
80+
MarkdownRendererComponent: class MockMarkdownRenderer {
81+
constructor() {}
82+
render() {}
83+
unload() {}
84+
},
85+
}));
86+
87+
jest.mock("../components/StatusComponent", () => ({
88+
StatusComponent: class MockStatusComponent {
89+
constructor() {}
90+
load() {}
91+
},
92+
}));
93+
94+
describe("QuickCaptureModal Time Parsing Integration", () => {
95+
let mockApp: any;
96+
let mockPlugin: any;
97+
let modal: QuickCaptureModal;
98+
99+
beforeEach(() => {
100+
mockApp = new App();
101+
mockPlugin = {
102+
settings: {
103+
quickCapture: {
104+
targetType: "fixed",
105+
targetFile: "test.md",
106+
placeholder: "Enter task...",
107+
dailyNoteSettings: {
108+
format: "YYYY-MM-DD",
109+
folder: "",
110+
template: "",
111+
},
112+
},
113+
preferMetadataFormat: "tasks",
114+
timeParsing: DEFAULT_TIME_PARSING_CONFIG,
115+
},
116+
};
117+
118+
modal = new QuickCaptureModal(mockApp, mockPlugin, undefined, true);
119+
});
120+
121+
afterEach(() => {
122+
jest.clearAllMocks();
123+
});
124+
125+
describe("Time Parsing Service Integration", () => {
126+
test("should initialize with plugin settings", () => {
127+
expect(modal.timeParsingService).toBeDefined();
128+
expect(modal.timeParsingService.getConfig()).toEqual(
129+
mockPlugin.settings.timeParsing
130+
);
131+
});
132+
133+
test("should fallback to default config when plugin settings missing", () => {
134+
const pluginWithoutTimeParsing = {
135+
...mockPlugin,
136+
settings: {
137+
...mockPlugin.settings,
138+
timeParsing: undefined,
139+
},
140+
};
141+
142+
const modalWithoutConfig = new QuickCaptureModal(
143+
mockApp,
144+
pluginWithoutTimeParsing,
145+
undefined,
146+
true
147+
);
148+
expect(modalWithoutConfig.timeParsingService).toBeDefined();
149+
expect(modalWithoutConfig.timeParsingService.getConfig()).toEqual(
150+
DEFAULT_TIME_PARSING_CONFIG
151+
);
152+
});
153+
});
154+
155+
describe("Content Processing with Time Parsing", () => {
156+
test("should parse time expressions and update metadata", () => {
157+
const content = "go to bed tomorrow";
158+
const result = modal.processContentWithMetadata(content);
159+
160+
// Should contain task metadata
161+
expect(result).toContain("📅");
162+
// Should not contain 'tomorrow' in the final result (cleaned)
163+
expect(result).not.toContain("tomorrow");
164+
});
165+
166+
test("should handle multiple time expressions", () => {
167+
const content = "start project tomorrow and finish by next week";
168+
const result = modal.processContentWithMetadata(content);
169+
170+
// Should process the content and add metadata
171+
expect(result).toContain("- [ ]");
172+
});
173+
174+
test("should preserve content when no time expressions found", () => {
175+
const content = "regular task without dates";
176+
const result = modal.processContentWithMetadata(content);
177+
178+
expect(result).toContain("regular task without dates");
179+
});
180+
181+
test("should handle Chinese time expressions", () => {
182+
const content = "明天开会";
183+
const result = modal.processContentWithMetadata(content);
184+
185+
// Should contain task metadata
186+
expect(result).toContain("📅");
187+
// Should not contain '明天' in the final result (cleaned)
188+
expect(result).not.toContain("明天");
189+
});
190+
});
191+
192+
describe("Manual Override Functionality", () => {
193+
test("should track manually set dates", () => {
194+
modal.markAsManuallySet("dueDate");
195+
expect(modal.isManuallySet("dueDate")).toBe(true);
196+
expect(modal.isManuallySet("startDate")).toBe(false);
197+
});
198+
199+
test("should not override manually set dates", () => {
200+
// Manually set a due date
201+
modal.taskMetadata.dueDate = new Date("2025-01-10");
202+
modal.markAsManuallySet("dueDate");
203+
204+
// Process content with time expression
205+
const content = "task tomorrow";
206+
modal.processContentWithMetadata(content);
207+
208+
// Should preserve manually set date
209+
expect(modal.taskMetadata.dueDate).toEqual(new Date("2025-01-10"));
210+
});
211+
});
212+
213+
describe("Metadata Format Generation", () => {
214+
test("should generate metadata in tasks format", () => {
215+
modal.preferMetadataFormat = "tasks";
216+
modal.taskMetadata.dueDate = new Date("2025-01-05");
217+
modal.taskMetadata.priority = 3;
218+
219+
const metadata = modal.generateMetadataString();
220+
expect(metadata).toContain("📅 2025-01-05");
221+
expect(metadata).toContain("🔼");
222+
});
223+
224+
test("should generate metadata in dataview format", () => {
225+
modal.preferMetadataFormat = "dataview";
226+
modal.taskMetadata.dueDate = new Date("2025-01-05");
227+
modal.taskMetadata.priority = 3;
228+
229+
const metadata = modal.generateMetadataString();
230+
expect(metadata).toContain("[due:: 2025-01-05]");
231+
expect(metadata).toContain("[priority:: medium]");
232+
});
233+
});
234+
235+
describe("Task Line Processing", () => {
236+
test("should convert plain text to task with metadata", () => {
237+
modal.taskMetadata.dueDate = new Date("2025-01-05");
238+
const taskLine = modal.addMetadataToTask("- [ ] test task");
239+
240+
expect(taskLine).toContain("- [ ] test task");
241+
expect(taskLine).toContain("📅 2025-01-05");
242+
});
243+
244+
test("should handle existing task format", () => {
245+
modal.taskMetadata.dueDate = new Date("2025-01-05");
246+
const taskLine = modal.addMetadataToTask("- [x] completed task");
247+
248+
expect(taskLine).toContain("- [x] completed task");
249+
expect(taskLine).toContain("📅 2025-01-05");
250+
});
251+
});
252+
253+
describe("Date Formatting", () => {
254+
test("should format dates correctly", () => {
255+
const date = new Date("2025-01-05");
256+
const formatted = modal.formatDate(date);
257+
expect(formatted).toBe("2025-01-05");
258+
});
259+
260+
test("should parse date strings correctly", () => {
261+
const parsed = modal.parseDate("2025-01-05");
262+
expect(parsed.getFullYear()).toBe(2025);
263+
expect(parsed.getMonth()).toBe(0); // January is 0
264+
expect(parsed.getDate()).toBe(5);
265+
});
266+
});
267+
268+
describe("Error Handling", () => {
269+
test("should handle invalid time expressions gracefully", () => {
270+
const content = "task with invalid date xyz123";
271+
const result = modal.processContentWithMetadata(content);
272+
273+
// Should not crash and should return valid content
274+
expect(result).toContain("task with invalid date xyz123");
275+
});
276+
277+
test("should handle empty content", () => {
278+
const content = "";
279+
const result = modal.processContentWithMetadata(content);
280+
281+
expect(result).toBe("");
282+
});
283+
});
284+
285+
describe("Configuration Updates", () => {
286+
test("should update time parsing service when config changes", () => {
287+
const newConfig = { enabled: false };
288+
modal.timeParsingService.updateConfig(newConfig);
289+
290+
const config = modal.timeParsingService.getConfig();
291+
expect(config.enabled).toBe(false);
292+
});
293+
});
294+
});

0 commit comments

Comments
 (0)