- Fix TS4111: use bracket notation for index signature access in metadata.spec.ts, neighborhood-poi-map.tsx, and neighborhood-poi-map.spec.tsx - Fix TS2740: add missing property fields (usableAreaM2, floor, totalFloors, nearbyPOIs, etc.) to test mock objects in 5 spec files - Fix TS2339: add missing estimate() and create() methods to transferApi - Fix TS4114: add override modifier to render() in page.tsx error boundary - Fix TS2532: add optional chaining for possibly undefined features in neighborhood-poi-map.tsx Co-Authored-By: Paperclip <noreply@paperclip.ing>
196 lines
6.1 KiB
TypeScript
196 lines
6.1 KiB
TypeScript
import { render, screen } from '@testing-library/react';
|
|
import userEvent from '@testing-library/user-event';
|
|
import { describe, expect, it, vi } from 'vitest';
|
|
import type { ListingDetail } from '@/lib/listings-api';
|
|
import { ComparisonTable } from '../comparison-table';
|
|
|
|
// Mock next/image
|
|
vi.mock('next/image', () => ({
|
|
default: (props: Record<string, unknown>) => <img {...props} />,
|
|
}));
|
|
|
|
// Mock next-intl
|
|
vi.mock('next-intl', () => ({
|
|
useTranslations: () => (key: string) => {
|
|
const translations: Record<string, string> = {
|
|
property: 'Bất động sản',
|
|
price: 'Giá',
|
|
transactionType: 'Loại giao dịch',
|
|
propertyType: 'Loại BĐS',
|
|
area: 'Diện tích',
|
|
pricePerM2: 'Giá/m²',
|
|
bedrooms: 'Phòng ngủ',
|
|
bathrooms: 'Phòng tắm',
|
|
direction: 'Hướng',
|
|
floors: 'Số tầng',
|
|
yearBuilt: 'Năm xây',
|
|
legalStatus: 'Pháp lý',
|
|
location: 'Vị trí',
|
|
amenities: 'Tiện ích',
|
|
projectName: 'Dự án',
|
|
rooms: 'phòng',
|
|
remove: 'Xóa',
|
|
noImage: 'Chưa có ảnh',
|
|
sale: 'Bán',
|
|
rent: 'Cho thuê',
|
|
};
|
|
return translations[key] ?? key;
|
|
},
|
|
}));
|
|
|
|
// Mock @/i18n/navigation
|
|
vi.mock('@/i18n/navigation', () => ({
|
|
Link: ({ children, href }: { children: React.ReactNode; href: string }) => (
|
|
<a href={href}>{children}</a>
|
|
),
|
|
}));
|
|
|
|
// Mock currency
|
|
vi.mock('@/lib/currency', () => ({
|
|
formatPrice: (price: string) => {
|
|
const n = Number(price);
|
|
return n >= 1_000_000_000 ? `${(n / 1_000_000_000).toFixed(1)} tỷ` : String(n);
|
|
},
|
|
formatPricePerM2: (price: number) => `${(price / 1_000_000).toFixed(1)} tr/m²`,
|
|
}));
|
|
|
|
// Mock image-blur
|
|
vi.mock('@/lib/image-blur', () => ({
|
|
shimmerBlurDataURL: () => 'data:image/svg+xml;base64,mock',
|
|
}));
|
|
|
|
// Mock lucide-react
|
|
vi.mock('lucide-react', () => ({
|
|
X: () => <span data-testid="x-icon">X</span>,
|
|
}));
|
|
|
|
function makeListing(id: string, overrides: Partial<ListingDetail> = {}): ListingDetail {
|
|
return {
|
|
id,
|
|
status: 'ACTIVE',
|
|
transactionType: 'SALE',
|
|
priceVND: '3500000000',
|
|
pricePerM2: 40_000_000,
|
|
rentPriceMonthly: null,
|
|
commissionPct: null,
|
|
viewCount: 100,
|
|
saveCount: 10,
|
|
inquiryCount: 5,
|
|
publishedAt: '2026-01-01T00:00:00Z',
|
|
createdAt: '2025-12-01T00:00:00Z',
|
|
valuationEstimate: null,
|
|
agentQualityScore: null,
|
|
similarCount: 0,
|
|
property: {
|
|
id: `prop-${id}`,
|
|
propertyType: 'APARTMENT',
|
|
title: `Căn hộ ${id}`,
|
|
description: 'Test',
|
|
address: '123 Test St',
|
|
ward: 'Ward',
|
|
district: 'Quận 1',
|
|
city: 'HCMC',
|
|
areaM2: 75,
|
|
bedrooms: 2,
|
|
bathrooms: 2,
|
|
floors: null,
|
|
direction: 'SOUTH',
|
|
yearBuilt: 2020,
|
|
legalStatus: 'Sổ hồng',
|
|
amenities: ['Gym', 'Pool'],
|
|
projectName: 'Vinhomes',
|
|
latitude: null,
|
|
longitude: null,
|
|
media: [{ id: 'm1', type: 'image', url: 'https://example.com/img.jpg', order: 0, caption: null }],
|
|
usableAreaM2: null,
|
|
floor: null,
|
|
totalFloors: null,
|
|
nearbyPOIs: null,
|
|
metroDistanceM: null,
|
|
furnishing: null,
|
|
propertyCondition: null,
|
|
balconyDirection: null,
|
|
maintenanceFeeVND: null,
|
|
parkingSlots: null,
|
|
viewType: [],
|
|
petFriendly: null,
|
|
suitableFor: [],
|
|
whyThisLocation: null,
|
|
},
|
|
seller: { id: 's1', fullName: 'Seller', phone: '0901234567' },
|
|
agent: null,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('ComparisonTable', () => {
|
|
it('returns null when listings are empty', () => {
|
|
const { container } = render(<ComparisonTable listings={[]} onRemove={vi.fn()} />);
|
|
expect(container.firstChild).toBeNull();
|
|
});
|
|
|
|
it('renders table with listings', () => {
|
|
render(<ComparisonTable listings={[makeListing('1'), makeListing('2')]} onRemove={vi.fn()} />);
|
|
expect(screen.getByText('Căn hộ 1')).toBeInTheDocument();
|
|
expect(screen.getByText('Căn hộ 2')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders comparison rows', () => {
|
|
render(<ComparisonTable listings={[makeListing('1')]} onRemove={vi.fn()} />);
|
|
expect(screen.getByText('Giá')).toBeInTheDocument();
|
|
expect(screen.getByText('Loại giao dịch')).toBeInTheDocument();
|
|
expect(screen.getByText('Loại BĐS')).toBeInTheDocument();
|
|
expect(screen.getByText('Diện tích')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders property area', () => {
|
|
render(<ComparisonTable listings={[makeListing('1')]} onRemove={vi.fn()} />);
|
|
expect(screen.getByText('75 m²')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders remove button', () => {
|
|
render(<ComparisonTable listings={[makeListing('1')]} onRemove={vi.fn()} />);
|
|
expect(screen.getByText('Xóa')).toBeInTheDocument();
|
|
});
|
|
|
|
it('calls onRemove when remove clicked', async () => {
|
|
const user = userEvent.setup();
|
|
const onRemove = vi.fn();
|
|
render(<ComparisonTable listings={[makeListing('1')]} onRemove={onRemove} />);
|
|
|
|
await user.click(screen.getByText('Xóa'));
|
|
expect(onRemove).toHaveBeenCalledWith('1');
|
|
});
|
|
|
|
it('renders direction value', () => {
|
|
render(<ComparisonTable listings={[makeListing('1')]} onRemove={vi.fn()} />);
|
|
expect(screen.getByText('Nam')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders amenities', () => {
|
|
render(<ComparisonTable listings={[makeListing('1')]} onRemove={vi.fn()} />);
|
|
expect(screen.getByText('Gym')).toBeInTheDocument();
|
|
expect(screen.getByText('Pool')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders project name', () => {
|
|
render(<ComparisonTable listings={[makeListing('1')]} onRemove={vi.fn()} />);
|
|
expect(screen.getByText('Vinhomes')).toBeInTheDocument();
|
|
});
|
|
|
|
it('shows "—" for missing values', () => {
|
|
const listing = makeListing('1');
|
|
listing.property.floors = null;
|
|
render(<ComparisonTable listings={[listing]} onRemove={vi.fn()} />);
|
|
// floors row should have —
|
|
const dashes = screen.getAllByText('—');
|
|
expect(dashes.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('renders bedrooms with room suffix', () => {
|
|
render(<ComparisonTable listings={[makeListing('1')]} onRemove={vi.fn()} />);
|
|
// Bedrooms and bathrooms both show "2 phòng"
|
|
expect(screen.getAllByText('2 phòng').length).toBeGreaterThanOrEqual(1);
|
|
});
|
|
});
|