feat: add unit tests for featured listings, neighborhood scores + price history chart

- Add unit tests for FeatureListingHandler (6 tests) and ActivateFeaturedListingHandler (6 tests)
- Add unit tests for NeighborhoodScoreServiceImpl (5 tests) and GetNeighborhoodScoreHandler (2 tests)
- Add PriceHistoryChart component with recharts LineChart for listing detail page
- Wire up price history API client and integrate chart into listing detail view

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-16 18:21:44 +07:00
parent 0dda2bffdb
commit 8e9d021465
7 changed files with 560 additions and 14 deletions

View File

@@ -0,0 +1,132 @@
import { NeighborhoodScoreServiceImpl } from '../services/neighborhood-score.service';
describe('NeighborhoodScoreServiceImpl', () => {
let service: NeighborhoodScoreServiceImpl;
let mockPrisma: {
neighborhoodScore: { findUnique: ReturnType<typeof vi.fn>; upsert: ReturnType<typeof vi.fn> };
pOI: { count: ReturnType<typeof vi.fn> };
};
let mockLogger: { log: ReturnType<typeof vi.fn> };
beforeEach(() => {
mockPrisma = {
neighborhoodScore: {
findUnique: vi.fn(),
upsert: vi.fn(),
},
pOI: { count: vi.fn() },
};
mockLogger = { log: vi.fn() };
service = new NeighborhoodScoreServiceImpl(mockPrisma as any, mockLogger as any);
});
describe('getScore', () => {
it('returns existing score from database', async () => {
const stored = {
district: 'Quận 1',
city: 'Hồ Chí Minh',
educationScore: 8,
healthcareScore: 7,
transportScore: 9,
shoppingScore: 6,
greeneryScore: 5,
safetyScore: 4,
totalScore: 68.5,
poiCounts: { education: 12, healthcare: 5 },
calculatedAt: new Date(),
};
mockPrisma.neighborhoodScore.findUnique.mockResolvedValue(stored);
const result = await service.getScore('Quận 1', 'Hồ Chí Minh');
expect(result).not.toBeNull();
expect(result!.district).toBe('Quận 1');
expect(result!.totalScore).toBe(68.5);
expect(result!.poiCounts).toEqual({ education: 12, healthcare: 5 });
});
it('returns null when no score exists', async () => {
mockPrisma.neighborhoodScore.findUnique.mockResolvedValue(null);
const result = await service.getScore('Quận 99', 'Hồ Chí Minh');
expect(result).toBeNull();
});
});
describe('calculateAndSave', () => {
it('calculates scores from POI counts and upserts', async () => {
// Simulate POI counts: education=15 (max), healthcare=4 (50%), transport=6 (50%),
// shopping=5 (50%), greenery=3 (50%), safety=2 (50%)
const poiCountsByCategory = [15, 4, 6, 5, 3, 2];
let callIndex = 0;
mockPrisma.pOI.count.mockImplementation(() => {
return Promise.resolve(poiCountsByCategory[callIndex++]!);
});
mockPrisma.neighborhoodScore.upsert.mockImplementation(({ create }) => {
return Promise.resolve(create);
});
const result = await service.calculateAndSave('Quận 1', 'Hồ Chí Minh');
// education: 15/15 * 10 = 10 → 10 * 20/10 = 20
// healthcare: 4/8 * 10 = 5 → 5 * 20/10 = 10
// transport: 6/12 * 10 = 5 → 5 * 20/10 = 10
// shopping: 5/10 * 10 = 5 → 5 * 15/10 = 7.5
// greenery: 3/6 * 10 = 5 → 5 * 15/10 = 7.5
// safety: 2/4 * 10 = 5 → 5 * 10/10 = 5
// total = 20 + 10 + 10 + 7.5 + 7.5 + 5 = 60
expect(result.educationScore).toBe(10);
expect(result.healthcareScore).toBe(5);
expect(result.totalScore).toBe(60);
expect(mockPrisma.neighborhoodScore.upsert).toHaveBeenCalledTimes(1);
});
it('caps category scores at 10', async () => {
// All categories have way more POIs than max
mockPrisma.pOI.count.mockResolvedValue(100);
mockPrisma.neighborhoodScore.upsert.mockImplementation(({ create }) => {
return Promise.resolve(create);
});
const result = await service.calculateAndSave('Quận 1', 'Hồ Chí Minh');
// All scores capped at 10 → total = sum of weights = 100
expect(result.educationScore).toBe(10);
expect(result.healthcareScore).toBe(10);
expect(result.transportScore).toBe(10);
expect(result.shoppingScore).toBe(10);
expect(result.greeneryScore).toBe(10);
expect(result.safetyScore).toBe(10);
expect(result.totalScore).toBe(100);
});
it('returns 0 scores when no POIs exist', async () => {
mockPrisma.pOI.count.mockResolvedValue(0);
mockPrisma.neighborhoodScore.upsert.mockImplementation(({ create }) => {
return Promise.resolve(create);
});
const result = await service.calculateAndSave('Quận 1', 'Hồ Chí Minh');
expect(result.educationScore).toBe(0);
expect(result.totalScore).toBe(0);
});
it('logs the calculated score', async () => {
mockPrisma.pOI.count.mockResolvedValue(5);
mockPrisma.neighborhoodScore.upsert.mockImplementation(({ create }) => {
return Promise.resolve(create);
});
await service.calculateAndSave('Quận 1', 'Hồ Chí Minh');
expect(mockLogger.log).toHaveBeenCalledWith(
expect.stringContaining('Quận 1'),
'NeighborhoodScoreService',
);
});
});
});