test(analytics): add unit tests for AVM batch, history, comparison endpoints
Add comprehensive test coverage for the three AVM API upgrade endpoints: - BatchValuationHandler: batch results, partial failures, error handling - ValuationHistoryHandler: history retrieval, limit, empty state, errors - ValuationComparisonHandler: multi-property compare, summary, edge cases - AnalyticsController: route-level tests for all new endpoints Fix async error handling in handlers by adding await to cache.getOrSet calls so try/catch blocks properly catch rejections. Fix pre-existing web test failures: add missing FLOOD_RISK_OPTIONS and QUALITY_LABELS to valuation-form mock, update valuation-results assertions to match current component rendering. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
import { InternalServerErrorException } from '@nestjs/common';
|
||||
import { type CacheService } from '@modules/shared/infrastructure/cache.service';
|
||||
import { DomainException } from '@modules/shared';
|
||||
import {
|
||||
type IAVMService,
|
||||
type BatchValuationResult,
|
||||
type ValuationResult,
|
||||
} from '../../domain/services/avm-service';
|
||||
import { BatchValuationHandler } from '../queries/batch-valuation/batch-valuation.handler';
|
||||
import { BatchValuationQuery } from '../queries/batch-valuation/batch-valuation.query';
|
||||
|
||||
describe('BatchValuationHandler', () => {
|
||||
let handler: BatchValuationHandler;
|
||||
let mockAvm: { [K in keyof IAVMService]: ReturnType<typeof vi.fn> };
|
||||
let mockLogger: { error: ReturnType<typeof vi.fn> };
|
||||
|
||||
const sampleValuation: ValuationResult = {
|
||||
estimatedPrice: '5000000000',
|
||||
confidence: 0.85,
|
||||
pricePerM2: 75000000,
|
||||
comparables: [],
|
||||
modelVersion: 'avm-v1.0',
|
||||
};
|
||||
|
||||
const sampleBatchResult: BatchValuationResult[] = [
|
||||
{ propertyId: 'prop-1', valuation: sampleValuation },
|
||||
{ propertyId: 'prop-2', valuation: sampleValuation },
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
mockAvm = {
|
||||
estimateValue: vi.fn(),
|
||||
getComparables: vi.fn(),
|
||||
estimateBatch: vi.fn(),
|
||||
};
|
||||
mockLogger = { error: vi.fn() };
|
||||
const mockCache = {
|
||||
getOrSet: vi.fn((_key: string, loader: () => Promise<unknown>) => loader()),
|
||||
} as unknown as CacheService;
|
||||
handler = new BatchValuationHandler(
|
||||
mockAvm as any,
|
||||
mockCache,
|
||||
mockLogger as any,
|
||||
);
|
||||
});
|
||||
|
||||
it('returns batch valuation results for multiple properties', async () => {
|
||||
mockAvm.estimateBatch.mockResolvedValue(sampleBatchResult);
|
||||
|
||||
const query = new BatchValuationQuery(['prop-1', 'prop-2']);
|
||||
const result = await handler.execute(query);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]!.propertyId).toBe('prop-1');
|
||||
expect(result[0]!.valuation!.estimatedPrice).toBe('5000000000');
|
||||
expect(result[1]!.propertyId).toBe('prop-2');
|
||||
expect(mockAvm.estimateBatch).toHaveBeenCalledWith([
|
||||
{ propertyId: 'prop-1' },
|
||||
{ propertyId: 'prop-2' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('handles partial failures in batch results', async () => {
|
||||
const partialResult: BatchValuationResult[] = [
|
||||
{ propertyId: 'prop-1', valuation: sampleValuation },
|
||||
{ propertyId: 'prop-2', valuation: null, error: 'Not found' },
|
||||
];
|
||||
mockAvm.estimateBatch.mockResolvedValue(partialResult);
|
||||
|
||||
const query = new BatchValuationQuery(['prop-1', 'prop-2']);
|
||||
const result = await handler.execute(query);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]!.valuation).not.toBeNull();
|
||||
expect(result[1]!.valuation).toBeNull();
|
||||
expect(result[1]!.error).toBe('Not found');
|
||||
});
|
||||
|
||||
it('re-throws DomainException directly', async () => {
|
||||
const domainError = new DomainException('VALIDATION_ERROR', 'Invalid input');
|
||||
mockAvm.estimateBatch.mockRejectedValue(domainError);
|
||||
|
||||
const query = new BatchValuationQuery(['prop-1']);
|
||||
|
||||
await expect(handler.execute(query)).rejects.toThrow(DomainException);
|
||||
});
|
||||
|
||||
it('wraps unexpected errors in InternalServerErrorException', async () => {
|
||||
mockAvm.estimateBatch.mockRejectedValue(new Error('Database timeout'));
|
||||
|
||||
const query = new BatchValuationQuery(['prop-1']);
|
||||
|
||||
await expect(handler.execute(query)).rejects.toThrow(InternalServerErrorException);
|
||||
expect(mockLogger.error).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('passes single property batch correctly', async () => {
|
||||
const singleResult: BatchValuationResult[] = [
|
||||
{ propertyId: 'prop-solo', valuation: sampleValuation },
|
||||
];
|
||||
mockAvm.estimateBatch.mockResolvedValue(singleResult);
|
||||
|
||||
const query = new BatchValuationQuery(['prop-solo']);
|
||||
const result = await handler.execute(query);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]!.propertyId).toBe('prop-solo');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,146 @@
|
||||
import { InternalServerErrorException } from '@nestjs/common';
|
||||
import { type CacheService, type PrismaService } from '@modules/shared';
|
||||
import { DomainException } from '@modules/shared';
|
||||
import { type IAVMService, type ValuationResult } from '../../domain/services/avm-service';
|
||||
import { ValuationComparisonHandler } from '../queries/valuation-comparison/valuation-comparison.handler';
|
||||
import { ValuationComparisonQuery } from '../queries/valuation-comparison/valuation-comparison.query';
|
||||
|
||||
describe('ValuationComparisonHandler', () => {
|
||||
let handler: ValuationComparisonHandler;
|
||||
let mockAvm: { [K in keyof IAVMService]: ReturnType<typeof vi.fn> };
|
||||
let mockPrisma: { property: { findMany: ReturnType<typeof vi.fn> } };
|
||||
let mockLogger: { error: ReturnType<typeof vi.fn> };
|
||||
|
||||
const makeValuation = (price: string, confidence: number, pricePerM2: number): ValuationResult => ({
|
||||
estimatedPrice: price,
|
||||
confidence,
|
||||
pricePerM2,
|
||||
comparables: [
|
||||
{
|
||||
propertyId: 'comp-1',
|
||||
address: '123 Test',
|
||||
district: 'Quận 1',
|
||||
priceVND: price,
|
||||
pricePerM2,
|
||||
areaM2: 70,
|
||||
propertyType: 'APARTMENT' as const,
|
||||
distanceMeters: 200,
|
||||
soldAt: '2026-03-01T00:00:00.000Z',
|
||||
},
|
||||
],
|
||||
modelVersion: 'avm-v1.0',
|
||||
});
|
||||
|
||||
const sampleProperties = [
|
||||
{ id: 'prop-1', address: '10 Nguyễn Huệ', district: 'Quận 1', areaM2: 80, propertyType: 'APARTMENT' },
|
||||
{ id: 'prop-2', address: '20 Lê Lợi', district: 'Quận 3', areaM2: 100, propertyType: 'HOUSE' },
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
mockAvm = {
|
||||
estimateValue: vi.fn(),
|
||||
getComparables: vi.fn(),
|
||||
estimateBatch: vi.fn(),
|
||||
};
|
||||
mockPrisma = {
|
||||
property: { findMany: vi.fn() },
|
||||
};
|
||||
mockLogger = { error: vi.fn() };
|
||||
const mockCache = {
|
||||
getOrSet: vi.fn((_key: string, loader: () => Promise<unknown>) => loader()),
|
||||
} as unknown as CacheService;
|
||||
handler = new ValuationComparisonHandler(
|
||||
mockAvm as any,
|
||||
mockPrisma as unknown as PrismaService,
|
||||
mockCache,
|
||||
mockLogger as any,
|
||||
);
|
||||
});
|
||||
|
||||
it('compares valuations for multiple properties with summary', async () => {
|
||||
mockPrisma.property.findMany.mockResolvedValue(sampleProperties);
|
||||
mockAvm.estimateValue
|
||||
.mockResolvedValueOnce(makeValuation('5000000000', 0.85, 75000000))
|
||||
.mockResolvedValueOnce(makeValuation('8000000000', 0.90, 80000000));
|
||||
|
||||
const query = new ValuationComparisonQuery(['prop-1', 'prop-2']);
|
||||
const result = await handler.execute(query);
|
||||
|
||||
expect(result.properties).toHaveLength(2);
|
||||
expect(result.properties[0]!.propertyId).toBe('prop-1');
|
||||
expect(result.properties[0]!.address).toBe('10 Nguyễn Huệ');
|
||||
expect(result.properties[1]!.propertyId).toBe('prop-2');
|
||||
|
||||
// Summary checks
|
||||
expect(result.summary.highestValue!.propertyId).toBe('prop-2');
|
||||
expect(result.summary.highestValue!.estimatedPrice).toBe('8000000000');
|
||||
expect(result.summary.lowestValue!.propertyId).toBe('prop-1');
|
||||
expect(result.summary.lowestValue!.estimatedPrice).toBe('5000000000');
|
||||
expect(result.summary.averagePricePerM2).toBe(77500000);
|
||||
expect(result.summary.averageConfidence).toBe(0.88);
|
||||
});
|
||||
|
||||
it('handles properties where valuation fails gracefully', async () => {
|
||||
mockPrisma.property.findMany.mockResolvedValue(sampleProperties);
|
||||
mockAvm.estimateValue
|
||||
.mockResolvedValueOnce(makeValuation('5000000000', 0.85, 75000000))
|
||||
.mockRejectedValueOnce(new Error('AI service timeout'));
|
||||
|
||||
const query = new ValuationComparisonQuery(['prop-1', 'prop-2']);
|
||||
const result = await handler.execute(query);
|
||||
|
||||
expect(result.properties).toHaveLength(2);
|
||||
expect(result.properties[0]!.valuation).not.toBeNull();
|
||||
expect(result.properties[1]!.valuation).toBeNull();
|
||||
|
||||
// Summary should only reflect the successful valuation
|
||||
expect(result.summary.highestValue!.propertyId).toBe('prop-1');
|
||||
expect(result.summary.lowestValue!.propertyId).toBe('prop-1');
|
||||
});
|
||||
|
||||
it('returns null summary values when all valuations fail', async () => {
|
||||
mockPrisma.property.findMany.mockResolvedValue(sampleProperties);
|
||||
mockAvm.estimateValue
|
||||
.mockRejectedValueOnce(new Error('fail 1'))
|
||||
.mockRejectedValueOnce(new Error('fail 2'));
|
||||
|
||||
const query = new ValuationComparisonQuery(['prop-1', 'prop-2']);
|
||||
const result = await handler.execute(query);
|
||||
|
||||
expect(result.summary.highestValue).toBeNull();
|
||||
expect(result.summary.lowestValue).toBeNull();
|
||||
expect(result.summary.averagePricePerM2).toBe(0);
|
||||
expect(result.summary.averageConfidence).toBe(0);
|
||||
});
|
||||
|
||||
it('handles unknown property IDs with empty details', async () => {
|
||||
mockPrisma.property.findMany.mockResolvedValue([]);
|
||||
mockAvm.estimateValue.mockRejectedValue(new Error('Not found'));
|
||||
|
||||
const query = new ValuationComparisonQuery(['unknown-1', 'unknown-2']);
|
||||
const result = await handler.execute(query);
|
||||
|
||||
expect(result.properties).toHaveLength(2);
|
||||
expect(result.properties[0]!.address).toBe('');
|
||||
expect(result.properties[0]!.district).toBe('');
|
||||
expect(result.properties[0]!.areaM2).toBe(0);
|
||||
});
|
||||
|
||||
it('re-throws DomainException directly', async () => {
|
||||
const domainError = new DomainException('VALIDATION_ERROR', 'Too many properties');
|
||||
mockPrisma.property.findMany.mockRejectedValue(domainError);
|
||||
|
||||
const query = new ValuationComparisonQuery(['prop-1', 'prop-2']);
|
||||
|
||||
await expect(handler.execute(query)).rejects.toThrow(DomainException);
|
||||
});
|
||||
|
||||
it('wraps unexpected errors in InternalServerErrorException', async () => {
|
||||
mockPrisma.property.findMany.mockRejectedValue(new Error('Connection refused'));
|
||||
|
||||
const query = new ValuationComparisonQuery(['prop-1', 'prop-2']);
|
||||
|
||||
await expect(handler.execute(query)).rejects.toThrow(InternalServerErrorException);
|
||||
expect(mockLogger.error).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,111 @@
|
||||
import { InternalServerErrorException } from '@nestjs/common';
|
||||
import { type CacheService } from '@modules/shared/infrastructure/cache.service';
|
||||
import { DomainException } from '@modules/shared';
|
||||
import { ValuationEntity } from '../../domain/entities/valuation.entity';
|
||||
import { type IValuationRepository } from '../../domain/repositories/valuation.repository';
|
||||
import { ValuationHistoryHandler } from '../queries/valuation-history/valuation-history.handler';
|
||||
import { ValuationHistoryQuery } from '../queries/valuation-history/valuation-history.query';
|
||||
|
||||
describe('ValuationHistoryHandler', () => {
|
||||
let handler: ValuationHistoryHandler;
|
||||
let mockRepo: { [K in keyof IValuationRepository]: ReturnType<typeof vi.fn> };
|
||||
let mockLogger: { error: ReturnType<typeof vi.fn> };
|
||||
|
||||
const createValuationEntity = (
|
||||
id: string,
|
||||
propertyId: string,
|
||||
estimatedPrice: bigint,
|
||||
confidence: number,
|
||||
pricePerM2: number,
|
||||
modelVersion: string,
|
||||
createdAt: Date,
|
||||
): ValuationEntity =>
|
||||
new ValuationEntity(
|
||||
id,
|
||||
{ propertyId, estimatedPrice, confidence, pricePerM2, comparables: [], features: {}, modelVersion },
|
||||
createdAt,
|
||||
createdAt,
|
||||
);
|
||||
|
||||
const sampleEntities = [
|
||||
createValuationEntity('v1', 'prop-1', 5000000000n, 0.85, 75000000, 'avm-v1.0', new Date('2026-04-01')),
|
||||
createValuationEntity('v2', 'prop-1', 5200000000n, 0.88, 78000000, 'avm-v1.1', new Date('2026-03-01')),
|
||||
createValuationEntity('v3', 'prop-1', 4800000000n, 0.82, 72000000, 'avm-v1.0', new Date('2026-02-01')),
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
mockRepo = {
|
||||
findById: vi.fn(),
|
||||
findByPropertyId: vi.fn(),
|
||||
findLatestByPropertyId: vi.fn(),
|
||||
save: vi.fn(),
|
||||
};
|
||||
mockLogger = { error: vi.fn() };
|
||||
const mockCache = {
|
||||
getOrSet: vi.fn((_key: string, loader: () => Promise<unknown>) => loader()),
|
||||
} as unknown as CacheService;
|
||||
handler = new ValuationHistoryHandler(
|
||||
mockRepo as any,
|
||||
mockCache,
|
||||
mockLogger as any,
|
||||
);
|
||||
});
|
||||
|
||||
it('returns valuation history for a property', async () => {
|
||||
mockRepo.findByPropertyId.mockResolvedValue(sampleEntities);
|
||||
|
||||
const query = new ValuationHistoryQuery('prop-1');
|
||||
const result = await handler.execute(query);
|
||||
|
||||
expect(result.propertyId).toBe('prop-1');
|
||||
expect(result.history).toHaveLength(3);
|
||||
expect(result.totalRecords).toBe(3);
|
||||
expect(result.history[0]!.estimatedPrice).toBe('5000000000');
|
||||
expect(result.history[0]!.confidence).toBe(0.85);
|
||||
expect(result.history[0]!.modelVersion).toBe('avm-v1.0');
|
||||
});
|
||||
|
||||
it('respects the limit parameter', async () => {
|
||||
mockRepo.findByPropertyId.mockResolvedValue(sampleEntities);
|
||||
|
||||
const query = new ValuationHistoryQuery('prop-1', 2);
|
||||
const result = await handler.execute(query);
|
||||
|
||||
expect(result.history).toHaveLength(2);
|
||||
expect(result.totalRecords).toBe(3);
|
||||
});
|
||||
|
||||
it('returns empty history when no valuations exist', async () => {
|
||||
mockRepo.findByPropertyId.mockResolvedValue([]);
|
||||
|
||||
const query = new ValuationHistoryQuery('prop-none');
|
||||
const result = await handler.execute(query);
|
||||
|
||||
expect(result.propertyId).toBe('prop-none');
|
||||
expect(result.history).toHaveLength(0);
|
||||
expect(result.totalRecords).toBe(0);
|
||||
});
|
||||
|
||||
it('re-throws DomainException directly', async () => {
|
||||
const domainError = new DomainException('NOT_FOUND', 'Property not found');
|
||||
mockRepo.findByPropertyId.mockRejectedValue(domainError);
|
||||
|
||||
const query = new ValuationHistoryQuery('prop-bad');
|
||||
|
||||
await expect(handler.execute(query)).rejects.toThrow(DomainException);
|
||||
});
|
||||
|
||||
it('wraps unexpected errors in InternalServerErrorException', async () => {
|
||||
mockRepo.findByPropertyId.mockRejectedValue(new Error('DB connection lost'));
|
||||
|
||||
const query = new ValuationHistoryQuery('prop-1');
|
||||
|
||||
await expect(handler.execute(query)).rejects.toThrow(InternalServerErrorException);
|
||||
expect(mockLogger.error).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('uses default limit of 50', () => {
|
||||
const query = new ValuationHistoryQuery('prop-1');
|
||||
expect(query.limit).toBe(50);
|
||||
});
|
||||
});
|
||||
@@ -26,7 +26,7 @@ export class BatchValuationHandler implements IQueryHandler<BatchValuationQuery>
|
||||
...query.propertyIds.slice().sort(),
|
||||
);
|
||||
|
||||
return this.cache.getOrSet(
|
||||
return await this.cache.getOrSet(
|
||||
cacheKey,
|
||||
async () => {
|
||||
const items = query.propertyIds.map((propertyId) => ({ propertyId }));
|
||||
|
||||
@@ -37,7 +37,7 @@ export class ValuationComparisonHandler implements IQueryHandler<ValuationCompar
|
||||
...query.propertyIds.slice().sort(),
|
||||
);
|
||||
|
||||
return this.cache.getOrSet(
|
||||
return await this.cache.getOrSet(
|
||||
cacheKey,
|
||||
() => this.buildComparison(query.propertyIds),
|
||||
CacheTTL.MARKET_DATA,
|
||||
|
||||
@@ -31,7 +31,7 @@ export class ValuationHistoryHandler implements IQueryHandler<ValuationHistoryQu
|
||||
query.limit.toString(),
|
||||
);
|
||||
|
||||
return this.cache.getOrSet(
|
||||
return await this.cache.getOrSet(
|
||||
cacheKey,
|
||||
async () => {
|
||||
const entities = await this.valuationRepo.findByPropertyId(query.propertyId);
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { type QueryBus } from '@nestjs/cqrs';
|
||||
import { BatchValuationQuery } from '../../application/queries/batch-valuation/batch-valuation.query';
|
||||
import { GetDistrictStatsQuery } from '../../application/queries/get-district-stats/get-district-stats.query';
|
||||
import { GetHeatmapQuery } from '../../application/queries/get-heatmap/get-heatmap.query';
|
||||
import { GetMarketReportQuery } from '../../application/queries/get-market-report/get-market-report.query';
|
||||
import { GetPriceTrendQuery } from '../../application/queries/get-price-trend/get-price-trend.query';
|
||||
import { GetValuationQuery } from '../../application/queries/get-valuation/get-valuation.query';
|
||||
import { ValuationComparisonQuery } from '../../application/queries/valuation-comparison/valuation-comparison.query';
|
||||
import { ValuationHistoryQuery } from '../../application/queries/valuation-history/valuation-history.query';
|
||||
import { AnalyticsController } from '../controllers/analytics.controller';
|
||||
|
||||
describe('AnalyticsController', () => {
|
||||
@@ -76,4 +80,80 @@ describe('AnalyticsController', () => {
|
||||
);
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
it('getValuation executes GetValuationQuery with correct params', async () => {
|
||||
const expected = { estimatedPrice: '5000000000', confidence: 0.85 };
|
||||
mockQueryBus.execute.mockResolvedValue(expected);
|
||||
|
||||
const result = await controller.getValuation({
|
||||
propertyId: 'prop-123',
|
||||
latitude: undefined,
|
||||
longitude: undefined,
|
||||
areaM2: undefined,
|
||||
propertyType: undefined,
|
||||
} as any);
|
||||
|
||||
expect(mockQueryBus.execute).toHaveBeenCalledWith(
|
||||
new GetValuationQuery('prop-123', undefined, undefined, undefined, undefined),
|
||||
);
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
it('batchValuation executes BatchValuationQuery with correct params', async () => {
|
||||
const expected = [
|
||||
{ propertyId: 'prop-1', valuation: { estimatedPrice: '5000000000' } },
|
||||
{ propertyId: 'prop-2', valuation: { estimatedPrice: '6000000000' } },
|
||||
];
|
||||
mockQueryBus.execute.mockResolvedValue(expected);
|
||||
|
||||
const result = await controller.batchValuation({
|
||||
propertyIds: ['prop-1', 'prop-2'],
|
||||
} as any);
|
||||
|
||||
expect(mockQueryBus.execute).toHaveBeenCalledWith(
|
||||
new BatchValuationQuery(['prop-1', 'prop-2']),
|
||||
);
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
it('getValuationHistory executes ValuationHistoryQuery with correct params', async () => {
|
||||
const expected = { propertyId: 'prop-1', history: [], totalRecords: 0 };
|
||||
mockQueryBus.execute.mockResolvedValue(expected);
|
||||
|
||||
const result = await controller.getValuationHistory('prop-1', { limit: 25 } as any);
|
||||
|
||||
expect(mockQueryBus.execute).toHaveBeenCalledWith(
|
||||
new ValuationHistoryQuery('prop-1', 25),
|
||||
);
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
it('getValuationHistory defaults limit to 50', async () => {
|
||||
const expected = { propertyId: 'prop-1', history: [], totalRecords: 0 };
|
||||
mockQueryBus.execute.mockResolvedValue(expected);
|
||||
|
||||
const result = await controller.getValuationHistory('prop-1', {} as any);
|
||||
|
||||
expect(mockQueryBus.execute).toHaveBeenCalledWith(
|
||||
new ValuationHistoryQuery('prop-1', 50),
|
||||
);
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
it('compareValuations executes ValuationComparisonQuery with correct params', async () => {
|
||||
const expected = {
|
||||
properties: [],
|
||||
summary: { highestValue: null, lowestValue: null, averagePricePerM2: 0, averageConfidence: 0 },
|
||||
};
|
||||
mockQueryBus.execute.mockResolvedValue(expected);
|
||||
|
||||
const result = await controller.compareValuations({
|
||||
propertyIds: ['prop-1', 'prop-2', 'prop-3'],
|
||||
} as any);
|
||||
|
||||
expect(mockQueryBus.execute).toHaveBeenCalledWith(
|
||||
new ValuationComparisonQuery(['prop-1', 'prop-2', 'prop-3']),
|
||||
);
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,6 +27,18 @@ vi.mock('@/lib/validations/valuation', () => ({
|
||||
{ value: 'Ha Noi', label: 'Hà Nội' },
|
||||
{ value: 'Da Nang', label: 'Đà Nẵng' },
|
||||
],
|
||||
FLOOD_RISK_OPTIONS: [
|
||||
{ value: '0', label: 'An toàn' },
|
||||
{ value: '0.5', label: 'Trung bình' },
|
||||
{ value: '1', label: 'Rất cao' },
|
||||
],
|
||||
QUALITY_LABELS: {
|
||||
renovationScore: 'Mức độ cải tạo',
|
||||
viewQuality: 'Chất lượng view',
|
||||
interiorQuality: 'Nội thất',
|
||||
noiseLevel: 'Mức ồn',
|
||||
naturalLight: 'Ánh sáng tự nhiên',
|
||||
},
|
||||
}));
|
||||
|
||||
function createWrapper() {
|
||||
|
||||
@@ -64,25 +64,25 @@ describe('ValuationResults', () => {
|
||||
|
||||
it('renders price drivers section', () => {
|
||||
render(<ValuationResults result={mockResult} />);
|
||||
expect(screen.getByText('Yếu tố ảnh hưởng giá')).toBeInTheDocument();
|
||||
expect(screen.getByText('Vị trí trung tâm')).toBeInTheDocument();
|
||||
expect(screen.getByText('Tầng thấp')).toBeInTheDocument();
|
||||
expect(screen.getByText('Yếu tố chính')).toBeInTheDocument();
|
||||
expect(screen.getByText(/Vị trí trung tâm/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Tầng thấp/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows positive driver with + sign', () => {
|
||||
render(<ValuationResults result={mockResult} />);
|
||||
expect(screen.getByText('+15.5%')).toBeInTheDocument();
|
||||
expect(screen.getByText(/\+15\.5%/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows negative driver with - sign', () => {
|
||||
render(<ValuationResults result={mockResult} />);
|
||||
expect(screen.getByText('-5.2%')).toBeInTheDocument();
|
||||
expect(screen.getByText(/-5\.2%/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('hides drivers section when empty', () => {
|
||||
const noDrivers = { ...mockResult, priceDrivers: [] };
|
||||
render(<ValuationResults result={noDrivers} />);
|
||||
expect(screen.queryByText('Yếu tố ảnh hưởng giá')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Yếu tố chính')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user