2929import { ComponentFixture , TestBed } from '@angular/core/testing' ;
3030import { I18nService } from 'core-app/core/i18n/i18n.service' ;
3131import { TimezoneService } from 'core-app/core/datetime/timezone.service' ;
32+ import { PathHelperService } from 'core-app/core/path-helper/path-helper.service' ;
3233import { ProjectTimelineItem , ProjectTimelineGraphComponent } from './project-timeline-graph.component' ;
3334
3435describe ( 'ProjectTimelineGraphComponent' , ( ) => {
@@ -37,6 +38,7 @@ describe('ProjectTimelineGraphComponent', () => {
3738 return {
3839 'js.grid.widgets.project_timeline.tooltip_type_phase' : 'Phase' ,
3940 'js.grid.widgets.project_timeline.tooltip_type_gate' : 'Gate' ,
41+ 'js.grid.widgets.project_timeline.tooltip_type_milestone' : 'Milestone' ,
4042 } [ key ] ?? key ;
4143 } ,
4244 } ;
@@ -93,10 +95,18 @@ describe('ProjectTimelineGraphComponent', () => {
9395 finishGateName : null ,
9496 } ;
9597
98+ const milestone = {
99+ id : 10 ,
100+ subject : 'Launch' ,
101+ date : '2024-06-30' ,
102+ typeId : 7 ,
103+ } ;
104+
96105 let fixture :ComponentFixture < ProjectTimelineGraphComponent > ;
97106 let component :ProjectTimelineGraphComponent ;
98107
99- let buildData :( phases :unknown [ ] ) => { items :ProjectTimelineItem [ ] ; groups :{ id :string ; content :string } [ ] } ;
108+ // eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-assignment
109+ let buildData :( phases :unknown [ ] , milestones :unknown [ ] ) => { items :ProjectTimelineItem [ ] ; groups :{ id :string ; content :string } [ ] } ;
100110 let tooltipTemplate :( item :ProjectTimelineItem ) => HTMLElement | string ;
101111
102112 beforeEach ( async ( ) => {
@@ -105,14 +115,16 @@ describe('ProjectTimelineGraphComponent', () => {
105115 providers : [
106116 { provide : I18nService , useValue : i18nStub } ,
107117 { provide : TimezoneService , useValue : timezoneStub } ,
118+ { provide : PathHelperService , useValue : { workPackagePath : ( id :string ) => `/work_packages/${ id } ` } } ,
108119 ] ,
109120 } ) . compileComponents ( ) ;
110121
111122 fixture = TestBed . createComponent ( ProjectTimelineGraphComponent ) ;
112123 component = fixture . componentInstance ;
113124
114- // Set required input before detectChanges triggers ngAfterViewInit
125+ // Set required inputs before detectChanges triggers ngAfterViewInit
115126 fixture . componentRef . setInput ( 'phasesData' , '[]' ) ;
127+ fixture . componentRef . setInput ( 'milestonesData' , '[]' ) ;
116128 fixture . detectChanges ( ) ;
117129
118130 // eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-assignment
@@ -123,31 +135,33 @@ describe('ProjectTimelineGraphComponent', () => {
123135
124136 describe ( 'buildData' , ( ) => {
125137 it ( 'creates a range item for a phase with dates' , ( ) => {
126- const { items } = buildData ( [ phaseWithDates ] ) ;
138+ const { items } = buildData ( [ phaseWithDates ] , [ ] ) ;
127139 const item = items . find ( ( i ) => i . id === 'phase-1' ) ;
128140
129141 expect ( item ) . toBeDefined ( ) ;
130142 expect ( item ! . type ) . toBe ( 'range' ) ;
131- expect ( item ! . group ) . toBe ( 'lifecycle ' ) ;
143+ expect ( item ! . group ) . toBe ( 'phases ' ) ;
132144 expect ( item ! . content ) . toBe ( 'Design' ) ;
133145 expect ( item ! . className ) . toContain ( '__hl_background_project_phase_definition_3' ) ;
146+ expect ( item ! . itemType ) . toBe ( 'phase' ) ;
134147 expect ( item ! . definitionId ) . toBe ( 3 ) ;
135148 } ) ;
136149
137150 it ( 'skips a phase without dates' , ( ) => {
138- const { items } = buildData ( [ phaseWithoutDates ] ) ;
151+ const { items } = buildData ( [ phaseWithoutDates ] , [ ] ) ;
139152 expect ( items . find ( ( i ) => i . id === 'phase-3' ) ) . toBeUndefined ( ) ;
140153 } ) ;
141154
142155 it ( 'creates start and finish gate items when configured' , ( ) => {
143- const { items } = buildData ( [ phaseWithGates ] ) ;
156+ const { items } = buildData ( [ phaseWithGates ] , [ ] ) ;
144157
145158 const startGate = items . find ( ( i ) => i . id === 'gate-start-2' ) ;
146159 expect ( startGate ) . toBeDefined ( ) ;
147160 expect ( startGate ! . type ) . toBe ( 'point' ) ;
148161 expect ( startGate ! . group ) . toBe ( 'gates' ) ;
149162 expect ( startGate ! . title ) . toBe ( 'Build Start' ) ;
150163 expect ( startGate ! . className ) . toContain ( 'op-timeline-gate' ) ;
164+ expect ( startGate ! . itemType ) . toBe ( 'gate' ) ;
151165 expect ( startGate ! . content instanceof HTMLElement ) . toBe ( true ) ;
152166 expect ( ( startGate ! . content as HTMLElement ) . querySelector ( '.__hl_inline_project_phase_definition_5' ) ) . toBeTruthy ( ) ;
153167
@@ -157,7 +171,7 @@ describe('ProjectTimelineGraphComponent', () => {
157171 } ) ;
158172
159173 it ( 'does not create gate items when gates are disabled' , ( ) => {
160- const { items } = buildData ( [ phaseWithDates ] ) ;
174+ const { items } = buildData ( [ phaseWithDates ] , [ ] ) ;
161175 expect ( items . find ( ( i ) => i . id === 'gate-start-1' ) ) . toBeUndefined ( ) ;
162176 expect ( items . find ( ( i ) => i . id === 'gate-finish-1' ) ) . toBeUndefined ( ) ;
163177 } ) ;
@@ -166,7 +180,7 @@ describe('ProjectTimelineGraphComponent', () => {
166180 let item :ProjectTimelineItem ;
167181
168182 beforeEach ( ( ) => {
169- ( { items : [ item ] } = buildData ( [ oneDayPhase ] ) ) ;
183+ ( { items : [ item ] } = buildData ( [ oneDayPhase ] , [ ] ) ) ;
170184 } ) ;
171185
172186 it ( 'creates a range item' , ( ) => {
@@ -193,9 +207,22 @@ describe('ProjectTimelineGraphComponent', () => {
193207 } ) ;
194208 } ) ;
195209
196- it ( 'always creates gates and lifecycle groups' , ( ) => {
197- const { groups } = buildData ( [ ] ) ;
198- expect ( groups . map ( ( g ) => g . id ) ) . toEqual ( [ 'gates' , 'lifecycle' ] ) ;
210+ it ( 'creates a milestone item' , ( ) => {
211+ const { items } = buildData ( [ ] , [ milestone ] ) ;
212+ const item = items . find ( ( i ) => i . id === 'milestone-10' ) ;
213+
214+ expect ( item ) . toBeDefined ( ) ;
215+ expect ( item ! . type ) . toBe ( 'point' ) ;
216+ expect ( item ! . group ) . toBe ( 'milestones' ) ;
217+ expect ( item ! . title ) . toBe ( 'Launch' ) ;
218+ expect ( item ! . className ) . toContain ( 'op-timeline-milestone' ) ;
219+ expect ( item ! . className ) . toContain ( '__hl_background_type_7' ) ;
220+ expect ( item ! . itemType ) . toBe ( 'milestone' ) ;
221+ } ) ;
222+
223+ it ( 'always creates gates, lifecycle, and milestones groups' , ( ) => {
224+ const { groups } = buildData ( [ ] , [ ] ) ;
225+ expect ( groups . map ( ( g ) => g . id ) ) . toEqual ( [ 'gates' , 'phases' , 'milestones' ] ) ;
199226 } ) ;
200227 } ) ;
201228
@@ -218,6 +245,7 @@ describe('ProjectTimelineGraphComponent', () => {
218245 content : 'Design' ,
219246 title : 'Design' ,
220247 className : '__hl_background_project_phase_definition_3' ,
248+ itemType : 'phase' ,
221249 definitionId : 3 ,
222250 } ) as HTMLElement ;
223251 } ) ;
@@ -282,6 +310,7 @@ describe('ProjectTimelineGraphComponent', () => {
282310 content : document . createElement ( 'i' ) ,
283311 title : 'Build Start' ,
284312 className : 'op-timeline-gate __hl_background_project_phase_definition_5' ,
313+ itemType : 'gate' ,
285314 definitionId : 5 ,
286315 } ) as HTMLElement ;
287316 } ) ;
@@ -305,5 +334,42 @@ describe('ProjectTimelineGraphComponent', () => {
305334 expect ( result . querySelector ( '.__hl_inline_project_phase_definition_5' ) ) . toBeTruthy ( ) ;
306335 } ) ;
307336 } ) ;
337+
338+ describe ( 'for a milestone item' , ( ) => {
339+ let result :HTMLElement ;
340+
341+ beforeEach ( ( ) => {
342+ result = tooltipTemplate ( {
343+ id : 'milestone-10' ,
344+ group : 'milestones' ,
345+ type : 'point' ,
346+ start : new Date ( '2024-06-30' ) ,
347+ content : '' ,
348+ title : 'Launch' ,
349+ className : 'op-timeline-milestone __hl_background_type_7' ,
350+ itemType : 'milestone' ,
351+ typeId : 7 ,
352+ } ) as HTMLElement ;
353+ } ) ;
354+
355+ it ( 'shows "Milestone" as the type label' , ( ) => {
356+ const meta = result . querySelector ( '.op-timeline-tooltip--meta-row' ) ;
357+ expect ( meta ?. textContent ) . toContain ( 'Milestone' ) ;
358+ } ) ;
359+
360+ it ( 'shows only a single date (no range)' , ( ) => {
361+ const meta = result . querySelector ( '.op-timeline-tooltip--meta-row' ) ;
362+ expect ( meta ?. textContent ) . not . toContain ( '–' ) ;
363+ } ) ;
364+
365+ it ( 'shows the milestone name' , ( ) => {
366+ const name = result . querySelector ( '.op-timeline-tooltip--name' ) ;
367+ expect ( name ?. textContent ) . toBe ( 'Launch' ) ;
368+ } ) ;
369+
370+ it ( 'applies the type highlight class to the icon' , ( ) => {
371+ expect ( result . querySelector ( '.__hl_inline_type_7' ) ) . toBeTruthy ( ) ;
372+ } ) ;
373+ } ) ;
308374 } ) ;
309375} ) ;
0 commit comments