Files
dashboard/utils/__tests__/buildReportSnapshot.test.ts
T

231 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, it, expect } from 'vitest';
import type { FeedSackColumnConfig } from '../../types/counting.ts';
import { buildReportSnapshot } from '../buildReportSnapshot.ts';
const START = '2026-05-21T17:00:00.000Z'; // 22 Mei 2026 00:00 WIB
const COLUMNS: FeedSackColumnConfig[] = [
{
id: 'in-1',
label: 'Karung Masuk K1',
cameraName: 'kandang_1_karung_masuk',
color: '#22c55e',
type: 'inbound',
kandangId: 1,
},
{
id: 'use-1',
label: 'Dituang K1',
cameraName: 'kandang_1_karung_pakan',
color: '#eab308',
type: 'usage',
kandangId: 1,
},
{
id: 'use-2',
label: 'Dituang K2',
cameraName: 'kandang_2_karung_pakan',
color: '#a855f7',
type: 'usage',
kandangId: 2,
},
{
id: 'out-1',
label: 'Keluar',
cameraName: 'kandang_1_karung_keluar',
color: '#ef4444',
type: 'outbound',
kandangId: 1,
},
];
const baseInput = {
cycleId: 'CYCLE-TEST',
cycleStartDate: START,
kandangId: 1,
kandangName: 'Kandang 1',
area: 'Sukawarna',
upToDay: 2,
initialPopulation: 100,
generatedAt: '2026-05-24T10:00:00.000Z',
mortalityRecords: [],
chickenCountByDay: {} as Record<number, number>,
feedSackHistory: [] as Array<{ cameraName: string; count: number; timestamp: Date | string }>,
manualFeedEntries: [] as Array<{ cameraName: string; count: number; date: string }>,
feedColumnConfig: COLUMNS,
cageUuid: 'cage-1',
weightByDay: [] as Array<{
day: number;
avgManual?: number | null;
avgIot?: number | null;
uniformity?: number | null;
}>,
};
describe('buildReportSnapshot', () => {
it('decreases counting after-count by mortality+harvest only for days 0–2', () => {
const snapshot = buildReportSnapshot({
...baseInput,
mortalityRecords: [
{ day: 1, mortalityCount: 3, afkir: 2, panen: 1, beratPanen: 4.5 },
{ day: 2, mortalityCount: 1, afkir: 0, panen: 0 },
],
});
expect(snapshot.counting).toHaveLength(3);
expect(snapshot.counting[0]).toMatchObject({
day: 0,
countBefore: 100,
mortality: 0,
harvest: 0,
countAfter: 100,
});
expect(snapshot.counting[1]).toMatchObject({
day: 1,
countBefore: 100,
mortality: 5,
harvest: 1,
harvestWeight: 4.5,
countAfter: 94,
});
expect(snapshot.counting[2]).toMatchObject({
day: 2,
countBefore: 94,
mortality: 1,
harvest: 0,
countAfter: 93,
});
});
it('combines left and right usage cameras for IoT and manual', () => {
const snapshot = buildReportSnapshot({
...baseInput,
kandangName: 'Kandang Atas',
upToDay: 0,
feedColumnConfig: [
{
id: 'in-1',
label: 'Karung Masuk',
cameraName: 'kandang_1_karung_masuk',
color: '#22c55e',
type: 'inbound',
},
{
id: 'use-atas',
label: 'Karung Dituang Kandang Atas',
cameraName: 'karung_tuang_feeder_kiri_atas',
color: '#f97316',
type: 'usage',
},
{
id: 'use-bawah',
label: 'Karung Dituang Kandang Bawah',
cameraName: 'karung_tuang_feeder_kanan_bawah',
color: '#a855f7',
type: 'usage',
},
],
feedSackHistory: [
{
cameraName: 'karung_tuang_feeder_kiri_atas',
count: 15,
timestamp: new Date(2026, 4, 22, 8, 0, 0),
},
{
cameraName: 'karung_tuang_feeder_kanan_bawah',
count: 15,
timestamp: new Date(2026, 4, 22, 8, 0, 0),
},
],
manualFeedEntries: [
{ cameraName: 'karung_tuang_feeder_kiri_atas', count: 15, date: '2026-05-22' },
{ cameraName: 'karung_tuang_feeder_kanan_bawah', count: 15, date: '2026-05-22' },
],
});
expect(snapshot.feedUse).toHaveLength(1);
expect(snapshot.feedUse[0]?.iotUse).toBe(30);
expect(snapshot.feedUse[0]?.manualUse).toBe(30);
expect(snapshot.feedUse[0]?.accuracy).toBe(100);
});
it('keeps feed-in empty when inbound mapping fails, but still combines usage cameras', () => {
const snapshot = buildReportSnapshot({
...baseInput,
kandangId: 99,
upToDay: 0,
feedSackHistory: [
{
cameraName: 'kandang_1_karung_pakan',
count: 10,
timestamp: new Date(2026, 4, 22, 8, 0, 0),
},
{
cameraName: 'kandang_2_karung_pakan',
count: 50,
timestamp: new Date(2026, 4, 22, 8, 0, 0),
},
],
});
expect(snapshot.feedUse[0]?.iotUse).toBe(60);
expect(snapshot.feedUse[0]?.manualUse).toBe(0);
expect(snapshot.feedIn[0]?.iotIn).toBeNull();
expect(snapshot.feedIn[0]?.manualIn).toBeNull();
});
it('returns empty weight rows without cageUuid but still builds a snapshot', () => {
const snapshot = buildReportSnapshot({
...baseInput,
cageUuid: null,
weightByDay: [{ day: 1, avgManual: 1000, avgIot: 900, uniformity: 80 }],
});
expect(snapshot.cycleId).toBe('CYCLE-TEST');
expect(snapshot.weight.every((row) => row.avgManual == null && row.avgIot == null)).toBe(
true
);
});
it('drops rows after the selected day', () => {
const snapshot = buildReportSnapshot({
...baseInput,
upToDay: 8,
mortalityRecords: [
{ day: 8, mortalityCount: 1 },
{ day: 9, mortalityCount: 99 },
],
weightByDay: [
{ day: 9, avgManual: 2000, avgIot: 1900 }, // 1-based day 9 → report day 8
{ day: 10, avgManual: 3000, avgIot: 2900 }, // report day 9 — dropped
],
});
expect(snapshot.counting.every((row) => row.day <= 8)).toBe(true);
expect(snapshot.counting.some((row) => row.day === 9)).toBe(false);
expect(snapshot.weight.some((row) => row.day === 8 && row.avgManual === 2000)).toBe(true);
expect(snapshot.weight.some((row) => row.avgManual === 3000)).toBe(false);
});
it('maps 1-based feed day 1 on cycle start to Hari ke-0', () => {
const snapshot = buildReportSnapshot({
...baseInput,
upToDay: 0,
feedSackHistory: [
{
cameraName: 'kandang_1_karung_masuk',
count: 7,
timestamp: new Date(2026, 4, 22, 6, 0, 0),
},
],
manualFeedEntries: [{ cameraName: 'kandang_1_karung_masuk', count: 7, date: '2026-05-22' }],
});
expect(snapshot.feedIn).toHaveLength(1);
expect(snapshot.feedIn[0] && 'day' in snapshot.feedIn[0]).toBe(false);
expect(snapshot.feedIn[0]?.date).toBe('2026-05-22');
expect(snapshot.feedIn[0]?.iotIn).toBe(7);
expect(snapshot.feedIn[0]?.manualIn).toBe(7);
});
});