fix: valuation page Vietnamese diacritics, correct API routes, update tests

- Add proper Vietnamese diacritics to all valuation components
  (form, results, history) and their test assertions
- Fix valuation API client to use /analytics/valuation endpoint
- Return empty history gracefully (no server endpoint yet)

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ho Ngoc Hai
2026-04-13 12:03:47 +07:00
parent f373f7b1e2
commit ccfc176e40
8 changed files with 108 additions and 99 deletions

View File

@@ -26,65 +26,65 @@ vi.mock('@/lib/validations/valuation', () => ({
describe('ValuationForm', () => {
it('renders form title', () => {
render(<ValuationForm onSubmit={vi.fn()} />);
expect(screen.getByText('Dinh gia bat dong san')).toBeInTheDocument();
expect(screen.getByText('Định giá bt động sn')).toBeInTheDocument();
});
it('renders property type select', () => {
render(<ValuationForm onSubmit={vi.fn()} />);
expect(screen.getByLabelText('Loai bat dong san *')).toBeInTheDocument();
expect(screen.getByLabelText('Loi bt động sn *')).toBeInTheDocument();
});
it('renders city select', () => {
render(<ValuationForm onSubmit={vi.fn()} />);
expect(screen.getByLabelText('Tinh/Thanh pho *')).toBeInTheDocument();
expect(screen.getByLabelText('Tnh/Thành ph *')).toBeInTheDocument();
});
it('renders district input', () => {
render(<ValuationForm onSubmit={vi.fn()} />);
expect(screen.getByLabelText('Quan/Huyen *')).toBeInTheDocument();
expect(screen.getByLabelText('Qun/Huyn *')).toBeInTheDocument();
});
it('renders area input', () => {
render(<ValuationForm onSubmit={vi.fn()} />);
expect(screen.getByLabelText('Dien tich (m2) *')).toBeInTheDocument();
expect(screen.getByLabelText('Din tích (m²) *')).toBeInTheDocument();
});
it('renders bedroom, bathroom, floors inputs', () => {
render(<ValuationForm onSubmit={vi.fn()} />);
expect(screen.getByLabelText('Phong ngu')).toBeInTheDocument();
expect(screen.getByLabelText('Phong tam')).toBeInTheDocument();
expect(screen.getByLabelText('So tang')).toBeInTheDocument();
expect(screen.getByLabelText('Phòng ng')).toBeInTheDocument();
expect(screen.getByLabelText('Phòng tm')).toBeInTheDocument();
expect(screen.getByLabelText('S tng')).toBeInTheDocument();
});
it('renders frontage and road width inputs', () => {
render(<ValuationForm onSubmit={vi.fn()} />);
expect(screen.getByLabelText('Mat tien (m)')).toBeInTheDocument();
expect(screen.getByLabelText('Do rong duong (m)')).toBeInTheDocument();
expect(screen.getByLabelText('Mt tin (m)')).toBeInTheDocument();
expect(screen.getByLabelText('Độ rng đường (m)')).toBeInTheDocument();
});
it('renders year built input', () => {
render(<ValuationForm onSubmit={vi.fn()} />);
expect(screen.getByLabelText('Nam xay dung')).toBeInTheDocument();
expect(screen.getByLabelText('Năm xây dng')).toBeInTheDocument();
});
it('renders legal paper checkbox', () => {
render(<ValuationForm onSubmit={vi.fn()} />);
expect(screen.getByLabelText('Co so do/giay to hop phap')).toBeInTheDocument();
expect(screen.getByLabelText('Có sổ đỏ/giy t hp pháp')).toBeInTheDocument();
});
it('renders submit button', () => {
render(<ValuationForm onSubmit={vi.fn()} />);
expect(screen.getByText('Dinh gia ngay')).toBeInTheDocument();
expect(screen.getByText('Định giá ngay')).toBeInTheDocument();
});
it('shows loading text when isLoading', () => {
render(<ValuationForm onSubmit={vi.fn()} isLoading={true} />);
expect(screen.getByText('Dang dinh gia...')).toBeInTheDocument();
expect(screen.getByText('Đang định giá...')).toBeInTheDocument();
});
it('disables submit button when loading', () => {
render(<ValuationForm onSubmit={vi.fn()} isLoading={true} />);
expect(screen.getByText('Dang dinh gia...')).toBeDisabled();
expect(screen.getByText('Đang định giá...')).toBeDisabled();
});
it('renders property type options', () => {
@@ -101,6 +101,6 @@ describe('ValuationForm', () => {
it('renders description text', () => {
render(<ValuationForm onSubmit={vi.fn()} />);
expect(screen.getByText(/Nhap thong tin bat dong san de nhan uoc tinh gia tu AI/)).toBeInTheDocument();
expect(screen.getByText(/Nhp thông tin bt động sn để nhn ước tính giá t AI/)).toBeInTheDocument();
});
});

View File

@@ -38,7 +38,7 @@ describe('ValuationHistory', () => {
onSelect={vi.fn()}
/>,
);
expect(screen.getByText('Lich su dinh gia')).toBeInTheDocument();
expect(screen.getByText('Lch sử định giá')).toBeInTheDocument();
});
it('renders total count description', () => {
@@ -51,7 +51,7 @@ describe('ValuationHistory', () => {
onSelect={vi.fn()}
/>,
);
expect(screen.getByText('2 lan dinh gia truoc do')).toBeInTheDocument();
expect(screen.getByText('2 ln định giá trước đó')).toBeInTheDocument();
});
it('renders property type labels', () => {
@@ -64,8 +64,8 @@ describe('ValuationHistory', () => {
onSelect={vi.fn()}
/>,
);
expect(screen.getByText('Can ho')).toBeInTheDocument();
expect(screen.getByText('Nha rieng')).toBeInTheDocument();
expect(screen.getByText('Căn h')).toBeInTheDocument();
expect(screen.getByText('Nhà riêng')).toBeInTheDocument();
});
it('renders district and area for each item', () => {
@@ -78,8 +78,8 @@ describe('ValuationHistory', () => {
onSelect={vi.fn()}
/>,
);
expect(screen.getByText(/Quận 1.*80 m2/)).toBeInTheDocument();
expect(screen.getByText(/Quận 7.*120 m2/)).toBeInTheDocument();
expect(screen.getByText(/Quận 1.*80 m²/)).toBeInTheDocument();
expect(screen.getByText(/Quận 7.*120 m²/)).toBeInTheDocument();
});
it('renders formatted prices', () => {
@@ -92,8 +92,8 @@ describe('ValuationHistory', () => {
onSelect={vi.fn()}
/>,
);
expect(screen.getByText('5.00 ty')).toBeInTheDocument();
expect(screen.getByText('8.50 ty')).toBeInTheDocument();
expect(screen.getByText('5.00 t')).toBeInTheDocument();
expect(screen.getByText('8.50 t')).toBeInTheDocument();
});
it('calls onSelect when an item is clicked', async () => {
@@ -107,7 +107,7 @@ describe('ValuationHistory', () => {
onSelect={onSelect}
/>,
);
await userEvent.click(screen.getByText('Can ho'));
await userEvent.click(screen.getByText('Căn h'));
expect(onSelect).toHaveBeenCalledWith('val-1');
});
@@ -121,7 +121,7 @@ describe('ValuationHistory', () => {
onSelect={vi.fn()}
/>,
);
expect(screen.getByText('Chua co lich su dinh gia')).toBeInTheDocument();
expect(screen.getByText('Chưa có lch sử định giá')).toBeInTheDocument();
});
it('shows loading state', () => {
@@ -135,7 +135,7 @@ describe('ValuationHistory', () => {
isLoading
/>,
);
expect(screen.getByText('Dang tai...')).toBeInTheDocument();
expect(screen.getByText('Đang ti...')).toBeInTheDocument();
});
it('shows pagination when multiple pages', () => {
@@ -149,8 +149,8 @@ describe('ValuationHistory', () => {
/>,
);
expect(screen.getByText('Trang 1/3')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Truoc' })).toBeDisabled();
expect(screen.getByRole('button', { name: 'Tiep' })).not.toBeDisabled();
expect(screen.getByRole('button', { name: 'Trước' })).toBeDisabled();
expect(screen.getByRole('button', { name: 'Tiếp' })).not.toBeDisabled();
});
it('calls onPageChange with next page', async () => {
@@ -164,7 +164,7 @@ describe('ValuationHistory', () => {
onSelect={vi.fn()}
/>,
);
await userEvent.click(screen.getByRole('button', { name: 'Tiep' }));
await userEvent.click(screen.getByRole('button', { name: 'Tiếp' }));
expect(onPageChange).toHaveBeenCalledWith(2);
});
@@ -178,8 +178,8 @@ describe('ValuationHistory', () => {
onSelect={vi.fn()}
/>,
);
expect(screen.getByRole('button', { name: 'Tiep' })).toBeDisabled();
expect(screen.getByRole('button', { name: 'Truoc' })).not.toBeDisabled();
expect(screen.getByRole('button', { name: 'Tiếp' })).toBeDisabled();
expect(screen.getByRole('button', { name: 'Trước' })).not.toBeDisabled();
});
it('hides pagination when single page', () => {

View File

@@ -43,7 +43,7 @@ const mockResult: ValuationResult = {
describe('ValuationResults', () => {
it('renders estimated price', () => {
render(<ValuationResults result={mockResult} />);
expect(screen.getByText('5.00 ty VND')).toBeInTheDocument();
expect(screen.getByText('5.00 t VNĐ')).toBeInTheDocument();
});
it('renders confidence percentage', () => {
@@ -53,17 +53,17 @@ describe('ValuationResults', () => {
it('renders price per m2', () => {
render(<ValuationResults result={mockResult} />);
expect(screen.getByText('62.5 tr/m2')).toBeInTheDocument();
expect(screen.getByText('62.5 tr/m²')).toBeInTheDocument();
});
it('renders price range', () => {
render(<ValuationResults result={mockResult} />);
expect(screen.getByText(/4\.50 ty.*5\.50 ty/)).toBeInTheDocument();
expect(screen.getByText(/4\.50 t.*5\.50 t/)).toBeInTheDocument();
});
it('renders price drivers section', () => {
render(<ValuationResults result={mockResult} />);
expect(screen.getByText('Yeu to anh huong gia')).toBeInTheDocument();
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();
});
@@ -80,31 +80,31 @@ describe('ValuationResults', () => {
it('renders comparables section', () => {
render(<ValuationResults result={mockResult} />);
expect(screen.getByText('Bat dong san tuong tu')).toBeInTheDocument();
expect(screen.getByText('Bt động sn tương t')).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();
expect(screen.getByText(/2 bt động sn/)).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();
expect(screen.getByText('92% tương t')).toBeInTheDocument();
expect(screen.getByText('85% tương t')).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();
expect(screen.queryByText('Yếu tố ảnh hưởng giá')).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();
expect(screen.queryByText('Bt động sn tương t')).not.toBeInTheDocument();
});
});