- Add TOTP-based MFA with setup, verify, disable, backup codes, and challenge flow - Add PII field encryption middleware with AES-256-GCM and deterministic search hashes - Add agents, inquiries, and leads domain modules with entities, events, value objects - Add web dashboard pages for inquiries and leads with detail dialogs - Add 30+ component tests (valuation, charts, listings, search, providers, UI) - Add Prisma migrations for encryption hash columns and MFA TOTP support - Fix all ESLint errors (unused imports, duplicate imports, lint auto-fixes) - Update dependencies and lock file - Clean up obsolete exploration/QA docs, add audit documentation Co-Authored-By: Paperclip <noreply@paperclip.ing>
111 lines
3.7 KiB
TypeScript
111 lines
3.7 KiB
TypeScript
import { render, screen } from '@testing-library/react';
|
|
import { describe, expect, it } from 'vitest';
|
|
import type { ValuationResult } from '@/lib/valuation-api';
|
|
import { ValuationResults } from '../valuation-results';
|
|
|
|
const mockResult: ValuationResult = {
|
|
id: 'val-1',
|
|
estimatedPriceVND: 5_000_000_000,
|
|
confidence: 0.87,
|
|
pricePerM2: 62_500_000,
|
|
priceRangeLow: 4_500_000_000,
|
|
priceRangeHigh: 5_500_000_000,
|
|
comparables: [
|
|
{
|
|
id: 'comp-1',
|
|
title: 'Căn hộ tương tự A',
|
|
address: '456 Nguyễn Hữu Thọ',
|
|
district: 'Quận 7',
|
|
priceVND: '4800000000',
|
|
areaM2: 78,
|
|
pricePerM2: 61_500_000,
|
|
similarity: 0.92,
|
|
},
|
|
{
|
|
id: 'comp-2',
|
|
title: 'Căn hộ tương tự B',
|
|
address: '789 Phạm Viết Chánh',
|
|
district: 'Bình Thạnh',
|
|
priceVND: '5200000000',
|
|
areaM2: 82,
|
|
pricePerM2: 63_400_000,
|
|
similarity: 0.85,
|
|
},
|
|
],
|
|
priceDrivers: [
|
|
{ feature: 'Vị trí trung tâm', impact: 15.5, direction: 'positive' },
|
|
{ feature: 'Tầng thấp', impact: -5.2, direction: 'negative' },
|
|
],
|
|
modelVersion: 'v1.0',
|
|
createdAt: '2026-01-15T10:00:00Z',
|
|
};
|
|
|
|
describe('ValuationResults', () => {
|
|
it('renders estimated price', () => {
|
|
render(<ValuationResults result={mockResult} />);
|
|
expect(screen.getByText('5.00 ty VND')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders confidence percentage', () => {
|
|
render(<ValuationResults result={mockResult} />);
|
|
expect(screen.getByText('87%')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders price per m2', () => {
|
|
render(<ValuationResults result={mockResult} />);
|
|
expect(screen.getByText('62.5 tr/m2')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders price range', () => {
|
|
render(<ValuationResults result={mockResult} />);
|
|
expect(screen.getByText(/4\.50 ty.*5\.50 ty/)).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders price drivers section', () => {
|
|
render(<ValuationResults result={mockResult} />);
|
|
expect(screen.getByText('Yeu to anh huong gia')).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();
|
|
});
|
|
|
|
it('shows negative driver with - sign', () => {
|
|
render(<ValuationResults result={mockResult} />);
|
|
expect(screen.getByText('-5.2%')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders comparables section', () => {
|
|
render(<ValuationResults result={mockResult} />);
|
|
expect(screen.getByText('Bat dong san tuong tu')).toBeInTheDocument();
|
|
expect(screen.getByText('Căn hộ tương tự A')).toBeInTheDocument();
|
|
expect(screen.getByText('Căn hộ tương tự B')).toBeInTheDocument();
|
|
});
|
|
|
|
it('shows comparable count', () => {
|
|
render(<ValuationResults result={mockResult} />);
|
|
expect(screen.getByText(/2 bat dong san/)).toBeInTheDocument();
|
|
});
|
|
|
|
it('shows similarity percentage for comparables', () => {
|
|
render(<ValuationResults result={mockResult} />);
|
|
expect(screen.getByText('92% tuong tu')).toBeInTheDocument();
|
|
expect(screen.getByText('85% tuong tu')).toBeInTheDocument();
|
|
});
|
|
|
|
it('hides drivers section when empty', () => {
|
|
const noDrivers = { ...mockResult, priceDrivers: [] };
|
|
render(<ValuationResults result={noDrivers} />);
|
|
expect(screen.queryByText('Yeu to anh huong gia')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('hides comparables section when empty', () => {
|
|
const noComps = { ...mockResult, comparables: [] };
|
|
render(<ValuationResults result={noComps} />);
|
|
expect(screen.queryByText('Bat dong san tuong tu')).not.toBeInTheDocument();
|
|
});
|
|
});
|