- 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>
201 lines
6.1 KiB
TypeScript
201 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, PaginatedResult } from '@/lib/listings-api';
|
|
import { SearchResults } from '../search-results';
|
|
|
|
// Mock PropertyCard
|
|
vi.mock('../property-card', () => ({
|
|
PropertyCard: ({ listing }: { listing: ListingDetail }) => (
|
|
<div data-testid={`property-card-${listing.id}`}>{listing.property.title}</div>
|
|
),
|
|
}));
|
|
|
|
function makeListing(id: string): 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: `Listing ${id}`,
|
|
description: 'Test listing',
|
|
address: '123 Test St',
|
|
ward: 'Ward',
|
|
district: 'District',
|
|
city: 'HCMC',
|
|
areaM2: 75,
|
|
bedrooms: 2,
|
|
bathrooms: 2,
|
|
floors: null,
|
|
direction: null,
|
|
yearBuilt: null,
|
|
legalStatus: null,
|
|
amenities: [],
|
|
projectName: null,
|
|
latitude: null,
|
|
longitude: null,
|
|
media: [],
|
|
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,
|
|
};
|
|
}
|
|
|
|
function makeResult(count: number, page = 1, totalPages = 1): PaginatedResult<ListingDetail> {
|
|
return {
|
|
data: Array.from({ length: count }, (_, i) => makeListing(`${i + 1}`)),
|
|
total: count,
|
|
page,
|
|
limit: 10,
|
|
totalPages,
|
|
};
|
|
}
|
|
|
|
const defaultProps = {
|
|
page: 1,
|
|
sort: '',
|
|
onPageChange: vi.fn(),
|
|
onSortChange: vi.fn(),
|
|
};
|
|
|
|
describe('SearchResults', () => {
|
|
it('renders loading spinner when loading', () => {
|
|
const { container } = render(
|
|
<SearchResults result={null} loading={true} {...defaultProps} />,
|
|
);
|
|
expect(container.querySelector('.animate-spin')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders error state', () => {
|
|
render(
|
|
<SearchResults result={null} loading={false} error={true} {...defaultProps} />,
|
|
);
|
|
expect(screen.getByText('Không thể tải kết quả tìm kiếm')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders retry button in error state', () => {
|
|
const onRetry = vi.fn();
|
|
render(
|
|
<SearchResults result={null} loading={false} error={true} onRetry={onRetry} {...defaultProps} />,
|
|
);
|
|
expect(screen.getByText('Thử lại')).toBeInTheDocument();
|
|
});
|
|
|
|
it('calls onRetry when retry button clicked', async () => {
|
|
const user = userEvent.setup();
|
|
const onRetry = vi.fn();
|
|
render(
|
|
<SearchResults result={null} loading={false} error={true} onRetry={onRetry} {...defaultProps} />,
|
|
);
|
|
|
|
await user.click(screen.getByText('Thử lại'));
|
|
expect(onRetry).toHaveBeenCalled();
|
|
});
|
|
|
|
it('renders empty state when no results', () => {
|
|
render(
|
|
<SearchResults result={makeResult(0)} loading={false} {...defaultProps} />,
|
|
);
|
|
expect(screen.getByText('Không tìm thấy kết quả')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders empty state with null result', () => {
|
|
render(
|
|
<SearchResults result={null} loading={false} {...defaultProps} />,
|
|
);
|
|
expect(screen.getByText('Không tìm thấy kết quả')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders property cards for results', () => {
|
|
render(
|
|
<SearchResults result={makeResult(3)} loading={false} {...defaultProps} />,
|
|
);
|
|
expect(screen.getByTestId('property-card-1')).toBeInTheDocument();
|
|
expect(screen.getByTestId('property-card-2')).toBeInTheDocument();
|
|
expect(screen.getByTestId('property-card-3')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders total results count', () => {
|
|
render(
|
|
<SearchResults result={makeResult(3)} loading={false} {...defaultProps} />,
|
|
);
|
|
expect(screen.getByText('3 kết quả')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders sort select', () => {
|
|
render(
|
|
<SearchResults result={makeResult(3)} loading={false} {...defaultProps} />,
|
|
);
|
|
expect(screen.getByText('Mới nhất')).toBeInTheDocument();
|
|
expect(screen.getByText('Giá: Thấp đến cao')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders pagination buttons for multi-page results', () => {
|
|
render(
|
|
<SearchResults result={makeResult(3, 1, 3)} loading={false} {...defaultProps} />,
|
|
);
|
|
expect(screen.getByText('Trước')).toBeInTheDocument();
|
|
expect(screen.getByText('Tiếp')).toBeInTheDocument();
|
|
});
|
|
|
|
it('disables Trước button on first page', () => {
|
|
render(
|
|
<SearchResults result={makeResult(3, 1, 3)} loading={false} page={1} sort="" onPageChange={vi.fn()} onSortChange={vi.fn()} />,
|
|
);
|
|
expect(screen.getByText('Trước')).toBeDisabled();
|
|
});
|
|
|
|
it('disables Tiếp button on last page', () => {
|
|
render(
|
|
<SearchResults result={makeResult(3, 3, 3)} loading={false} page={3} sort="" onPageChange={vi.fn()} onSortChange={vi.fn()} />,
|
|
);
|
|
expect(screen.getByText('Tiếp')).toBeDisabled();
|
|
});
|
|
|
|
it('calls onPageChange when Tiếp clicked', async () => {
|
|
const user = userEvent.setup();
|
|
const onPageChange = vi.fn();
|
|
render(
|
|
<SearchResults result={makeResult(3, 1, 3)} loading={false} page={1} sort="" onPageChange={onPageChange} onSortChange={vi.fn()} />,
|
|
);
|
|
|
|
await user.click(screen.getByText('Tiếp'));
|
|
expect(onPageChange).toHaveBeenCalledWith(2);
|
|
});
|
|
|
|
it('does not render pagination for single page', () => {
|
|
render(
|
|
<SearchResults result={makeResult(3, 1, 1)} loading={false} {...defaultProps} />,
|
|
);
|
|
expect(screen.queryByText('Trước')).not.toBeInTheDocument();
|
|
});
|
|
});
|