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