This commit is contained in:
Rene Kievits
2025-12-18 01:30:52 +01:00
commit 6438660b03
78 changed files with 14230 additions and 0 deletions

View File

@@ -0,0 +1,147 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { getUserStats } from '../../src/services/stats.service.js';
import { StudyItem } from '../../src/models/StudyItem.js';
import { ReviewLog } from '../../src/models/ReviewLog.js';
vi.mock('../../src/models/StudyItem.js');
vi.mock('../../src/models/ReviewLog.js');
describe('Stats Service', () => {
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2023-01-10T12:00:00Z'));
});
afterEach(() => {
vi.useRealTimers();
vi.clearAllMocks();
});
const mockUser = {
_id: 'u1',
stats: {
totalReviews: 10,
correctReviews: 8,
currentStreak: 5,
lastStudyDate: '2023-01-10',
lastFreezeDate: '2022-01-01'
}
};
it('should calculate stats correctly including ghosts, forecast, and unknown SRS levels', async () => {
StudyItem.aggregate.mockResolvedValue([
{ _id: 1, count: 5 },
{ _id: 8, count: 2 }
]);
StudyItem.find.mockImplementation(() => ({
select: vi.fn().mockImplementation((fields) => {
if (fields.includes('nextReview')) {
const now = new Date();
return Promise.resolve([
{ nextReview: new Date(now.getTime() - 1000) },
{ nextReview: new Date(now.getTime() + 3600000) },
{ nextReview: new Date(now.getTime() + 24 * 3600000) }
]);
}
if (fields.includes('stats')) {
const mkGhost = (acc) => ({
char: 'A', meaning: 'B', stats: { correct: acc, total: 100 }, srsLevel: 1,
toObject: () => ({ char: 'A', meaning: 'B', stats: { correct: acc, total: 100 }, srsLevel: 1 })
});
return Promise.resolve([mkGhost(60), mkGhost(50), mkGhost(90)]);
}
if (fields.includes('srsLevel')) {
return Promise.resolve([{ srsLevel: 1 }, { srsLevel: 4 }]);
}
return Promise.resolve([]);
})
}));
ReviewLog.find.mockResolvedValue([
{ date: '2023-01-09', count: 5 }
]);
const stats = await getUserStats(mockUser);
expect(stats.forecast[0]).toBe(1);
expect(stats.forecast[1]).toBe(1);
expect(stats.ghosts.length).toBe(2);
expect(stats.ghosts[0].accuracy).toBe(50);
expect(stats.ghosts[1].accuracy).toBe(60);
expect(stats.distribution[1]).toBe(5);
expect(stats.distribution[8]).toBeUndefined();
expect(stats.heatmap['2023-01-09']).toBe(5);
});
it('should handle queue with same levels (hasLowerLevels = false)', async () => {
StudyItem.aggregate.mockResolvedValue([]);
StudyItem.find.mockImplementation(() => ({
select: vi.fn().mockImplementation((fields) => {
if (fields.includes('stats')) {
return Promise.resolve([]);
}
if (fields.includes('srsLevel')) {
return Promise.resolve([{ srsLevel: 2 }, { srsLevel: 2 }]);
}
return Promise.resolve([]);
})
}));
ReviewLog.find.mockResolvedValue([]);
const stats = await getUserStats(mockUser);
expect(stats.hasLowerLevels).toBe(false);
});
it('should calculate shield cooldown', async () => {
StudyItem.aggregate.mockResolvedValue([]);
StudyItem.find.mockReturnValue({ select: vi.fn().mockResolvedValue([]) });
ReviewLog.find.mockResolvedValue([]);
const frozenUser = {
...mockUser,
stats: { ...mockUser.stats, lastFreezeDate: '2023-01-07' }
};
const stats = await getUserStats(frozenUser);
expect(stats.streak.shield.ready).toBe(false);
expect(stats.streak.shield.cooldown).toBe(4);
});
it('should handle streak logic when lastStudyDate is missing or old', async () => {
const userNoDate = { ...mockUser, stats: { ...mockUser.stats, lastStudyDate: null, currentStreak: 5 } };
StudyItem.aggregate.mockResolvedValue([]);
StudyItem.find.mockReturnValue({ select: vi.fn().mockResolvedValue([]) });
ReviewLog.find.mockResolvedValue([]);
const res1 = await getUserStats(userNoDate);
expect(res1.streak.current).toBe(5);
const userMissed = { ...mockUser, stats: { ...mockUser.stats, lastStudyDate: '2023-01-08' } };
const res2 = await getUserStats(userMissed);
expect(res2.streak.current).toBe(0);
});
it('should handle missing user stats fields (null checks)', async () => {
const emptyUser = {
_id: 'u2',
stats: {}
};
StudyItem.aggregate.mockResolvedValue([]);
StudyItem.find.mockReturnValue({ select: vi.fn().mockResolvedValue([]) });
ReviewLog.find.mockResolvedValue([]);
const stats = await getUserStats(emptyUser);
expect(stats.streak.current).toBe(0);
expect(stats.streak.best).toBe(0);
expect(stats.accuracy.total).toBe(0);
expect(stats.accuracy.correct).toBe(0);
expect(stats.streak.shield.ready).toBe(true);
});
});