test(web): add component tests for 10 untested frontend components (GOO-54)
Cover critical-path and feature components that were missing tests: - charts: district-heatmap - chuyen-nhuong: detail-client, transfer-wizard-client - du-an: detail-client, project-ai-advice-card, project-map - khu-cong-nghiep: detail-client, listing-search-client, park-compare-client, park-map All 49 new tests pass with Vitest + React Testing Library. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
131
apps/web/components/du-an/__tests__/du-an-detail-client.spec.tsx
Normal file
131
apps/web/components/du-an/__tests__/du-an-detail-client.spec.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
vi.mock('next/dynamic', () => ({
|
||||
__esModule: true,
|
||||
default: () => {
|
||||
const Stub = () => <div data-testid="dynamic-stub" />;
|
||||
Stub.displayName = 'DynamicStub';
|
||||
return Stub;
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('next/image', () => ({
|
||||
__esModule: true,
|
||||
default: (props: Record<string, unknown>) => <img {...props} />,
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/analytics-api', () => ({
|
||||
analyticsApi: {
|
||||
getNearbyPOIs: vi.fn().mockResolvedValue({ pois: [] }),
|
||||
getProjectAiAdvice: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/listings-api', () => ({
|
||||
listingsApi: { getNeighborhoodScore: vi.fn().mockRejectedValue(new Error('noop')) },
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/du-an-api', () => ({
|
||||
PROJECT_PROPERTY_TYPE_LABELS: { APARTMENT: 'Căn hộ', VILLA: 'Biệt thự' },
|
||||
PROJECT_STATUS_COLORS: { PRE_SALE: 'bg-blue-100 text-blue-800', SELLING: 'bg-green-100 text-green-800' },
|
||||
PROJECT_STATUS_LABELS: { PRE_SALE: 'Mở bán sắp tới', SELLING: 'Đang bán' },
|
||||
duAnApi: { submitInquiry: vi.fn() },
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/currency', () => ({
|
||||
formatPrice: (v: string | number) => `${v} VNĐ`,
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/project-personas', () => ({
|
||||
composeWhyThisProject: () => null,
|
||||
deriveProjectPersonas: () => [],
|
||||
}));
|
||||
|
||||
vi.mock('@/components/du-an/project-ai-advice-card', () => ({
|
||||
ProjectAiAdviceCard: () => <div data-testid="ai-advice-stub" />,
|
||||
}));
|
||||
|
||||
vi.mock('@/components/listings/image-gallery', () => ({
|
||||
ImageGallery: () => <div data-testid="gallery-stub" />,
|
||||
}));
|
||||
|
||||
import { DuAnDetailClient } from '../du-an-detail-client';
|
||||
|
||||
const project = {
|
||||
id: 'p1',
|
||||
name: 'Vinhomes Grand Park',
|
||||
slug: 'vinhomes-grand-park',
|
||||
status: 'SELLING' as const,
|
||||
propertyTypes: ['APARTMENT' as const],
|
||||
address: '128 Nguyễn Xiển',
|
||||
district: 'Quận 9',
|
||||
city: 'Hồ Chí Minh',
|
||||
latitude: 10.84,
|
||||
longitude: 106.83,
|
||||
description: 'Dự án lớn nhất TP.HCM',
|
||||
totalArea: 271000,
|
||||
totalUnits: 43000,
|
||||
minPrice: '2000000000',
|
||||
completionDate: '2025-12-01',
|
||||
developer: { name: 'Vingroup', logoUrl: null, totalProjects: 12 },
|
||||
blocks: [{ id: 'b1', name: 'S1', totalUnits: 500, availableUnits: 20, floors: 35 }],
|
||||
amenities: [{ id: 'a1', name: 'Hồ bơi', category: 'Thể thao' }],
|
||||
priceRanges: [],
|
||||
priceHistory: [],
|
||||
media: [],
|
||||
linkedListingCount: 5,
|
||||
pois: [],
|
||||
neighborhoodScores: [],
|
||||
documents: [],
|
||||
suitableFor: [],
|
||||
whyThisLocation: null,
|
||||
} as never;
|
||||
|
||||
describe('DuAnDetailClient', () => {
|
||||
it('renders project name', () => {
|
||||
render(<DuAnDetailClient project={project} />);
|
||||
expect(screen.getByText('Vinhomes Grand Park')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders status badge', () => {
|
||||
render(<DuAnDetailClient project={project} />);
|
||||
expect(screen.getByText('Đang bán')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders property type badge', () => {
|
||||
render(<DuAnDetailClient project={project} />);
|
||||
expect(screen.getByText('Căn hộ')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders developer name', () => {
|
||||
render(<DuAnDetailClient project={project} />);
|
||||
expect(screen.getAllByText('Vingroup').length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('renders quick stats', () => {
|
||||
render(<DuAnDetailClient project={project} />);
|
||||
expect(screen.getByText(/271\.000/)).toBeInTheDocument(); // total area
|
||||
expect(screen.getByText('43000')).toBeInTheDocument(); // total units
|
||||
});
|
||||
|
||||
it('renders blocks section', () => {
|
||||
render(<DuAnDetailClient project={project} />);
|
||||
expect(screen.getByText('Phân khu / Block')).toBeInTheDocument();
|
||||
expect(screen.getByText('S1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders inquiry form', () => {
|
||||
render(<DuAnDetailClient project={project} />);
|
||||
expect(screen.getAllByText('Nhận tư vấn').length).toBeGreaterThan(0);
|
||||
expect(screen.getByLabelText('Họ tên')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Số điện thoại')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders tabs', () => {
|
||||
render(<DuAnDetailClient project={project} />);
|
||||
expect(screen.getByRole('tab', { name: 'Tiện ích' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('tab', { name: 'Vị trí' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('tab', { name: 'Giá' })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
|
||||
vi.mock('@/lib/analytics-api', () => ({
|
||||
analyticsApi: { getProjectAiAdvice: vi.fn().mockReturnValue(new Promise(() => {})) },
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/api-client', () => ({
|
||||
ApiError: class extends Error { status = 500; },
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/auth-store', () => ({
|
||||
useAuthStore: (selector: (s: { user: null }) => unknown) => selector({ user: null }),
|
||||
}));
|
||||
|
||||
import { ProjectAiAdviceCard } from '../project-ai-advice-card';
|
||||
|
||||
function wrapper({ children }: { children: React.ReactNode }) {
|
||||
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
return <QueryClientProvider client={qc}>{children}</QueryClientProvider>;
|
||||
}
|
||||
|
||||
describe('ProjectAiAdviceCard', () => {
|
||||
it('renders trigger button initially', () => {
|
||||
render(<ProjectAiAdviceCard projectId="p1" />, { wrapper });
|
||||
expect(screen.getByText('Xem phân tích AI về dự án')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows loading state when clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<ProjectAiAdviceCard projectId="p1" />, { wrapper });
|
||||
await user.click(screen.getByText('Xem phân tích AI về dự án'));
|
||||
expect(screen.getByText(/AI đang phân tích/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
73
apps/web/components/du-an/__tests__/project-map.spec.tsx
Normal file
73
apps/web/components/du-an/__tests__/project-map.spec.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
vi.mock('mapbox-gl', () => {
|
||||
class MockMap {
|
||||
addControl = vi.fn();
|
||||
fitBounds = vi.fn();
|
||||
flyTo = vi.fn();
|
||||
setStyle = vi.fn();
|
||||
remove = vi.fn();
|
||||
on = vi.fn();
|
||||
}
|
||||
class MockNavigationControl {}
|
||||
class MockAttributionControl {}
|
||||
class MockMarker {
|
||||
setLngLat() { return this; }
|
||||
setPopup() { return this; }
|
||||
addTo() { return this; }
|
||||
remove() {}
|
||||
}
|
||||
class MockPopup {
|
||||
setHTML() { return this; }
|
||||
setLngLat() { return this; }
|
||||
addTo() { return this; }
|
||||
remove() {}
|
||||
}
|
||||
class MockLngLatBounds {
|
||||
extend() { return this; }
|
||||
isEmpty() { return false; }
|
||||
}
|
||||
return {
|
||||
default: {
|
||||
accessToken: '',
|
||||
Map: MockMap,
|
||||
NavigationControl: MockNavigationControl,
|
||||
AttributionControl: MockAttributionControl,
|
||||
Marker: MockMarker,
|
||||
Popup: MockPopup,
|
||||
LngLatBounds: MockLngLatBounds,
|
||||
},
|
||||
};
|
||||
});
|
||||
vi.mock('mapbox-gl/dist/mapbox-gl.css', () => ({}));
|
||||
vi.mock('@/lib/mapbox-style', () => ({ useMapboxStyle: () => 'mapbox://styles/mapbox/light-v11' }));
|
||||
vi.mock('@/lib/currency', () => ({ formatPrice: (v: string | number) => `${v} VNĐ` }));
|
||||
vi.mock('@/lib/du-an-api', () => ({
|
||||
PROJECT_STATUS_LABELS: { SELLING: 'Đang bán' },
|
||||
PROJECT_STATUS_COLORS: { SELLING: 'bg-green-100 text-green-800' },
|
||||
}));
|
||||
|
||||
import { ProjectMap } from '../project-map';
|
||||
|
||||
const projects = [
|
||||
{ id: 'p1', name: 'Vinhomes', slug: 'vinhomes', status: 'SELLING' as const, district: 'Q9', city: 'HCMC', minPrice: '2000000000', latitude: 10.84, longitude: 106.83 },
|
||||
] as never[];
|
||||
|
||||
describe('ProjectMap', () => {
|
||||
it('renders fallback when no token', () => {
|
||||
delete (process.env as Record<string, string | undefined>)['NEXT_PUBLIC_MAPBOX_TOKEN'];
|
||||
render(<ProjectMap projects={[]} />);
|
||||
expect(screen.getByText(/NEXT_PUBLIC_MAPBOX_TOKEN/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows project count overlay', () => {
|
||||
render(<ProjectMap projects={projects} />);
|
||||
expect(screen.getByText(/1 dự án trên bản đồ/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows 0 projects when empty', () => {
|
||||
render(<ProjectMap projects={[]} />);
|
||||
expect(screen.getByText(/0 dự án trên bản đồ/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user