test(web): increase frontend test coverage to ~70% page coverage
- Fix vitest config to include [locale] directory tests (was excluded)
- Fix register.spec.tsx: use getByRole('heading') to avoid duplicate text match
- Fix search.spec.tsx: add QueryClientProvider wrapper and mock saved searches hook
- Add 12 new page test files covering dashboard, admin, public, and OAuth pages:
- dashboard (main, profile, payments, subscription, KYC)
- admin (dashboard, users)
- public (landing, pricing)
- analytics
- OAuth callbacks (Google, Zalo)
- 29 test files, 174 tests, 16/23 pages covered (69.6%)
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
/* eslint-disable import-x/order */
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
vi.mock('next/dynamic', () => ({
|
||||
default: () => {
|
||||
const Mock = () => <div data-testid="chart-placeholder">Chart</div>;
|
||||
Mock.displayName = 'MockChart';
|
||||
return Mock;
|
||||
},
|
||||
}));
|
||||
|
||||
const mockMarketReport = {
|
||||
districts: [
|
||||
{ district: 'Quan 1', totalListings: 100, avgPriceM2: 120000000, medianPrice: '15000000000', daysOnMarket: 45, yoyChange: 5.2, inventoryLevel: 50 },
|
||||
],
|
||||
};
|
||||
|
||||
const mockHeatmap = {
|
||||
dataPoints: [
|
||||
{ district: 'Quan 1', avgPriceM2: 120000000, totalListings: 100, lat: 10.77, lng: 106.7 },
|
||||
],
|
||||
};
|
||||
|
||||
const mockDistrictStats = {
|
||||
districts: [
|
||||
{ district: 'Quan 1', propertyType: 'APARTMENT', medianPrice: '15000000000', avgPriceM2: 120000000, totalListings: 100, daysOnMarket: 45, yoyChange: 5.2 },
|
||||
],
|
||||
};
|
||||
|
||||
const mockPriceTrend = {
|
||||
trend: [
|
||||
{ period: '2025-Q4', avgPriceM2: 115000000, totalListings: 90 },
|
||||
{ period: '2026-Q1', avgPriceM2: 120000000, totalListings: 100 },
|
||||
],
|
||||
};
|
||||
|
||||
vi.mock('@/lib/hooks/use-analytics', () => ({
|
||||
useMarketReport: vi.fn(() => ({ data: mockMarketReport, isLoading: false, error: null })),
|
||||
useHeatmap: vi.fn(() => ({ data: mockHeatmap, isLoading: false })),
|
||||
useDistrictStats: vi.fn(() => ({ data: mockDistrictStats, isLoading: false })),
|
||||
usePriceTrend: vi.fn(() => ({ data: mockPriceTrend, isLoading: false })),
|
||||
}));
|
||||
|
||||
import AnalyticsPage from '../page';
|
||||
|
||||
describe('AnalyticsPage', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders analytics page heading', () => {
|
||||
render(<AnalyticsPage />);
|
||||
expect(screen.getByText('Phân tích thị trường')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders summary cards', () => {
|
||||
render(<AnalyticsPage />);
|
||||
expect(screen.getByText('Tổng tin đăng')).toBeInTheDocument();
|
||||
expect(screen.getByText('Giá TB/m²')).toBeInTheDocument();
|
||||
expect(screen.getByText('Ngày trung bình để bán')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders city selection buttons', () => {
|
||||
render(<AnalyticsPage />);
|
||||
expect(screen.getByRole('button', { name: 'Ho Chi Minh' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Ha Noi' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Da Nang' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders tab navigation', () => {
|
||||
render(<AnalyticsPage />);
|
||||
expect(screen.getByText('Tổng quan')).toBeInTheDocument();
|
||||
expect(screen.getByText('Xu hướng giá')).toBeInTheDocument();
|
||||
expect(screen.getByText('Chi tiết quận')).toBeInTheDocument();
|
||||
expect(screen.getByText('Hiệu suất')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,130 @@
|
||||
/* eslint-disable import-x/order */
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
vi.mock('next/dynamic', () => ({
|
||||
default: () => {
|
||||
const Mock = () => <div data-testid="chart-placeholder">Chart</div>;
|
||||
Mock.displayName = 'MockChart';
|
||||
return Mock;
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('next/image', () => ({
|
||||
default: (props: Record<string, unknown>) => <img {...props} />,
|
||||
}));
|
||||
|
||||
vi.mock('next/link', () => ({
|
||||
default: ({ children, href, ...props }: { children: React.ReactNode; href: string; [key: string]: unknown }) => (
|
||||
<a href={href} {...props}>{children}</a>
|
||||
),
|
||||
}));
|
||||
|
||||
const mockMarketReport = {
|
||||
districts: [
|
||||
{ district: 'Quan 1', totalListings: 100, avgPriceM2: 120000000, medianPrice: '15000000000', daysOnMarket: 45, yoyChange: 5.2, inventoryLevel: 50 },
|
||||
{ district: 'Quan 7', totalListings: 80, avgPriceM2: 60000000, medianPrice: '8000000000', daysOnMarket: 30, yoyChange: -2.1, inventoryLevel: 30 },
|
||||
],
|
||||
};
|
||||
|
||||
const mockHeatmap = {
|
||||
dataPoints: [
|
||||
{ district: 'Quan 1', avgPriceM2: 120000000, totalListings: 100, lat: 10.77, lng: 106.7 },
|
||||
{ district: 'Quan 7', avgPriceM2: 60000000, totalListings: 80, lat: 10.73, lng: 106.72 },
|
||||
],
|
||||
};
|
||||
|
||||
const mockListings = {
|
||||
data: [
|
||||
{
|
||||
id: '1',
|
||||
status: 'ACTIVE',
|
||||
transactionType: 'SALE',
|
||||
priceVND: '5000000000',
|
||||
viewCount: 10,
|
||||
saveCount: 2,
|
||||
inquiryCount: 3,
|
||||
publishedAt: '2024-01-01',
|
||||
createdAt: '2024-01-01',
|
||||
pricePerM2: null,
|
||||
rentPriceMonthly: null,
|
||||
commissionPct: null,
|
||||
property: {
|
||||
id: 'p1',
|
||||
propertyType: 'APARTMENT',
|
||||
title: 'Căn hộ Quận 7',
|
||||
description: 'Test',
|
||||
address: '123 Nguyễn Hữu Thọ',
|
||||
ward: 'Tân Hưng',
|
||||
district: 'Quận 7',
|
||||
city: 'Hồ Chí Minh',
|
||||
areaM2: 75,
|
||||
bedrooms: 2,
|
||||
bathrooms: 2,
|
||||
floors: null,
|
||||
direction: null,
|
||||
yearBuilt: null,
|
||||
legalStatus: null,
|
||||
amenities: null,
|
||||
projectName: null,
|
||||
media: [],
|
||||
},
|
||||
seller: { id: 's1', fullName: 'Nguyen Van A', phone: '0912345678' },
|
||||
agent: null,
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
page: 1,
|
||||
limit: 6,
|
||||
totalPages: 1,
|
||||
};
|
||||
|
||||
vi.mock('@/lib/hooks/use-analytics', () => ({
|
||||
useMarketReport: vi.fn(() => ({ data: mockMarketReport, isLoading: false })),
|
||||
useHeatmap: vi.fn(() => ({ data: mockHeatmap, isLoading: false })),
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/hooks/use-listings', () => ({
|
||||
useListingsSearch: vi.fn(() => ({ data: mockListings, isLoading: false })),
|
||||
}));
|
||||
|
||||
vi.mock('@/components/listings/listing-status-badge', () => ({
|
||||
ListingStatusBadge: ({ status }: { status: string }) => <span data-testid="status-badge">{status}</span>,
|
||||
}));
|
||||
|
||||
import DashboardPage from '../page';
|
||||
|
||||
describe('DashboardPage', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders the dashboard heading', () => {
|
||||
render(<DashboardPage />);
|
||||
expect(screen.getByText('Bảng điều khiển')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders stat cards with data', () => {
|
||||
render(<DashboardPage />);
|
||||
expect(screen.getByText('Tin đăng của tôi')).toBeInTheDocument();
|
||||
expect(screen.getByText('Lượt xem')).toBeInTheDocument();
|
||||
expect(screen.getByText('Liên hệ')).toBeInTheDocument();
|
||||
expect(screen.getByText('Giá TB thị trường')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders recent listings section', () => {
|
||||
render(<DashboardPage />);
|
||||
expect(screen.getByText('Tin đăng gần đây')).toBeInTheDocument();
|
||||
expect(screen.getByText(/căn hộ quận 7/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders "Đăng tin mới" button', () => {
|
||||
render(<DashboardPage />);
|
||||
expect(screen.getByRole('link', { name: /đăng tin mới/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders market summary card', () => {
|
||||
render(<DashboardPage />);
|
||||
expect(screen.getByText(/thị trường ho chi minh/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
/* eslint-disable import-x/order */
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { useAuthStore } from '@/lib/auth-store';
|
||||
|
||||
vi.mock('@/lib/auth-store', () => {
|
||||
const store = {
|
||||
user: {
|
||||
id: 'user-1',
|
||||
fullName: 'Nguyen Van A',
|
||||
phone: '0912345678',
|
||||
kycStatus: 'NONE',
|
||||
},
|
||||
fetchProfile: vi.fn(),
|
||||
};
|
||||
return {
|
||||
useAuthStore: vi.fn((selector) => {
|
||||
if (typeof selector === 'function') return selector(store);
|
||||
return store;
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@/lib/api-client', () => ({
|
||||
apiClient: {
|
||||
patch: vi.fn().mockResolvedValue({}),
|
||||
},
|
||||
}));
|
||||
|
||||
import KycPage from '../kyc/page';
|
||||
|
||||
describe('KycPage', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders KYC page heading', () => {
|
||||
render(<KycPage />);
|
||||
expect(screen.getByText('Xác minh danh tính (KYC)')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders KYC status badge as not verified', () => {
|
||||
render(<KycPage />);
|
||||
expect(screen.getByText('Chưa xác minh')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders step indicator', () => {
|
||||
render(<KycPage />);
|
||||
expect(screen.getByText('1')).toBeInTheDocument();
|
||||
expect(screen.getByText('2')).toBeInTheDocument();
|
||||
expect(screen.getByText('3')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders document type selector on step 1', () => {
|
||||
render(<KycPage />);
|
||||
expect(screen.getByLabelText(/loại giấy tờ/i)).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/số giấy tờ/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows error when continuing without document number', async () => {
|
||||
render(<KycPage />);
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: /tiếp tục/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/vui lòng nhập số giấy tờ/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('advances to step 2 after filling document number', async () => {
|
||||
render(<KycPage />);
|
||||
|
||||
await userEvent.type(screen.getByLabelText(/số giấy tờ/i), '012345678901');
|
||||
await userEvent.click(screen.getByRole('button', { name: /tiếp tục/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText(/ảnh mặt trước/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
/* eslint-disable import-x/order */
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const mockTransactions = {
|
||||
items: [
|
||||
{
|
||||
id: 'tx-1',
|
||||
type: 'SUBSCRIPTION',
|
||||
status: 'COMPLETED',
|
||||
amountVND: '499000',
|
||||
provider: 'VNPAY',
|
||||
providerTxId: 'TXN123456789012',
|
||||
createdAt: '2024-06-15T10:00:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 'tx-2',
|
||||
type: 'LISTING_FEE',
|
||||
status: 'PENDING',
|
||||
amountVND: '100000',
|
||||
provider: 'MOMO',
|
||||
providerTxId: null,
|
||||
createdAt: '2024-06-20T10:00:00.000Z',
|
||||
},
|
||||
],
|
||||
total: 2,
|
||||
};
|
||||
|
||||
vi.mock('@/lib/hooks/use-payments', () => ({
|
||||
useTransactions: vi.fn(() => ({ data: mockTransactions, isLoading: false })),
|
||||
}));
|
||||
|
||||
import PaymentsPage from '../payments/page';
|
||||
|
||||
describe('PaymentsPage', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders payment page heading', () => {
|
||||
render(<PaymentsPage />);
|
||||
expect(screen.getByText('Thanh toán')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders summary cards', () => {
|
||||
render(<PaymentsPage />);
|
||||
expect(screen.getByText('Tổng giao dịch')).toBeInTheDocument();
|
||||
expect(screen.getByText('Đã thanh toán')).toBeInTheDocument();
|
||||
expect(screen.getByText('Đang chờ')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders transaction history section', () => {
|
||||
render(<PaymentsPage />);
|
||||
expect(screen.getByText('Lịch sử giao dịch')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders transaction count correctly', () => {
|
||||
render(<PaymentsPage />);
|
||||
expect(screen.getByText('2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders status filter', () => {
|
||||
render(<PaymentsPage />);
|
||||
expect(screen.getByText('Tất cả')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
/* eslint-disable import-x/order */
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { useAuthStore } from '@/lib/auth-store';
|
||||
|
||||
vi.mock('@/lib/auth-store', () => {
|
||||
const store = {
|
||||
user: {
|
||||
id: 'user-1',
|
||||
fullName: 'Nguyen Van A',
|
||||
phone: '0912345678',
|
||||
email: 'test@example.com',
|
||||
role: 'BUYER',
|
||||
kycStatus: 'NONE',
|
||||
isActive: true,
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
},
|
||||
fetchProfile: vi.fn(),
|
||||
};
|
||||
return {
|
||||
useAuthStore: vi.fn((selector) => {
|
||||
if (typeof selector === 'function') return selector(store);
|
||||
return store;
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@/lib/profile-api', () => ({
|
||||
profileApi: {
|
||||
getAgentProfile: vi.fn().mockRejectedValue(new Error('Not an agent')),
|
||||
updateProfile: vi.fn().mockResolvedValue({}),
|
||||
},
|
||||
}));
|
||||
|
||||
import ProfilePage from '../profile/page';
|
||||
|
||||
describe('ProfilePage', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders profile heading', async () => {
|
||||
render(<ProfilePage />);
|
||||
expect(screen.getByText('Hồ sơ cá nhân')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders user information', async () => {
|
||||
render(<ProfilePage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Nguyen Van A')).toBeInTheDocument();
|
||||
expect(screen.getByText('0912345678')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders KYC status badge', async () => {
|
||||
render(<ProfilePage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Chưa xác minh')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders edit button', async () => {
|
||||
render(<ProfilePage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: /chỉnh sửa/i })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('enters edit mode when edit button is clicked', async () => {
|
||||
render(<ProfilePage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: /chỉnh sửa/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: /chỉnh sửa/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: /lưu thay đổi/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /hủy/i })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders account status section', async () => {
|
||||
render(<ProfilePage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Trạng thái tài khoản')).toBeInTheDocument();
|
||||
expect(screen.getByText('Hoạt động')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
/* eslint-disable import-x/order */
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const mockPlans = [
|
||||
{
|
||||
id: 'plan-1',
|
||||
tier: 'FREE',
|
||||
name: 'Miễn phí',
|
||||
priceMonthlyVND: '0',
|
||||
priceYearlyVND: '0',
|
||||
maxListings: 3,
|
||||
maxSavedSearches: 5,
|
||||
features: {},
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
id: 'plan-2',
|
||||
tier: 'AGENT_PRO',
|
||||
name: 'Agent Pro',
|
||||
priceMonthlyVND: '499000',
|
||||
priceYearlyVND: '4990000',
|
||||
maxListings: 50,
|
||||
maxSavedSearches: 30,
|
||||
features: {},
|
||||
isActive: true,
|
||||
},
|
||||
];
|
||||
|
||||
const mockBilling = {
|
||||
subscription: null,
|
||||
payments: [],
|
||||
};
|
||||
|
||||
vi.mock('@/lib/hooks/use-subscription', () => ({
|
||||
usePlans: vi.fn(() => ({ data: mockPlans, isLoading: false })),
|
||||
useBillingHistory: vi.fn(() => ({ data: mockBilling, isLoading: false })),
|
||||
useQuota: vi.fn(() => ({ data: null })),
|
||||
subscriptionKeys: { billing: () => ['billing'] },
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/subscription-api', () => ({
|
||||
subscriptionApi: {
|
||||
createSubscription: vi.fn(),
|
||||
upgradeSubscription: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@tanstack/react-query', async (importOriginal) => {
|
||||
const actual = (await importOriginal()) as Record<string, unknown>;
|
||||
return {
|
||||
...actual,
|
||||
useQueryClient: () => ({
|
||||
invalidateQueries: vi.fn(),
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
import SubscriptionPage from '../subscription/page';
|
||||
|
||||
describe('SubscriptionPage', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders subscription page heading', () => {
|
||||
render(<SubscriptionPage />);
|
||||
expect(screen.getByText('Gói dịch vụ')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders tab navigation', () => {
|
||||
render(<SubscriptionPage />);
|
||||
expect(screen.getByText('Gói hiện tại')).toBeInTheDocument();
|
||||
expect(screen.getByText('So sánh gói')).toBeInTheDocument();
|
||||
expect(screen.getByText('Lịch sử thanh toán')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders current plan info for free tier', () => {
|
||||
render(<SubscriptionPage />);
|
||||
expect(screen.getByText(/bạn đang sử dụng gói miễn phí/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders plan description text', () => {
|
||||
render(<SubscriptionPage />);
|
||||
expect(screen.getByText(/quản lý gói đăng ký/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user