dd-timer/tests/unit/application/queries.test.ts
POL Mickaël 3e485fd09b chore: normalise fins de ligne CRLF → LF dans tout le repo
Applique .gitattributes sur tous les fichiers existants.
Élimine les différences fantômes entre WSL et Windows.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 08:55:10 +02:00

674 lines
30 KiB
TypeScript
Executable File

import { describe, it, expect } from 'vitest';
import type { AppState } from '@domain/ports/StateRepository';
import { createEnclos, addDragodinde } from '@domain/entities/Enclos';
import { createGetDashboardHandler } from '@application/queries/GetDashboard';
import { createGetEnclosDetailHandler } from '@application/queries/GetEnclosDetail';
import { createGetTimerStateHandler } from '@application/queries/GetTimerState';
import { createGetBreedingOptionsHandler } from '@application/queries/GetBreedingOptions';
import { createGetReapproTreeHandler } from '@application/queries/GetReapproTree';
import { createGetInventaireHandler } from '@application/queries/GetInventaire';
import { createGetSettingsHandler } from '@application/queries/GetSettings';
import { createGetStatisticsHandler, TOTAL_RACES } from '@application/queries/GetStatistics';
import { createGetWorkflowsHandler } from '@application/queries/GetWorkflows';
function makeState(): AppState {
let enc = createEnclos(1);
enc = addDragodinde(enc);
return {
enclos: [enc], activeId: 1, nextEnclosId: 2,
alarmSound: 'arpege', notifsEnabled: true, ntfyTopic: '',
archivedStats: [], inventaire: {}, workflows: [],
accouplements: [
{ parentA: 'Rousse', parentB: 'Dorée', baby: 'Dorée et Rousse', gen: 2, couples: 5, babiesObtained: 3, date: '2026-01-01' },
],
};
}
describe('GetDashboard', () => {
it('aggregates stats', () => {
const state = makeState();
const handler = createGetDashboardHandler(state);
const r = handler({ type: 'get-dashboard' });
expect(r.totalCouples).toBe(5);
expect(r.totalBabies).toBe(3);
expect(r.enclosSummaries).toHaveLength(1);
expect(r.successRate).toBe(60);
expect(r.raceBreakdown['Dorée et Rousse']).toBe(3);
});
it('handles empty state', () => {
const state = makeState();
state.accouplements = [];
const r = createGetDashboardHandler(state)({ type: 'get-dashboard' });
expect(r.totalCouples).toBe(0);
expect(r.successRate).toBe(0);
});
});
describe('GetEnclosDetail', () => {
it('returns enclos by id', () => {
const state = makeState();
const handler = createGetEnclosDetailHandler(state);
const r = handler({ type: 'get-enclos-detail', enclosId: 1 });
expect(r).not.toBeNull();
expect(r!.id).toBe(1);
});
it('returns null for unknown id', () => {
const state = makeState();
const r = createGetEnclosDetailHandler(state)({ type: 'get-enclos-detail', enclosId: 999 });
expect(r).toBeNull();
});
});
describe('GetTimerState', () => {
it('returns timer state', () => {
const state = makeState();
const r = createGetTimerStateHandler(state)({ type: 'get-timer-state', enclosId: 1 });
expect(r).not.toBeNull();
expect(r!.running).toBe(false);
});
it('returns null for unknown enclos', () => {
const state = makeState();
const r = createGetTimerStateHandler(state)({ type: 'get-timer-state', enclosId: 999 });
expect(r).toBeNull();
});
});
describe('GetBreedingOptions', () => {
it('returns partners for Rousse', () => {
const handler = createGetBreedingOptionsHandler();
const r = handler({ type: 'get-breeding-options', race: 'Rousse' });
expect(r.partners.length).toBeGreaterThan(0);
expect(r.partners.some(p => p.partner === 'Dorée')).toBe(true);
});
});
describe('GetReapproTree', () => {
it('computes breeding plan', () => {
const handler = createGetReapproTreeHandler();
const r = handler({ type: 'get-reappro-tree', target: 'Dorée et Rousse', qty: 4, repro: {}, inverted: {} });
expect(r.steps).toHaveLength(1);
expect(r.totalGen1).toBe(8);
});
});
describe('GetInventaire', () => {
it('retourne le stock brut de l\'inventaire', () => {
const state = makeState();
state.inventaire = { Rousse: { m: 1, f: 0 }, Dorée: { m: 0, f: 1 } };
const handler = createGetInventaireHandler(state);
const r = handler({ type: 'get-inventaire' });
expect(r).toEqual({ Rousse: { m: 1, f: 0 }, Dorée: { m: 0, f: 1 } });
});
it('inventaire vide retourne objet vide', () => {
const state = makeState();
const r = createGetInventaireHandler(state)({ type: 'get-inventaire' });
expect(r).toEqual({});
});
it('reflète les changements de state.inventaire en temps réel', () => {
const state = makeState();
const handler = createGetInventaireHandler(state);
expect(handler({ type: 'get-inventaire' })).toEqual({});
state.inventaire = { Amande: { m: 3, f: 2 } };
expect(handler({ type: 'get-inventaire' })).toEqual({ Amande: { m: 3, f: 2 } });
});
});
// ── Helpers pour les dates relatives ──────────────────────────────
function daysAgo(n: number): string {
const d = new Date();
d.setDate(d.getDate() - n);
return d.toISOString().slice(0, 10);
}
function dayOfWeekName(dateStr: string): string {
const names = ['Dimanche', 'Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi'];
const d = new Date(dateStr + 'T12:00:00');
return names[d.getDay()];
}
// ══════════════════════════════════════════════════════════════════
// GetDashboard — archivedStats path (lignes 44-51)
// ══════════════════════════════════════════════════════════════════
describe('GetDashboard — archivedStats', () => {
it('agrège accouplements + archivedStats', () => {
const state = makeState();
state.archivedStats = [
{ baby: 'Ebène', couples: 10, babiesObtained: 4 },
{ baby: 'Dorée et Rousse', couples: 3, babiesObtained: 1 },
];
const r = createGetDashboardHandler(state)({ type: 'get-dashboard' });
// accouplements: 5 couples, 3 babies + archived: 13 couples, 5 babies
expect(r.totalCouples).toBe(18);
expect(r.totalBabies).toBe(8);
expect(r.raceBreakdown['Ebène']).toBe(4);
expect(r.raceBreakdown['Dorée et Rousse']).toBe(4); // 3 + 1
expect(r.successRate).toBe(44); // Math.round(8/18*100)
});
it('ignore les entrées archivedStats sans baby', () => {
const state = makeState();
state.archivedStats = [
{ couples: 10, babiesObtained: 4 }, // pas de baby
];
const r = createGetDashboardHandler(state)({ type: 'get-dashboard' });
expect(r.totalCouples).toBe(5);
expect(r.totalBabies).toBe(3);
});
it('gère les champs manquants dans archivedStats', () => {
const state = makeState();
state.accouplements = [];
state.archivedStats = [
{ baby: 'Indigo' }, // couples et babiesObtained absents → default 0
];
const r = createGetDashboardHandler(state)({ type: 'get-dashboard' });
expect(r.totalCouples).toBe(0);
expect(r.totalBabies).toBe(0);
expect(r.raceBreakdown['Indigo']).toBe(0);
expect(r.successRate).toBe(0);
});
});
// ══════════════════════════════════════════════════════════════════
// GetSettings
// ══════════════════════════════════════════════════════════════════
describe('GetSettings', () => {
it('retourne les paramètres courants', () => {
const state = makeState();
const r = createGetSettingsHandler(state)({ type: 'get-settings' });
expect(r.alarmSound).toBe('arpege');
expect(r.notifsEnabled).toBe(true);
expect(r.ntfyTopic).toBe('');
});
it('reflète les changements de paramètres en temps réel', () => {
const state = makeState();
const handler = createGetSettingsHandler(state);
state.alarmSound = 'gong';
state.notifsEnabled = false;
state.ntfyTopic = 'mon-topic';
const r = handler({ type: 'get-settings' });
expect(r.alarmSound).toBe('gong');
expect(r.notifsEnabled).toBe(false);
expect(r.ntfyTopic).toBe('mon-topic');
});
});
// ══════════════════════════════════════════════════════════════════
// GetWorkflows
// ══════════════════════════════════════════════════════════════════
describe('GetWorkflows', () => {
it('retourne la liste des workflows', () => {
const state = makeState();
const wf = {
id: 1, name: 'Test WF', target: 'Ebène', qty: 2, createdAt: Date.now(),
materials: [{ name: 'Rousse', needed: 4, done: 2 }],
steps: [{ gen: 2, crossings: [{ race: 'Dorée et Rousse', needed: 2, parentA: 'Dorée', parentB: 'Rousse', couples: 2, repro: 1, done: 1 }] }],
};
state.workflows = [wf];
const r = createGetWorkflowsHandler(state)({ type: 'get-workflows' });
expect(r).toHaveLength(1);
expect(r[0]).toBe(wf);
});
it('retourne un tableau vide quand aucun workflow', () => {
const state = makeState();
const r = createGetWorkflowsHandler(state)({ type: 'get-workflows' });
expect(r).toEqual([]);
});
it('reflète les changements en temps réel', () => {
const state = makeState();
const handler = createGetWorkflowsHandler(state);
expect(handler({ type: 'get-workflows' })).toEqual([]);
state.workflows = [{ id: 42, name: 'WF2', target: 'Indigo', qty: 1, createdAt: 0, materials: [], steps: [] }];
expect(handler({ type: 'get-workflows' })).toHaveLength(1);
});
});
// ══════════════════════════════════════════════════════════════════
// GetStatistics
// ══════════════════════════════════════════════════════════════════
describe('GetStatistics', () => {
// ── Historique vide ────────────────────────────────────────────
it('historique vide retourne des KPI à zéro', () => {
const state = makeState();
state.accouplements = [];
const r = createGetStatisticsHandler(state)({ type: 'get-statistics', days: 0 });
expect(r.totalBabies.value).toBe(0);
expect(r.totalCouples.value).toBe(0);
expect(r.successRate.value).toBe(0);
expect(r.racesCount.value).toBe(0);
expect(r.raceShares).toEqual([]);
expect(r.raceSuccessRates).toEqual([]);
expect(r.bestCouples).toEqual([]);
expect(r.genBreakdown).toEqual([]);
expect(r.weekdayActivity).toHaveLength(7);
expect(r.weekdayActivity.every(w => w.count === 0)).toBe(true);
});
// ── days=0 → tout l'historique, delta=null ──────────────────
it('days=0 retourne tout l\'historique sans delta', () => {
const state = makeState();
state.accouplements = [
{ parentA: 'Rousse', parentB: 'Dorée', baby: 'Dorée et Rousse', gen: 2, couples: 10, babiesObtained: 6, date: '2025-06-15' },
{ parentA: 'Amande', parentB: 'Dorée', baby: 'Amande et Dorée', gen: 2, couples: 8, babiesObtained: 3, date: '2025-01-10' },
];
const r = createGetStatisticsHandler(state)({ type: 'get-statistics', days: 0 });
expect(r.days).toBe(0);
expect(r.totalBabies).toEqual({ value: 9, delta: null });
expect(r.totalCouples).toEqual({ value: 18, delta: null });
expect(r.successRate).toEqual({ value: 50, delta: null });
expect(r.racesCount).toEqual({ value: 2, delta: null });
});
// ── Exclusion Gen 1 ────────────────────────────────────────────
it('exclut les entrées Gen 1 (Rousse, Dorée, Amande)', () => {
const state = makeState();
state.accouplements = [
{ parentA: 'X', parentB: 'Y', baby: 'Rousse', gen: 1, couples: 100, babiesObtained: 50, date: daysAgo(1) },
{ parentA: 'Rousse', parentB: 'Dorée', baby: 'Dorée et Rousse', gen: 2, couples: 4, babiesObtained: 2, date: daysAgo(1) },
];
const r = createGetStatisticsHandler(state)({ type: 'get-statistics', days: 0 });
expect(r.totalCouples.value).toBe(4);
expect(r.totalBabies.value).toBe(2);
});
it('exclut les entrées dont le baby est Gen 1 même si gen != 1', () => {
const state = makeState();
state.accouplements = [
{ parentA: 'A', parentB: 'B', baby: 'Amande', gen: 0, couples: 20, babiesObtained: 10, date: daysAgo(1) },
{ parentA: 'Rousse', parentB: 'Dorée', baby: 'Dorée et Rousse', gen: 2, couples: 5, babiesObtained: 3, date: daysAgo(1) },
];
const r = createGetStatisticsHandler(state)({ type: 'get-statistics', days: 0 });
expect(r.totalCouples.value).toBe(5);
expect(r.totalBabies.value).toBe(3);
});
// ── Filtrage par période avec delta ─────────────────────────
it('days=7 filtre la période courante et calcule le delta', () => {
const state = makeState();
state.accouplements = [
// Période courante (7 derniers jours)
{ parentA: 'Rousse', parentB: 'Dorée', baby: 'Dorée et Rousse', gen: 2, couples: 10, babiesObtained: 6, date: daysAgo(2) },
// Période précédente (7 à 14 jours)
{ parentA: 'Amande', parentB: 'Dorée', baby: 'Amande et Dorée', gen: 2, couples: 8, babiesObtained: 2, date: daysAgo(10) },
// Trop ancien (hors des deux fenêtres)
{ parentA: 'Rousse', parentB: 'Dorée', baby: 'Dorée et Rousse', gen: 2, couples: 100, babiesObtained: 50, date: '2020-01-01' },
];
const r = createGetStatisticsHandler(state)({ type: 'get-statistics', days: 7 });
expect(r.days).toBe(7);
expect(r.totalBabies.value).toBe(6);
expect(r.totalBabies.delta).toBe(6 - 2); // cur - prev
expect(r.totalCouples.value).toBe(10);
expect(r.totalCouples.delta).toBe(10 - 8);
});
// ── Default days=30 ─────────────────────────────────────────
it('days par défaut est 30', () => {
const state = makeState();
state.accouplements = [];
const r = createGetStatisticsHandler(state)({ type: 'get-statistics' });
expect(r.days).toBe(30);
});
// ── archivedStats agrégation ──────────────────────────────────
it('inclut les archivedStats dans les statistiques', () => {
const state = makeState();
state.accouplements = [];
state.archivedStats = [
{ parentA: 'Rousse', parentB: 'Dorée', baby: 'Dorée et Rousse', gen: 2, couples: 7, babiesObtained: 4, date: daysAgo(1) },
];
const r = createGetStatisticsHandler(state)({ type: 'get-statistics', days: 0 });
expect(r.totalCouples.value).toBe(7);
expect(r.totalBabies.value).toBe(4);
});
it('exclut les archivedStats Gen 1', () => {
const state = makeState();
state.accouplements = [];
state.archivedStats = [
{ baby: 'Rousse', gen: 1, couples: 50, babiesObtained: 25, date: daysAgo(1) },
{ baby: 'Dorée et Rousse', gen: 2, couples: 3, babiesObtained: 1, date: daysAgo(1) },
];
const r = createGetStatisticsHandler(state)({ type: 'get-statistics', days: 0 });
expect(r.totalCouples.value).toBe(3);
expect(r.totalBabies.value).toBe(1);
});
it('archivedStats avec champs manquants utilise des défauts', () => {
const state = makeState();
state.accouplements = [];
state.archivedStats = [
{ baby: 'Ebène' }, // gen, couples, babiesObtained, date, parentA, parentB absents
];
const r = createGetStatisticsHandler(state)({ type: 'get-statistics', days: 0 });
// Ebène est Gen 3, ne sera pas exclu
expect(r.totalCouples.value).toBe(0);
expect(r.totalBabies.value).toBe(0);
});
it('ignore les archivedStats sans baby', () => {
const state = makeState();
state.accouplements = [];
state.archivedStats = [
{ couples: 10, babiesObtained: 5 },
];
const r = createGetStatisticsHandler(state)({ type: 'get-statistics', days: 0 });
expect(r.totalCouples.value).toBe(0);
expect(r.totalBabies.value).toBe(0);
});
// ── Répartition des races (raceShares) ─────────────────────
it('calcule la répartition des races triée par count décroissant', () => {
const state = makeState();
state.accouplements = [
{ parentA: 'Rousse', parentB: 'Dorée', baby: 'Dorée et Rousse', gen: 2, couples: 10, babiesObtained: 7, date: daysAgo(1) },
{ parentA: 'Amande', parentB: 'Dorée', baby: 'Amande et Dorée', gen: 2, couples: 5, babiesObtained: 3, date: daysAgo(1) },
];
const r = createGetStatisticsHandler(state)({ type: 'get-statistics', days: 0 });
expect(r.raceShares).toHaveLength(2);
expect(r.raceShares[0].race).toBe('Dorée et Rousse');
expect(r.raceShares[0].count).toBe(7);
expect(r.raceShares[0].pct).toBe(70); // 7/10*100
expect(r.raceShares[1].race).toBe('Amande et Dorée');
expect(r.raceShares[1].count).toBe(3);
expect(r.raceShares[1].pct).toBe(30);
});
// ── Taux de réussite par race ──────────────────────────────
it('calcule le taux de réussite par race', () => {
const state = makeState();
state.accouplements = [
{ parentA: 'Rousse', parentB: 'Dorée', baby: 'Dorée et Rousse', gen: 2, couples: 10, babiesObtained: 8, date: daysAgo(1) },
{ parentA: 'Amande', parentB: 'Dorée', baby: 'Amande et Dorée', gen: 2, couples: 10, babiesObtained: 3, date: daysAgo(1) },
];
const r = createGetStatisticsHandler(state)({ type: 'get-statistics', days: 0 });
expect(r.raceSuccessRates).toHaveLength(2);
// Trié par rate décroissant
expect(r.raceSuccessRates[0].race).toBe('Dorée et Rousse');
expect(r.raceSuccessRates[0].rate).toBe(80);
expect(r.raceSuccessRates[1].race).toBe('Amande et Dorée');
expect(r.raceSuccessRates[1].rate).toBe(30);
});
// ── Meilleurs couples ──────────────────────────────────────
it('calcule les meilleurs couples et normalise l\'ordre A|B', () => {
const state = makeState();
state.accouplements = [
{ parentA: 'Rousse', parentB: 'Dorée', baby: 'Dorée et Rousse', gen: 2, couples: 5, babiesObtained: 4, date: daysAgo(1) },
{ parentA: 'Dorée', parentB: 'Rousse', baby: 'Dorée et Rousse', gen: 2, couples: 5, babiesObtained: 3, date: daysAgo(2) },
];
const r = createGetStatisticsHandler(state)({ type: 'get-statistics', days: 0 });
// Les deux entrées doivent être fusionnées (clé normalisée)
expect(r.bestCouples).toHaveLength(1);
expect(r.bestCouples[0].couples).toBe(10);
expect(r.bestCouples[0].babies).toBe(7);
expect(r.bestCouples[0].rate).toBe(70);
});
it('ignore les couples sans parentA ou parentB', () => {
const state = makeState();
state.accouplements = [
{ parentA: '', parentB: 'Dorée', baby: 'Dorée et Rousse', gen: 2, couples: 5, babiesObtained: 3, date: daysAgo(1) },
];
const r = createGetStatisticsHandler(state)({ type: 'get-statistics', days: 0 });
expect(r.bestCouples).toEqual([]);
});
it('limite les meilleurs couples à 10', () => {
const state = makeState();
state.accouplements = [];
// Créer 12 couples distincts
const races = [
'Dorée et Rousse', 'Amande et Rousse', 'Amande et Dorée',
'Ebène', 'Indigo',
'Indigo et Rousse', 'Ebène et Rousse', 'Amande et Indigo',
'Amande et Ebène', 'Dorée et Indigo', 'Dorée et Ebène',
'Ebène et Indigo',
];
for (let i = 0; i < 12; i++) {
state.accouplements.push({
parentA: `ParentA${i}`, parentB: `ParentB${i}`, baby: races[i],
gen: i < 3 ? 2 : i < 5 ? 3 : 4,
couples: 5, babiesObtained: 3, date: daysAgo(1),
});
}
const r = createGetStatisticsHandler(state)({ type: 'get-statistics', days: 0 });
expect(r.bestCouples).toHaveLength(10);
});
// ── Répartition par génération ──────────────────────────────
it('calcule la répartition par génération', () => {
const state = makeState();
state.accouplements = [
{ parentA: 'Rousse', parentB: 'Dorée', baby: 'Dorée et Rousse', gen: 2, couples: 10, babiesObtained: 5, date: daysAgo(1) },
{ parentA: 'Amande', parentB: 'Dorée', baby: 'Amande et Dorée', gen: 2, couples: 8, babiesObtained: 3, date: daysAgo(1) },
{ parentA: 'Amande et Dorée', parentB: 'Dorée et Rousse', baby: 'Ebène', gen: 3, couples: 6, babiesObtained: 2, date: daysAgo(1) },
];
const r = createGetStatisticsHandler(state)({ type: 'get-statistics', days: 0 });
expect(r.genBreakdown).toHaveLength(2);
const gen2 = r.genBreakdown.find(g => g.gen === 2)!;
const gen3 = r.genBreakdown.find(g => g.gen === 3)!;
expect(gen2.babies).toBe(8);
expect(gen2.couples).toBe(18);
expect(gen2.races).toBe(2);
expect(gen3.babies).toBe(2);
expect(gen3.couples).toBe(6);
expect(gen3.races).toBe(1);
});
it('genBreakdown trié par gen croissant', () => {
const state = makeState();
state.accouplements = [
{ parentA: 'A', parentB: 'B', baby: 'Ebène', gen: 3, couples: 5, babiesObtained: 2, date: daysAgo(1) },
{ parentA: 'C', parentB: 'D', baby: 'Dorée et Rousse', gen: 2, couples: 5, babiesObtained: 3, date: daysAgo(1) },
];
const r = createGetStatisticsHandler(state)({ type: 'get-statistics', days: 0 });
expect(r.genBreakdown[0].gen).toBe(2);
expect(r.genBreakdown[1].gen).toBe(3);
});
// ── Races manquantes ────────────────────────────────────────
it('calcule les races manquantes (exclut Gen 1 et races obtenues)', () => {
const state = makeState();
state.accouplements = [
{ parentA: 'Rousse', parentB: 'Dorée', baby: 'Dorée et Rousse', gen: 2, couples: 5, babiesObtained: 3, date: daysAgo(1) },
];
const r = createGetStatisticsHandler(state)({ type: 'get-statistics', days: 0 });
// Dorée et Rousse obtenue → ne doit pas apparaître
expect(r.missingRaces.find(m => m.name === 'Dorée et Rousse')).toBeUndefined();
// Les Gen 1 ne doivent pas apparaître
expect(r.missingRaces.find(m => m.name === 'Rousse')).toBeUndefined();
expect(r.missingRaces.find(m => m.name === 'Dorée')).toBeUndefined();
expect(r.missingRaces.find(m => m.name === 'Amande')).toBeUndefined();
// Races non obtenues doivent apparaître
expect(r.missingRaces.find(m => m.name === 'Amande et Rousse')).toBeDefined();
expect(r.missingRaces.length).toBe(TOTAL_RACES - 1); // 63 - 1 obtenue
});
it('races manquantes utilise all (pas seulement la période courante)', () => {
const state = makeState();
state.accouplements = [
// Très ancien mais obtenu
{ parentA: 'A', parentB: 'B', baby: 'Ebène', gen: 3, couples: 5, babiesObtained: 1, date: '2020-01-01' },
// Récent
{ parentA: 'C', parentB: 'D', baby: 'Dorée et Rousse', gen: 2, couples: 5, babiesObtained: 2, date: daysAgo(1) },
];
const r = createGetStatisticsHandler(state)({ type: 'get-statistics', days: 7 });
// Ebène obtenue même si hors période courante → exclue des manquantes
expect(r.missingRaces.find(m => m.name === 'Ebène')).toBeUndefined();
expect(r.missingRaces.length).toBe(TOTAL_RACES - 2);
});
it('races manquantes triées par gen croissant puis nom', () => {
const state = makeState();
state.accouplements = [];
const r = createGetStatisticsHandler(state)({ type: 'get-statistics', days: 0 });
// Vérifier le tri
for (let i = 1; i < r.missingRaces.length; i++) {
const prev = r.missingRaces[i - 1];
const cur = r.missingRaces[i];
if (prev.gen === cur.gen) {
expect(prev.name.localeCompare(cur.name)).toBeLessThanOrEqual(0);
} else {
expect(prev.gen).toBeLessThan(cur.gen);
}
}
});
// ── Naissances par jour (dailyBirths) ──────────────────────
it('génère 30 jours de naissances quand days=0', () => {
const state = makeState();
state.accouplements = [];
const r = createGetStatisticsHandler(state)({ type: 'get-statistics', days: 0 });
expect(r.dailyBirths).toHaveLength(30);
});
it('génère N jours de naissances quand days=N', () => {
const state = makeState();
state.accouplements = [];
const r = createGetStatisticsHandler(state)({ type: 'get-statistics', days: 7 });
expect(r.dailyBirths).toHaveLength(7);
});
it('dailyBirths contient les naissances du jour', () => {
const today = daysAgo(0);
const state = makeState();
state.accouplements = [
{ parentA: 'Rousse', parentB: 'Dorée', baby: 'Dorée et Rousse', gen: 2, couples: 5, babiesObtained: 3, date: today },
];
const r = createGetStatisticsHandler(state)({ type: 'get-statistics', days: 7 });
const todayEntry = r.dailyBirths.find(d => d.date === today);
expect(todayEntry).toBeDefined();
expect(todayEntry!.count).toBe(3);
});
it('dailyBirths remplit 0 pour les jours sans naissance', () => {
const state = makeState();
state.accouplements = [];
const r = createGetStatisticsHandler(state)({ type: 'get-statistics', days: 7 });
expect(r.dailyBirths.every(d => d.count === 0)).toBe(true);
});
it('dailyBirths format label DD/MM', () => {
const state = makeState();
state.accouplements = [];
const r = createGetStatisticsHandler(state)({ type: 'get-statistics', days: 7 });
for (const d of r.dailyBirths) {
expect(d.label).toMatch(/^\d{2}\/\d{2}$/);
}
});
// ── Activité par jour de la semaine ─────────────────────────
it('activité par jour de la semaine commence lundi et finit dimanche', () => {
const state = makeState();
state.accouplements = [];
const r = createGetStatisticsHandler(state)({ type: 'get-statistics', days: 0 });
expect(r.weekdayActivity).toHaveLength(7);
expect(r.weekdayActivity[0].day).toBe('Lundi');
expect(r.weekdayActivity[6].day).toBe('Dimanche');
});
it('activité par jour de la semaine comptabilise les naissances', () => {
const today = daysAgo(0);
const todayName = dayOfWeekName(today);
const state = makeState();
state.accouplements = [
{ parentA: 'Rousse', parentB: 'Dorée', baby: 'Dorée et Rousse', gen: 2, couples: 5, babiesObtained: 7, date: today },
];
const r = createGetStatisticsHandler(state)({ type: 'get-statistics', days: 0 });
const dayEntry = r.weekdayActivity.find(w => w.day === todayName);
expect(dayEntry).toBeDefined();
expect(dayEntry!.count).toBe(7);
});
// ── Entrées sans date valide ────────────────────────────────
it('entrées sans date ne cassent pas le weekdayActivity ni dailyBirths', () => {
const state = makeState();
state.accouplements = [
{ parentA: 'Rousse', parentB: 'Dorée', baby: 'Dorée et Rousse', gen: 2, couples: 5, babiesObtained: 3, date: '' },
];
const r = createGetStatisticsHandler(state)({ type: 'get-statistics', days: 0 });
// Pas de crash, les compteurs weekday sont à 0
expect(r.weekdayActivity).toHaveLength(7);
expect(r.totalBabies.value).toBe(3); // toujours compté dans le total
});
// ── racesCount ne compte que les races avec babiesObtained > 0 ──
it('racesCount ne compte que les races avec au moins un bébé', () => {
const state = makeState();
state.accouplements = [
{ parentA: 'Rousse', parentB: 'Dorée', baby: 'Dorée et Rousse', gen: 2, couples: 10, babiesObtained: 5, date: daysAgo(1) },
{ parentA: 'Amande', parentB: 'Dorée', baby: 'Amande et Dorée', gen: 2, couples: 8, babiesObtained: 0, date: daysAgo(1) },
];
const r = createGetStatisticsHandler(state)({ type: 'get-statistics', days: 0 });
expect(r.racesCount.value).toBe(1); // Seule Dorée et Rousse a des bébés
});
// ── raceSuccessRates avec couples=0 ─────────────────────────
it('raceSuccessRates rate=0 quand couples=0', () => {
const state = makeState();
state.accouplements = [
{ parentA: 'Rousse', parentB: 'Dorée', baby: 'Dorée et Rousse', gen: 2, couples: 0, babiesObtained: 0, date: daysAgo(1) },
];
const r = createGetStatisticsHandler(state)({ type: 'get-statistics', days: 0 });
const dr = r.raceSuccessRates.find(rs => rs.race === 'Dorée et Rousse');
expect(dr).toBeDefined();
expect(dr!.rate).toBe(0);
});
// ── genBreakdown ne compte les races que si babiesObtained > 0 ──
it('genBreakdown.races ne compte que les races avec bébés', () => {
const state = makeState();
state.accouplements = [
{ parentA: 'A', parentB: 'B', baby: 'Dorée et Rousse', gen: 2, couples: 10, babiesObtained: 5, date: daysAgo(1) },
{ parentA: 'C', parentB: 'D', baby: 'Amande et Dorée', gen: 2, couples: 10, babiesObtained: 0, date: daysAgo(1) },
];
const r = createGetStatisticsHandler(state)({ type: 'get-statistics', days: 0 });
const gen2 = r.genBreakdown.find(g => g.gen === 2)!;
expect(gen2.races).toBe(1); // Seule Dorée et Rousse
});
// ── Delta successRate ──────────────────────────────────────
it('delta successRate calcule la différence entre périodes', () => {
const state = makeState();
state.accouplements = [
// Période courante : 10 couples, 8 bébés → 80%
{ parentA: 'Rousse', parentB: 'Dorée', baby: 'Dorée et Rousse', gen: 2, couples: 10, babiesObtained: 8, date: daysAgo(2) },
// Période précédente : 10 couples, 5 bébés → 50%
{ parentA: 'Amande', parentB: 'Dorée', baby: 'Amande et Dorée', gen: 2, couples: 10, babiesObtained: 5, date: daysAgo(10) },
];
const r = createGetStatisticsHandler(state)({ type: 'get-statistics', days: 7 });
expect(r.successRate.value).toBe(80);
expect(r.successRate.delta).toBe(30); // 80 - 50
});
});