Skip to content

Commit fb1c12c

Browse files
committed
fix: duplicate task in timeline view
1 parent 47e1292 commit fb1c12c

File tree

4 files changed

+398
-5
lines changed

4 files changed

+398
-5
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "task-genius",
3-
"version": "9.1.0-beta.3",
3+
"version": "9.1.0-beta.5",
44
"description": "Comprehensive task management plugin for Obsidian with progress bars, task status cycling, and advanced task tracking features.",
55
"main": "main.js",
66
"scripts": {
Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
import { Task } from '../types/task';
2+
import { TimelineSidebarView } from '../components/timeline-sidebar/TimelineSidebarView';
3+
4+
// Mock translations first
5+
jest.mock('../translations/helper', () => ({
6+
t: jest.fn((key: string) => key),
7+
}));
8+
9+
// Mock all Obsidian dependencies
10+
jest.mock('obsidian', () => {
11+
const actualMoment = jest.requireActual('moment');
12+
const mockMoment = jest.fn().mockImplementation((date?: any) => {
13+
return actualMoment(date);
14+
});
15+
// Add moment methods
16+
mockMoment.locale = jest.fn(() => 'en');
17+
mockMoment.format = actualMoment.format;
18+
19+
return {
20+
ItemView: class MockItemView {
21+
constructor(leaf: any) {}
22+
getViewType() { return 'mock'; }
23+
getDisplayText() { return 'Mock'; }
24+
getIcon() { return 'mock'; }
25+
},
26+
moment: mockMoment,
27+
setIcon: jest.fn(),
28+
debounce: jest.fn((fn: any) => fn),
29+
Component: class MockComponent {},
30+
ButtonComponent: class MockButtonComponent {},
31+
Platform: {},
32+
TFile: class MockTFile {},
33+
AbstractInputSuggest: class MockAbstractInputSuggest {},
34+
App: class MockApp {},
35+
Modal: class MockModal {},
36+
Setting: class MockSetting {},
37+
PluginSettingTab: class MockPluginSettingTab {},
38+
};
39+
});
40+
41+
// Mock other dependencies
42+
jest.mock('../components/QuickCaptureModal', () => ({
43+
QuickCaptureModal: class MockQuickCaptureModal {},
44+
}));
45+
46+
jest.mock('../editor-ext/markdownEditor', () => ({
47+
createEmbeddableMarkdownEditor: jest.fn(),
48+
}));
49+
50+
jest.mock('../utils/fileUtils', () => ({
51+
saveCapture: jest.fn(),
52+
}));
53+
54+
jest.mock('../components/task-view/details', () => ({
55+
createTaskCheckbox: jest.fn(),
56+
}));
57+
58+
jest.mock('../components/MarkdownRenderer', () => ({
59+
MarkdownRendererComponent: class MockMarkdownRendererComponent {},
60+
}));
61+
62+
const actualMoment = jest.requireActual('moment');
63+
const moment = actualMoment;
64+
65+
// Mock plugin and dependencies
66+
const mockPlugin = {
67+
taskManager: {
68+
getAllTasks: jest.fn(() => []),
69+
updateTask: jest.fn(),
70+
},
71+
settings: {
72+
timelineSidebar: {
73+
showCompletedTasks: true,
74+
},
75+
taskStatuses: {
76+
completed: 'x',
77+
notStarted: ' ',
78+
},
79+
quickCapture: {
80+
targetType: 'file',
81+
targetFile: 'test.md',
82+
},
83+
},
84+
app: {
85+
vault: {
86+
on: jest.fn(),
87+
getFileByPath: jest.fn(),
88+
},
89+
workspace: {
90+
on: jest.fn(),
91+
getLeavesOfType: jest.fn(() => []),
92+
getLeaf: jest.fn(),
93+
setActiveLeaf: jest.fn(),
94+
},
95+
},
96+
};
97+
98+
const mockLeaf = {
99+
view: null,
100+
};
101+
102+
describe('TimelineSidebarView Date Deduplication', () => {
103+
let timelineView: TimelineSidebarView;
104+
105+
beforeEach(() => {
106+
jest.clearAllMocks();
107+
timelineView = new TimelineSidebarView(mockLeaf as any, mockPlugin as any);
108+
});
109+
110+
// Helper function to create a mock task
111+
const createMockTask = (
112+
id: string,
113+
content: string,
114+
metadata: Partial<Task['metadata']> = {}
115+
): Task => ({
116+
id,
117+
content,
118+
filePath: 'test.md',
119+
line: 1,
120+
status: ' ',
121+
completed: false,
122+
metadata: {
123+
dueDate: undefined,
124+
scheduledDate: undefined,
125+
startDate: undefined,
126+
completedDate: undefined,
127+
tags: [],
128+
...metadata,
129+
},
130+
} as Task);
131+
132+
describe('deduplicateDatesByPriority', () => {
133+
it('should return empty array for empty input', () => {
134+
const result = (timelineView as any).deduplicateDatesByPriority([]);
135+
expect(result).toEqual([]);
136+
});
137+
138+
it('should return single date unchanged', () => {
139+
const dates = [{ date: new Date('2025-01-15'), type: 'due' }];
140+
const result = (timelineView as any).deduplicateDatesByPriority(dates);
141+
expect(result).toEqual(dates);
142+
});
143+
144+
it('should keep different dates on different days', () => {
145+
const dates = [
146+
{ date: new Date('2025-01-15'), type: 'due' },
147+
{ date: new Date('2025-01-16'), type: 'scheduled' },
148+
];
149+
const result = (timelineView as any).deduplicateDatesByPriority(dates);
150+
expect(result).toHaveLength(2);
151+
expect(result).toEqual(expect.arrayContaining(dates));
152+
});
153+
154+
it('should prioritize due over completed on same day', () => {
155+
const dates = [
156+
{ date: new Date('2025-01-15T10:00:00'), type: 'due' },
157+
{ date: new Date('2025-01-15T14:00:00'), type: 'completed' },
158+
];
159+
const result = (timelineView as any).deduplicateDatesByPriority(dates);
160+
expect(result).toHaveLength(1);
161+
expect(result[0].type).toBe('due');
162+
});
163+
164+
it('should prioritize due over scheduled on same day', () => {
165+
const dates = [
166+
{ date: new Date('2025-01-15T10:00:00'), type: 'scheduled' },
167+
{ date: new Date('2025-01-15T14:00:00'), type: 'due' },
168+
];
169+
const result = (timelineView as any).deduplicateDatesByPriority(dates);
170+
expect(result).toHaveLength(1);
171+
expect(result[0].type).toBe('due');
172+
});
173+
174+
it('should prioritize scheduled over start on same day', () => {
175+
const dates = [
176+
{ date: new Date('2025-01-15T10:00:00'), type: 'start' },
177+
{ date: new Date('2025-01-15T14:00:00'), type: 'scheduled' },
178+
];
179+
const result = (timelineView as any).deduplicateDatesByPriority(dates);
180+
expect(result).toHaveLength(1);
181+
expect(result[0].type).toBe('scheduled');
182+
});
183+
184+
it('should handle multiple date types with correct priority order', () => {
185+
const dates = [
186+
{ date: new Date('2025-01-15T08:00:00'), type: 'start' },
187+
{ date: new Date('2025-01-15T10:00:00'), type: 'scheduled' },
188+
{ date: new Date('2025-01-15T12:00:00'), type: 'due' },
189+
{ date: new Date('2025-01-15T16:00:00'), type: 'completed' },
190+
];
191+
const result = (timelineView as any).deduplicateDatesByPriority(dates);
192+
expect(result).toHaveLength(1);
193+
expect(result[0].type).toBe('due');
194+
});
195+
196+
it('should handle mixed same-day and different-day dates', () => {
197+
const dates = [
198+
{ date: new Date('2025-01-15T10:00:00'), type: 'due' },
199+
{ date: new Date('2025-01-15T14:00:00'), type: 'completed' },
200+
{ date: new Date('2025-01-16T10:00:00'), type: 'scheduled' },
201+
{ date: new Date('2025-01-17T10:00:00'), type: 'start' },
202+
];
203+
const result = (timelineView as any).deduplicateDatesByPriority(dates);
204+
expect(result).toHaveLength(3);
205+
206+
const jan15Result = result.find((d: any) => moment(d.date).format('YYYY-MM-DD') === '2025-01-15');
207+
const jan16Result = result.find((d: any) => moment(d.date).format('YYYY-MM-DD') === '2025-01-16');
208+
const jan17Result = result.find((d: any) => moment(d.date).format('YYYY-MM-DD') === '2025-01-17');
209+
210+
expect(jan15Result?.type).toBe('due');
211+
expect(jan16Result?.type).toBe('scheduled');
212+
expect(jan17Result?.type).toBe('start');
213+
});
214+
});
215+
216+
describe('extractDatesFromTask', () => {
217+
it('should return empty array for task with no dates', () => {
218+
const task = createMockTask('test-1', 'Test task');
219+
const result = (timelineView as any).extractDatesFromTask(task);
220+
expect(result).toEqual([]);
221+
});
222+
223+
it('should return single date for task with one date type', () => {
224+
const dueDate = new Date('2025-01-15').getTime();
225+
const task = createMockTask('test-1', 'Test task', { dueDate });
226+
const result = (timelineView as any).extractDatesFromTask(task);
227+
expect(result).toHaveLength(1);
228+
expect(result[0].type).toBe('due');
229+
});
230+
231+
// New tests for task-level deduplication
232+
describe('completed task behavior', () => {
233+
it('should return due date for completed task with due date', () => {
234+
const task = createMockTask('test-1', 'Test task', {
235+
dueDate: new Date('2025-01-15T10:00:00').getTime(),
236+
completedDate: new Date('2025-01-16T16:00:00').getTime(),
237+
});
238+
task.completed = true;
239+
const result = (timelineView as any).extractDatesFromTask(task);
240+
expect(result).toHaveLength(1);
241+
expect(result[0].type).toBe('due');
242+
});
243+
244+
it('should return completed date for completed task without due date', () => {
245+
const task = createMockTask('test-1', 'Test task', {
246+
scheduledDate: new Date('2025-01-14T10:00:00').getTime(),
247+
completedDate: new Date('2025-01-16T16:00:00').getTime(),
248+
});
249+
task.completed = true;
250+
const result = (timelineView as any).extractDatesFromTask(task);
251+
expect(result).toHaveLength(1);
252+
expect(result[0].type).toBe('completed');
253+
});
254+
255+
it('should always return due date for completed task regardless of other dates', () => {
256+
const task = createMockTask('test-1', 'Test task', {
257+
startDate: new Date('2025-01-13T08:00:00').getTime(),
258+
scheduledDate: new Date('2025-01-14T10:00:00').getTime(),
259+
dueDate: new Date('2025-01-15T12:00:00').getTime(),
260+
completedDate: new Date('2025-01-16T16:00:00').getTime(),
261+
});
262+
task.completed = true;
263+
const result = (timelineView as any).extractDatesFromTask(task);
264+
expect(result).toHaveLength(1);
265+
expect(result[0].type).toBe('due');
266+
});
267+
});
268+
269+
describe('non-completed task behavior', () => {
270+
it('should return highest priority date for non-completed task with multiple dates', () => {
271+
const task = createMockTask('test-1', 'Test task', {
272+
startDate: new Date('2025-01-13T08:00:00').getTime(),
273+
scheduledDate: new Date('2025-01-14T10:00:00').getTime(),
274+
dueDate: new Date('2025-01-15T12:00:00').getTime(),
275+
});
276+
const result = (timelineView as any).extractDatesFromTask(task);
277+
expect(result).toHaveLength(1);
278+
expect(result[0].type).toBe('due');
279+
});
280+
281+
it('should return scheduled date when no due date exists', () => {
282+
const task = createMockTask('test-1', 'Test task', {
283+
startDate: new Date('2025-01-13T08:00:00').getTime(),
284+
scheduledDate: new Date('2025-01-14T10:00:00').getTime(),
285+
});
286+
const result = (timelineView as any).extractDatesFromTask(task);
287+
expect(result).toHaveLength(1);
288+
expect(result[0].type).toBe('scheduled');
289+
});
290+
291+
it('should return start date when only start date exists', () => {
292+
const task = createMockTask('test-1', 'Test task', {
293+
startDate: new Date('2025-01-13T08:00:00').getTime(),
294+
});
295+
const result = (timelineView as any).extractDatesFromTask(task);
296+
expect(result).toHaveLength(1);
297+
expect(result[0].type).toBe('start');
298+
});
299+
});
300+
});
301+
});

0 commit comments

Comments
 (0)