Cover auth (login, register, OAuth callbacks), search with filters, listing detail, dashboard, analytics, create listing form, admin dashboard/users/moderation/KYC, navigation routing, and responsive design. Total 91 test cases using Playwright with API route mocking. Co-Authored-By: Paperclip <noreply@paperclip.ing>
172 lines
5.0 KiB
TypeScript
172 lines
5.0 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
|
|
|
const mockListings = {
|
|
data: [
|
|
{
|
|
id: 'listing-1',
|
|
transactionType: 'SALE',
|
|
priceVND: '5000000000',
|
|
pricePerM2: 66666667,
|
|
rentPriceMonthly: null,
|
|
commissionPct: null,
|
|
status: 'ACTIVE',
|
|
viewCount: 120,
|
|
saveCount: 15,
|
|
inquiryCount: 8,
|
|
publishedAt: '2026-01-15T00:00:00Z',
|
|
property: {
|
|
id: 'prop-1',
|
|
propertyType: 'APARTMENT',
|
|
title: 'Căn hộ cao cấp Quận 1',
|
|
description: 'Căn hộ đẹp view sông',
|
|
address: '123 Nguyễn Huệ',
|
|
ward: 'Bến Nghé',
|
|
district: 'Quận 1',
|
|
city: 'Hồ Chí Minh',
|
|
latitude: 10.7769,
|
|
longitude: 106.7009,
|
|
areaM2: 75,
|
|
bedrooms: 2,
|
|
bathrooms: 2,
|
|
floors: 1,
|
|
direction: 'SOUTH',
|
|
yearBuilt: null,
|
|
legalStatus: null,
|
|
projectName: null,
|
|
amenities: [],
|
|
media: [],
|
|
},
|
|
seller: { id: 's1', fullName: 'Nguyen Van A', phone: '0912345678' },
|
|
agent: null,
|
|
},
|
|
{
|
|
id: 'listing-2',
|
|
transactionType: 'RENT',
|
|
priceVND: '15000000',
|
|
pricePerM2: null,
|
|
rentPriceMonthly: '15000000',
|
|
commissionPct: null,
|
|
status: 'ACTIVE',
|
|
viewCount: 50,
|
|
saveCount: 5,
|
|
inquiryCount: 3,
|
|
publishedAt: '2026-02-01T00:00:00Z',
|
|
property: {
|
|
id: 'prop-2',
|
|
propertyType: 'HOUSE',
|
|
title: 'Nhà phố Quận 7',
|
|
description: 'Nhà phố đẹp khu an ninh',
|
|
address: '456 Nguyễn Thị Thập',
|
|
ward: 'Tân Phú',
|
|
district: 'Quận 7',
|
|
city: 'Hồ Chí Minh',
|
|
latitude: 10.7385,
|
|
longitude: 106.7218,
|
|
areaM2: 120,
|
|
bedrooms: 4,
|
|
bathrooms: 3,
|
|
floors: 3,
|
|
direction: 'EAST',
|
|
yearBuilt: 2020,
|
|
legalStatus: null,
|
|
projectName: null,
|
|
amenities: [],
|
|
media: [],
|
|
},
|
|
seller: { id: 's2', fullName: 'Tran Thi B', phone: '0987654321' },
|
|
agent: null,
|
|
},
|
|
],
|
|
total: 2,
|
|
page: 1,
|
|
limit: 12,
|
|
totalPages: 1,
|
|
};
|
|
|
|
test.describe('Search Page', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
// Mock the listings API to return consistent data
|
|
await page.route('**/listings**', (route) => {
|
|
if (route.request().method() === 'GET') {
|
|
return route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify(mockListings),
|
|
});
|
|
}
|
|
return route.continue();
|
|
});
|
|
});
|
|
|
|
test('renders search page with title and filters', async ({ page }) => {
|
|
await page.goto('/search');
|
|
|
|
await expect(page.getByRole('heading', { name: 'Tìm kiếm bất động sản' })).toBeVisible();
|
|
await expect(
|
|
page.getByText('Tìm bất động sản phù hợp với nhu cầu của bạn'),
|
|
).toBeVisible();
|
|
});
|
|
|
|
test('displays view mode toggle buttons', async ({ page }) => {
|
|
await page.goto('/search');
|
|
|
|
await expect(page.getByRole('button', { name: /Danh sách/i })).toBeVisible();
|
|
await expect(page.getByRole('button', { name: /Bản đồ/i })).toBeVisible();
|
|
});
|
|
|
|
test('displays listing results', async ({ page }) => {
|
|
await page.goto('/search');
|
|
|
|
await expect(page.getByText('Căn hộ cao cấp Quận 1')).toBeVisible({ timeout: 10000 });
|
|
await expect(page.getByText('Nhà phố Quận 7')).toBeVisible();
|
|
});
|
|
|
|
test('switches to map view mode', async ({ page }) => {
|
|
await page.goto('/search');
|
|
await page.getByRole('button', { name: /Bản đồ/i }).click();
|
|
|
|
// Map view should be active — list results should not be visible
|
|
await expect(page.getByRole('button', { name: /Bản đồ/i })).toHaveAttribute(
|
|
'data-state',
|
|
/.*/,
|
|
);
|
|
});
|
|
|
|
test('syncs filters to URL query parameters', async ({ page }) => {
|
|
await page.goto('/search?transactionType=SALE');
|
|
|
|
// The URL should contain the filter
|
|
await expect(page).toHaveURL(/transactionType=SALE/);
|
|
});
|
|
|
|
test('shows error state on API failure', async ({ page }) => {
|
|
await page.route('**/listings**', (route) =>
|
|
route.fulfill({ status: 500, body: 'Internal Server Error' }),
|
|
);
|
|
|
|
await page.goto('/search');
|
|
|
|
// Should show some error indication
|
|
await page.waitForTimeout(2000);
|
|
// The page should still be navigable (not crash)
|
|
await expect(page.getByRole('heading', { name: 'Tìm kiếm bất động sản' })).toBeVisible();
|
|
});
|
|
|
|
test('shows loading spinner initially', async ({ page }) => {
|
|
await page.route('**/listings**', async (route) => {
|
|
await new Promise((r) => setTimeout(r, 2000));
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify(mockListings),
|
|
});
|
|
});
|
|
|
|
await page.goto('/search');
|
|
|
|
// Should show loading indication (spinner or skeleton)
|
|
const spinner = page.locator('.animate-spin, .animate-pulse');
|
|
await expect(spinner.first()).toBeVisible({ timeout: 3000 });
|
|
});
|
|
});
|