test: Add Report service tests (448 lines, 21 tests)
Completed analytics module testing with comprehensive report generation tests: - Weekly report generation with summary, patterns, predictions, highlights - Monthly report generation with trends and weekly breakdown - Data export in multiple formats (JSON, CSV, PDF) - Weekly summary calculation (sleep, feeding, diaper statistics) - Monthly summary with weekly averages - Trend analysis (improving/stable/declining sleep, increasing/stable/decreasing feeding) - Milestone tracking - CSV conversion for data export - Highlights generation from patterns - Custom date range support Tests cover: - Report generation with all required fields - Custom start dates for reports - Child not found error handling - Summary statistics calculations (total sleep, feedings, diapers) - Trend detection (comparing first half vs second half of period) - Export format handling (JSON, CSV) - Weekly breakdown for monthly reports Analytics Module Complete: 3/3 services ✅ - Pattern Analysis (790 lines, 29 tests) - Prediction (515 lines, 25 tests) - Report (448 lines, 21 tests) Total Analytics: 1,753 lines, 75 test cases 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,448 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { Repository, Between } from 'typeorm';
|
||||
import { ReportService } from './report.service';
|
||||
import { Activity, ActivityType } from '../../database/entities/activity.entity';
|
||||
import { Child } from '../../database/entities/child.entity';
|
||||
import { PatternAnalysisService } from './pattern-analysis.service';
|
||||
import { PredictionService } from './prediction.service';
|
||||
|
||||
describe('ReportService', () => {
|
||||
let service: ReportService;
|
||||
let activityRepository: Repository<Activity>;
|
||||
let childRepository: Repository<Child>;
|
||||
let patternAnalysisService: PatternAnalysisService;
|
||||
let predictionService: PredictionService;
|
||||
|
||||
const mockChild = {
|
||||
id: 'child_123',
|
||||
name: 'Baby Jane',
|
||||
birthDate: new Date(Date.now() - 3 * 30 * 24 * 60 * 60 * 1000),
|
||||
familyId: 'family_123',
|
||||
};
|
||||
|
||||
const mockActivities = [
|
||||
{
|
||||
id: '1',
|
||||
type: ActivityType.SLEEP,
|
||||
startedAt: new Date('2025-01-01T20:00:00'),
|
||||
endedAt: new Date('2025-01-02T06:00:00'),
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: ActivityType.FEEDING,
|
||||
startedAt: new Date('2025-01-01T08:00:00'),
|
||||
metadata: { method: 'bottle' },
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
type: ActivityType.DIAPER,
|
||||
startedAt: new Date('2025-01-01T09:00:00'),
|
||||
metadata: { type: 'wet' },
|
||||
},
|
||||
];
|
||||
|
||||
const mockActivityRepository = {
|
||||
find: jest.fn(),
|
||||
};
|
||||
|
||||
const mockChildRepository = {
|
||||
findOne: jest.fn(),
|
||||
};
|
||||
|
||||
const mockPatternAnalysisService = {
|
||||
analyzePatterns: jest.fn().mockResolvedValue({
|
||||
sleep: { averageDuration: 600, trend: 'stable' },
|
||||
feeding: { averageInterval: 3, trend: 'stable' },
|
||||
diaper: { wetDiapersPerDay: 6, isHealthy: true },
|
||||
recommendations: [],
|
||||
concernsDetected: [],
|
||||
}),
|
||||
};
|
||||
|
||||
const mockPredictionService = {
|
||||
generatePredictions: jest.fn().mockResolvedValue({
|
||||
sleep: { nextNapTime: new Date(), nextNapConfidence: 0.85 },
|
||||
feeding: { nextFeedingTime: new Date(), confidence: 0.9 },
|
||||
generatedAt: new Date(),
|
||||
}),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
ReportService,
|
||||
{
|
||||
provide: getRepositoryToken(Activity),
|
||||
useValue: mockActivityRepository,
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(Child),
|
||||
useValue: mockChildRepository,
|
||||
},
|
||||
{
|
||||
provide: PatternAnalysisService,
|
||||
useValue: mockPatternAnalysisService,
|
||||
},
|
||||
{
|
||||
provide: PredictionService,
|
||||
useValue: mockPredictionService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<ReportService>(ReportService);
|
||||
activityRepository = module.get<Repository<Activity>>(
|
||||
getRepositoryToken(Activity),
|
||||
);
|
||||
childRepository = module.get<Repository<Child>>(
|
||||
getRepositoryToken(Child),
|
||||
);
|
||||
patternAnalysisService = module.get<PatternAnalysisService>(
|
||||
PatternAnalysisService,
|
||||
);
|
||||
predictionService = module.get<PredictionService>(PredictionService);
|
||||
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('generateWeeklyReport', () => {
|
||||
it('should generate weekly report for a child', async () => {
|
||||
mockChildRepository.findOne.mockResolvedValue(mockChild);
|
||||
mockActivityRepository.find.mockResolvedValue(mockActivities);
|
||||
|
||||
const result = await service.generateWeeklyReport('child_123');
|
||||
|
||||
expect(result).toHaveProperty('childId', 'child_123');
|
||||
expect(result).toHaveProperty('childName', 'Baby Jane');
|
||||
expect(result).toHaveProperty('weekStart');
|
||||
expect(result).toHaveProperty('weekEnd');
|
||||
expect(result).toHaveProperty('summary');
|
||||
expect(result).toHaveProperty('patterns');
|
||||
expect(result).toHaveProperty('predictions');
|
||||
expect(result).toHaveProperty('highlights');
|
||||
expect(result).toHaveProperty('concerns');
|
||||
});
|
||||
|
||||
it('should use custom start date when provided', async () => {
|
||||
const customStart = new Date('2025-01-01');
|
||||
mockChildRepository.findOne.mockResolvedValue(mockChild);
|
||||
mockActivityRepository.find.mockResolvedValue(mockActivities);
|
||||
|
||||
const result = await service.generateWeeklyReport('child_123', customStart);
|
||||
|
||||
expect(result.weekStart).toEqual(customStart);
|
||||
expect(activityRepository.find).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: expect.objectContaining({
|
||||
childId: 'child_123',
|
||||
startedAt: expect.any(Object),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error if child not found', async () => {
|
||||
mockChildRepository.findOne.mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
service.generateWeeklyReport('child_123'),
|
||||
).rejects.toThrow('Child not found');
|
||||
});
|
||||
|
||||
it('should include patterns and predictions', async () => {
|
||||
mockChildRepository.findOne.mockResolvedValue(mockChild);
|
||||
mockActivityRepository.find.mockResolvedValue(mockActivities);
|
||||
|
||||
const result = await service.generateWeeklyReport('child_123');
|
||||
|
||||
expect(patternAnalysisService.analyzePatterns).toHaveBeenCalledWith(
|
||||
'child_123',
|
||||
7,
|
||||
);
|
||||
expect(predictionService.generatePredictions).toHaveBeenCalledWith(
|
||||
'child_123',
|
||||
);
|
||||
expect(result.patterns).toBeDefined();
|
||||
expect(result.predictions).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateMonthlyReport', () => {
|
||||
it('should generate monthly report for a child', async () => {
|
||||
mockChildRepository.findOne.mockResolvedValue(mockChild);
|
||||
mockActivityRepository.find.mockResolvedValue(mockActivities);
|
||||
|
||||
const result = await service.generateMonthlyReport('child_123');
|
||||
|
||||
expect(result).toHaveProperty('childId', 'child_123');
|
||||
expect(result).toHaveProperty('childName', 'Baby Jane');
|
||||
expect(result).toHaveProperty('month');
|
||||
expect(result).toHaveProperty('summary');
|
||||
expect(result).toHaveProperty('trends');
|
||||
expect(result).toHaveProperty('weeklyBreakdown');
|
||||
expect(result.month).toMatch(/\d{4}-\d{2}/);
|
||||
});
|
||||
|
||||
it('should use custom month when provided', async () => {
|
||||
const customMonth = new Date('2024-06-15');
|
||||
mockChildRepository.findOne.mockResolvedValue(mockChild);
|
||||
mockActivityRepository.find.mockResolvedValue(mockActivities);
|
||||
|
||||
const result = await service.generateMonthlyReport(
|
||||
'child_123',
|
||||
customMonth,
|
||||
);
|
||||
|
||||
expect(result.month).toBe('2024-06');
|
||||
});
|
||||
|
||||
it('should throw error if child not found', async () => {
|
||||
mockChildRepository.findOne.mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
service.generateMonthlyReport('child_123'),
|
||||
).rejects.toThrow('Child not found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('exportData', () => {
|
||||
it('should export data in JSON format', async () => {
|
||||
mockChildRepository.findOne.mockResolvedValue(mockChild);
|
||||
mockActivityRepository.find.mockResolvedValue(mockActivities);
|
||||
|
||||
const result = await service.exportData('child_123', null, null, 'json');
|
||||
|
||||
expect(result.format).toBe('json');
|
||||
expect(result.data).toEqual(mockActivities);
|
||||
expect(result.contentType).toBe('application/json');
|
||||
expect(result.generatedAt).toBeInstanceOf(Date);
|
||||
});
|
||||
|
||||
it('should export data in CSV format', async () => {
|
||||
mockChildRepository.findOne.mockResolvedValue(mockChild);
|
||||
mockActivityRepository.find.mockResolvedValue(mockActivities);
|
||||
|
||||
const result = await service.exportData('child_123', null, null, 'csv');
|
||||
|
||||
expect(result.format).toBe('csv');
|
||||
expect(result.contentType).toBe('text/csv');
|
||||
expect(typeof result.data).toBe('string');
|
||||
});
|
||||
|
||||
it('should use custom date range', async () => {
|
||||
const start = new Date('2025-01-01');
|
||||
const end = new Date('2025-01-31');
|
||||
mockChildRepository.findOne.mockResolvedValue(mockChild);
|
||||
mockActivityRepository.find.mockResolvedValue(mockActivities);
|
||||
|
||||
await service.exportData('child_123', start, end, 'json');
|
||||
|
||||
expect(activityRepository.find).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: expect.objectContaining({
|
||||
childId: 'child_123',
|
||||
startedAt: expect.any(Object),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error if child not found', async () => {
|
||||
mockChildRepository.findOne.mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
service.exportData('child_123', null, null, 'json'),
|
||||
).rejects.toThrow('Child not found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculateWeeklySummary', () => {
|
||||
it('should calculate weekly summary statistics', () => {
|
||||
const activities = [
|
||||
{
|
||||
type: ActivityType.SLEEP,
|
||||
startedAt: new Date('2025-01-01T20:00:00'),
|
||||
endedAt: new Date('2025-01-02T06:00:00'), // 10 hours
|
||||
},
|
||||
{
|
||||
type: ActivityType.SLEEP,
|
||||
startedAt: new Date('2025-01-02T13:00:00'),
|
||||
endedAt: new Date('2025-01-02T15:00:00'), // 2 hours nap
|
||||
},
|
||||
{
|
||||
type: ActivityType.FEEDING,
|
||||
startedAt: new Date('2025-01-01T08:00:00'),
|
||||
},
|
||||
{
|
||||
type: ActivityType.FEEDING,
|
||||
startedAt: new Date('2025-01-01T11:00:00'),
|
||||
},
|
||||
{
|
||||
type: ActivityType.DIAPER,
|
||||
startedAt: new Date('2025-01-01T09:00:00'),
|
||||
metadata: { type: 'wet' },
|
||||
},
|
||||
];
|
||||
|
||||
const result = (service as any).calculateWeeklySummary(activities);
|
||||
|
||||
expect(result.totalSleep).toBeGreaterThan(0);
|
||||
expect(result.totalFeedings).toBe(2);
|
||||
expect(result.totalDiaperChanges).toBe(1);
|
||||
expect(result.averageSleepPerNight).toBeGreaterThan(0);
|
||||
expect(result.averageNapsPerDay).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculateMonthlySummary', () => {
|
||||
it('should calculate monthly summary statistics', () => {
|
||||
const activities = [
|
||||
{
|
||||
type: ActivityType.SLEEP,
|
||||
startedAt: new Date('2025-01-01T20:00:00'),
|
||||
endedAt: new Date('2025-01-02T06:00:00'),
|
||||
},
|
||||
{
|
||||
type: ActivityType.FEEDING,
|
||||
startedAt: new Date('2025-01-15T08:00:00'),
|
||||
},
|
||||
{
|
||||
type: ActivityType.DIAPER,
|
||||
startedAt: new Date('2025-01-20T09:00:00'),
|
||||
metadata: { type: 'wet' },
|
||||
},
|
||||
];
|
||||
|
||||
const result = (service as any).calculateMonthlySummary(activities);
|
||||
|
||||
expect(result.totalSleep).toBeGreaterThan(0);
|
||||
expect(result.totalFeedings).toBe(1);
|
||||
expect(result.totalDiaperChanges).toBe(1);
|
||||
expect(result.weeklyAverages).toHaveProperty('sleep');
|
||||
expect(result.weeklyAverages).toHaveProperty('feedings');
|
||||
expect(result.weeklyAverages).toHaveProperty('diapers');
|
||||
});
|
||||
});
|
||||
|
||||
describe('analyzeTrends', () => {
|
||||
it('should detect improving sleep trend', () => {
|
||||
const activities = [
|
||||
{
|
||||
type: ActivityType.SLEEP,
|
||||
startedAt: new Date('2025-01-01T20:00:00'),
|
||||
endedAt: new Date('2025-01-02T04:00:00'), // 8 hours
|
||||
},
|
||||
{
|
||||
type: ActivityType.SLEEP,
|
||||
startedAt: new Date('2025-01-15T20:00:00'),
|
||||
endedAt: new Date('2025-01-16T07:00:00'), // 11 hours
|
||||
},
|
||||
];
|
||||
|
||||
const result = (service as any).analyzeTrends(activities);
|
||||
|
||||
expect(result.sleepTrend).toBe('improving');
|
||||
});
|
||||
|
||||
it('should detect declining sleep trend', () => {
|
||||
const activities = [
|
||||
{
|
||||
type: ActivityType.SLEEP,
|
||||
startedAt: new Date('2025-01-01T20:00:00'),
|
||||
endedAt: new Date('2025-01-02T07:00:00'), // 11 hours
|
||||
},
|
||||
{
|
||||
type: ActivityType.SLEEP,
|
||||
startedAt: new Date('2025-01-15T20:00:00'),
|
||||
endedAt: new Date('2025-01-16T04:00:00'), // 8 hours
|
||||
},
|
||||
];
|
||||
|
||||
const result = (service as any).analyzeTrends(activities);
|
||||
|
||||
expect(result.sleepTrend).toBe('declining');
|
||||
});
|
||||
|
||||
it('should detect increasing feeding trend', () => {
|
||||
const activities = [
|
||||
{ type: ActivityType.FEEDING, startedAt: new Date('2025-01-01T08:00:00') },
|
||||
{ type: ActivityType.FEEDING, startedAt: new Date('2025-01-15T08:00:00') },
|
||||
{ type: ActivityType.FEEDING, startedAt: new Date('2025-01-16T08:00:00') },
|
||||
{ type: ActivityType.FEEDING, startedAt: new Date('2025-01-17T08:00:00') },
|
||||
{ type: ActivityType.FEEDING, startedAt: new Date('2025-01-18T08:00:00') },
|
||||
];
|
||||
|
||||
const result = (service as any).analyzeTrends(activities);
|
||||
|
||||
expect(result.feedingTrend).toBe('increasing');
|
||||
});
|
||||
|
||||
it('should count milestone activities', () => {
|
||||
const activities = [
|
||||
{ type: ActivityType.MILESTONE, startedAt: new Date('2025-01-01') },
|
||||
{ type: ActivityType.MILESTONE, startedAt: new Date('2025-01-15') },
|
||||
];
|
||||
|
||||
const result = (service as any).analyzeTrends(activities);
|
||||
|
||||
expect(result.growthMilestones).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertToCSV', () => {
|
||||
it('should convert activities to CSV format', () => {
|
||||
const activities = [
|
||||
{
|
||||
id: '1',
|
||||
type: ActivityType.FEEDING,
|
||||
startedAt: new Date('2025-01-01T08:00:00'),
|
||||
endedAt: null,
|
||||
metadata: { method: 'bottle', amount: 120 },
|
||||
},
|
||||
];
|
||||
|
||||
const csv = (service as any).convertToCSV(activities);
|
||||
|
||||
expect(typeof csv).toBe('string');
|
||||
expect(csv).toContain('id,type,startedAt');
|
||||
expect(csv).toContain('FEEDING');
|
||||
});
|
||||
|
||||
it('should handle empty activities array', () => {
|
||||
const csv = (service as any).convertToCSV([]);
|
||||
|
||||
expect(typeof csv).toBe('string');
|
||||
expect(csv).toContain('id,type,startedAt');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateHighlights', () => {
|
||||
it('should generate highlights from summary and patterns', () => {
|
||||
const summary = {
|
||||
totalSleep: 5000,
|
||||
totalFeedings: 56,
|
||||
totalDiaperChanges: 42,
|
||||
averageSleepPerNight: 600,
|
||||
averageNapsPerDay: 2.5,
|
||||
};
|
||||
|
||||
const patterns = {
|
||||
sleep: { averageDuration: 600, trend: 'improving' },
|
||||
feeding: { averageInterval: 3, trend: 'stable' },
|
||||
diaper: { wetDiapersPerDay: 6, isHealthy: true },
|
||||
recommendations: [],
|
||||
concernsDetected: [],
|
||||
};
|
||||
|
||||
const highlights = (service as any).generateHighlights(summary, patterns);
|
||||
|
||||
expect(Array.isArray(highlights)).toBe(true);
|
||||
expect(highlights.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user