init
This commit is contained in:
147
server/tests/services/stats.service.test.js
Normal file
147
server/tests/services/stats.service.test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user