Ajoute du tableau de bord
Signed-off-by: Gato <cedric@goutailler-olivier.fr>
This commit is contained in:
@@ -0,0 +1,289 @@
|
||||
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',
|
||||
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']);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user