import { signal } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { provideRouter, Router } from '@angular/router'; import { ProjectContextService } from '../projects/project-context.service'; import { vi } from 'vitest'; import { Dashboard } from './dashboard'; import { IssueEntity, IssuesStore } from '../issues/issues.store'; import { MilestoneEntity, MilestonesStore } from '../milestones/milestones.store'; import { StatusesStore } from '../statuses/statuses.store'; const makeIssue = (overrides: Partial = {}): IssueEntity => ({ id: 1, type: 'Story', assignee: '', epic: '', name: 'Test Issue', startDate: '', startDateMode: 'forced', endDate: '', dueDate: '', description: '', estimatedTime: null, dependsOnIds: [], comments: [], priority: 'MOYENNE', status: 'todo', progress: 0, ...overrides, }); class FakeIssuesStore { private _data = signal([]); readonly issues = this._data.asReadonly(); readonly loading = signal(false); readonly loaded = signal(true); seed(issues: IssueEntity[]): void { this._data.set(issues); } load = vi.fn().mockResolvedValue(undefined); getById = vi.fn(); getNextId = vi.fn().mockReturnValue(1); upsert = vi.fn(); deleteById = vi.fn(); } class FakeStatusesStore { private completedIds = new Set(['done']); setCompleted(ids: string[]): void { this.completedIds = new Set(ids); } isCompleted = vi.fn((statusId: string) => this.completedIds.has(statusId)); } class FakeMilestonesStore { private _data = signal([]); readonly milestones = this._data.asReadonly(); readonly loading = signal(false); readonly loaded = signal(true); seed(milestones: MilestoneEntity[]): void { this._data.set(milestones); } load = vi.fn().mockResolvedValue(undefined); upsert = vi.fn(); deleteById = vi.fn(); } describe('Dashboard', () => { let component: Dashboard; let fixture: ComponentFixture; let issuesStore: FakeIssuesStore; let milestonesStore: FakeMilestonesStore; let statusesStore: FakeStatusesStore; let router: Router; beforeEach(async () => { issuesStore = new FakeIssuesStore(); milestonesStore = new FakeMilestonesStore(); statusesStore = new FakeStatusesStore(); await TestBed.configureTestingModule({ imports: [Dashboard], providers: [ provideRouter([]), { provide: IssuesStore, useValue: issuesStore }, { provide: MilestonesStore, useValue: milestonesStore }, { provide: StatusesStore, useValue: statusesStore }, { provide: ProjectContextService, useValue: { projectId: signal(1), project: signal(null) } }, ], }).compileComponents(); router = TestBed.inject(Router); fixture = TestBed.createComponent(Dashboard); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); describe('totalIssues', () => { it('retourne 0 quand il n\'y a pas d\'issues', () => { expect((component as any).totalIssues()).toBe(0); }); it('retourne le nombre total d\'issues', () => { issuesStore.seed([makeIssue({ id: 1 }), makeIssue({ id: 2 }), makeIssue({ id: 3 })]); expect((component as any).totalIssues()).toBe(3); }); }); describe('completionRate', () => { it('retourne 0 quand il n\'y a pas d\'issues', () => { expect((component as any).completionRate()).toBe(0); }); it('retourne 0 quand aucune issue n\'est terminée', () => { issuesStore.seed([makeIssue({ status: 'todo' }), makeIssue({ status: 'in-progress' })]); expect((component as any).completionRate()).toBe(0); }); it('retourne 100 quand toutes les issues sont terminées', () => { issuesStore.seed([makeIssue({ status: 'done' }), makeIssue({ status: 'done' })]); expect((component as any).completionRate()).toBe(100); }); it('calcule le pourcentage correct', () => { issuesStore.seed([ makeIssue({ id: 1, status: 'done' }), makeIssue({ id: 2, status: 'done' }), makeIssue({ id: 3, status: 'todo' }), makeIssue({ id: 4, status: 'todo' }), ]); expect((component as any).completionRate()).toBe(50); }); it('compte un statut personnalisé countsAsCompleted comme terminé', () => { statusesStore.setCompleted(['done', 'abandoned']); issuesStore.seed([ makeIssue({ id: 1, status: 'done' }), makeIssue({ id: 2, status: 'abandoned' }), makeIssue({ id: 3, status: 'todo' }), makeIssue({ id: 4, status: 'todo' }), ]); expect((component as any).completionRate()).toBe(50); }); }); describe('statusCounts', () => { it('compte correctement chaque statut', () => { issuesStore.seed([ makeIssue({ id: 1, status: 'draft' }), makeIssue({ id: 2, status: 'todo' }), makeIssue({ id: 3, status: 'todo' }), makeIssue({ id: 4, status: 'in-progress' }), makeIssue({ id: 5, status: 'done' }), ]); const counts = (component as any).statusCounts(); expect(counts.draft).toBe(1); expect(counts.todo).toBe(2); expect(counts.inProgress).toBe(1); expect(counts.done).toBe(1); }); }); describe('overdueCount', () => { it('retourne 0 quand aucune issue n\'est en retard', () => { const future = new Date(Date.now() + 10 * 24 * 60 * 60 * 1000).toISOString(); issuesStore.seed([makeIssue({ dueDate: future, status: 'todo' })]); expect((component as any).overdueCount()).toBe(0); }); it('ne compte pas les issues terminées même dépassées', () => { const past = new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(); issuesStore.seed([makeIssue({ dueDate: past, status: 'done' })]); expect((component as any).overdueCount()).toBe(0); }); it('compte les issues non terminées avec date passée', () => { const past = new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(); issuesStore.seed([ makeIssue({ id: 1, dueDate: past, status: 'todo' }), makeIssue({ id: 2, dueDate: past, status: 'in-progress' }), ]); expect((component as any).overdueCount()).toBe(2); }); }); describe('highPriorityIssues', () => { it('retourne les issues HAUTE et TRES_HAUTE non terminées', () => { issuesStore.seed([ makeIssue({ id: 1, priority: 'TRES_HAUTE', status: 'todo' }), makeIssue({ id: 2, priority: 'HAUTE', status: 'in-progress' }), makeIssue({ id: 3, priority: 'MOYENNE', status: 'todo' }), ]); expect((component as any).highPriorityIssues().length).toBe(2); }); it('exclut les issues haute priorité terminées', () => { issuesStore.seed([makeIssue({ priority: 'TRES_HAUTE', status: 'done' })]); expect((component as any).highPriorityIssues().length).toBe(0); }); it('exclut les issues avec un statut personnalisé countsAsCompleted', () => { statusesStore.setCompleted(['done', 'abandoned']); issuesStore.seed([makeIssue({ priority: 'TRES_HAUTE', status: 'abandoned' })]); expect((component as any).highPriorityIssues().length).toBe(0); }); it('limite à 6 résultats', () => { issuesStore.seed( Array.from({ length: 10 }, (_, i) => makeIssue({ id: i + 1, priority: 'HAUTE', status: 'todo' }), ), ); expect((component as any).highPriorityIssues().length).toBe(6); }); }); describe('upcomingIssues', () => { it('retourne les issues avec échéance dans les 14 prochains jours', () => { const soon = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(); issuesStore.seed([makeIssue({ dueDate: soon, status: 'todo' })]); expect((component as any).upcomingIssues().length).toBe(1); }); it('exclut les issues terminées', () => { const soon = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(); issuesStore.seed([makeIssue({ dueDate: soon, status: 'done' })]); expect((component as any).upcomingIssues().length).toBe(0); }); it('exclut les issues sans date d\'échéance', () => { issuesStore.seed([makeIssue({ dueDate: '', status: 'todo' })]); expect((component as any).upcomingIssues().length).toBe(0); }); it('exclut les issues au-delà de 14 jours', () => { const farFuture = new Date(Date.now() + 20 * 24 * 60 * 60 * 1000).toISOString(); issuesStore.seed([makeIssue({ dueDate: farFuture, status: 'todo' })]); expect((component as any).upcomingIssues().length).toBe(0); }); }); describe('issuesByType', () => { it('regroupe correctement par type', () => { issuesStore.seed([ makeIssue({ id: 1, type: 'Bug' }), makeIssue({ id: 2, type: 'Bug' }), makeIssue({ id: 3, type: 'Story' }), ]); const byType = (component as any).issuesByType(); const bug = byType.find((t: any) => t.type === 'Bug'); const story = byType.find((t: any) => t.type === 'Story'); expect(bug?.count).toBe(2); expect(story?.count).toBe(1); }); it('exclut les types sans issues', () => { issuesStore.seed([makeIssue({ type: 'Bug' })]); const byType = (component as any).issuesByType(); expect(byType.every((t: any) => t.count > 0)).toBe(true); }); }); describe('activeMilestones', () => { it('exclut les milestones terminés à 100%', () => { issuesStore.seed([makeIssue({ id: 1, status: 'done' })]); milestonesStore.seed([{ id: 1, name: 'Done Milestone', description: '', startDate: '', endDate: '', dueDate: '', issueIds: [1], dependsOnIds: [] }]); expect((component as any).activeMilestones().length).toBe(0); }); it('inclut les milestones non terminés', () => { issuesStore.seed([ makeIssue({ id: 1, status: 'done' }), makeIssue({ id: 2, status: 'todo' }), ]); milestonesStore.seed([{ id: 1, name: 'Active', description: '', startDate: '', endDate: '', dueDate: '', issueIds: [1, 2], dependsOnIds: [] }]); expect((component as any).activeMilestones().length).toBe(1); }); }); describe('formatDate', () => { it('retourne "—" pour une chaîne vide', () => { expect((component as any).formatDate('')).toBe('—'); }); it('formate une date ISO en format français', () => { const result = (component as any).formatDate('2025-06-15T00:00:00.000Z'); expect(result).toMatch(/15\/06\/2025/); }); }); describe('navigation', () => { it('navigue vers /projects/:pid/issues/:id via openIssue', () => { const spy = vi.spyOn(router, 'navigate').mockResolvedValue(true); (component as any).openIssue(42); expect(spy).toHaveBeenCalledWith(['/projects', 1, 'issues', 42]); }); it('navigue vers /projects/:pid/milestones/:id via openMilestone', () => { const spy = vi.spyOn(router, 'navigate').mockResolvedValue(true); (component as any).openMilestone(7); expect(spy).toHaveBeenCalledWith(['/projects', 1, 'milestones', 7]); }); it('navigue vers /projects/:pid/issues via navigateToIssues', () => { const spy = vi.spyOn(router, 'navigate').mockResolvedValue(true); (component as any).navigateToIssues(); expect(spy).toHaveBeenCalledWith(['/projects', 1, 'issues']); }); it('navigue vers /projects/:pid/milestones via navigateToMilestones', () => { const spy = vi.spyOn(router, 'navigate').mockResolvedValue(true); (component as any).navigateToMilestones(); expect(spy).toHaveBeenCalledWith(['/projects', 1, 'milestones']); }); }); });