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>
674 lines
30 KiB
TypeScript
Executable File
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
|
|
});
|
|
});
|