Files
Bonsai-webapp/src/app/dashboard/dashboard.spec.ts
T
2026-05-29 07:58:51 +02:00

292 lines
10 KiB
TypeScript

import { signal } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { provideRouter, Router } from '@angular/router';
import { vi } from 'vitest';
import { Dashboard } from './dashboard';
import { IssueEntity, IssuesStore } from '../issues/issues.store';
import { MilestoneEntity, MilestonesStore } from '../milestones/milestones.store';
const makeIssue = (overrides: Partial<IssueEntity> = {}): IssueEntity => ({
id: 1,
type: 'Story',
assignee: '',
epic: '',
name: 'Test Issue',
startDate: '',
endDate: '',
dueDate: '',
description: '',
estimatedTime: null,
dependsOnIds: [],
comments: [],
priority: 'MOYENNE',
status: 'todo',
progress: 0,
...overrides,
});
class FakeIssuesStore {
private _data = signal<IssueEntity[]>([]);
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 FakeMilestonesStore {
private _data = signal<MilestoneEntity[]>([]);
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<Dashboard>;
let issuesStore: FakeIssuesStore;
let milestonesStore: FakeMilestonesStore;
let router: Router;
beforeEach(async () => {
issuesStore = new FakeIssuesStore();
milestonesStore = new FakeMilestonesStore();
await TestBed.configureTestingModule({
imports: [Dashboard],
providers: [
provideRouter([]),
{ provide: IssuesStore, useValue: issuesStore },
{ provide: MilestonesStore, useValue: milestonesStore },
],
}).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);
});
});
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('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: '', dueDate: '', issueIds: [1] }]);
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: '', dueDate: '', issueIds: [1, 2] }]);
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 /issues/:id via openIssue', () => {
const spy = vi.spyOn(router, 'navigate').mockResolvedValue(true);
(component as any).openIssue(42);
expect(spy).toHaveBeenCalledWith(['/issues', 42]);
});
it('navigue vers /milestones/:id via openMilestone', () => {
const spy = vi.spyOn(router, 'navigate').mockResolvedValue(true);
(component as any).openMilestone(7);
expect(spy).toHaveBeenCalledWith(['/milestones', 7]);
});
it('navigue vers /issues via navigateToIssues', () => {
const spy = vi.spyOn(router, 'navigate').mockResolvedValue(true);
(component as any).navigateToIssues();
expect(spy).toHaveBeenCalledWith(['/issues']);
});
it('navigue vers /milestones via navigateToMilestones', () => {
const spy = vi.spyOn(router, 'navigate').mockResolvedValue(true);
(component as any).navigateToMilestones();
expect(spy).toHaveBeenCalledWith(['/milestones']);
});
});
});