91 lines
3.9 KiB
TypeScript
91 lines
3.9 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
|
import { mockAuthenticatedUser } from './support/auth';
|
|
|
|
const mockMarketReport = {
|
|
districts: [
|
|
{ district: 'Quan 1', propertyType: 'APARTMENT', avgPriceM2: 85000000, medianPriceM2: 80000000, totalListings: 150, daysOnMarket: 45, yoyChange: 5.2 },
|
|
{ district: 'Quan 7', propertyType: 'HOUSE', avgPriceM2: 65000000, medianPriceM2: 60000000, totalListings: 200, daysOnMarket: 60, yoyChange: -2.1 },
|
|
],
|
|
};
|
|
|
|
const mockHeatmap = {
|
|
dataPoints: [
|
|
{ district: 'Quan 1', avgPriceM2: 85000000, totalListings: 150, lat: 10.7769, lng: 106.7009 },
|
|
{ district: 'Quan 7', avgPriceM2: 65000000, totalListings: 200, lat: 10.7385, lng: 106.7218 },
|
|
],
|
|
};
|
|
|
|
const mockDistrictStats = {
|
|
districts: [
|
|
{ district: 'Quan 1', propertyType: 'APARTMENT', medianPrice: 5000000000, pricePerM2: 85000000, totalListings: 150, daysOnMarket: 45, yoyChange: 5.2 },
|
|
],
|
|
};
|
|
|
|
const mockTrends = {
|
|
dataPoints: [
|
|
{ period: '2025-Q1', avgPriceM2: 78000000, totalListings: 130, transactionVolume: 80 },
|
|
{ period: '2025-Q2', avgPriceM2: 80000000, totalListings: 140, transactionVolume: 85 },
|
|
{ period: '2026-Q1', avgPriceM2: 85000000, totalListings: 150, transactionVolume: 95 },
|
|
],
|
|
};
|
|
|
|
test.describe('Analytics Page', () => {
|
|
test.beforeEach(async ({ page, context, baseURL }) => {
|
|
await mockAuthenticatedUser(page, context, baseURL, { role: 'AGENT' });
|
|
|
|
await page.route('**/api/v1/analytics/market-report**', (route) =>
|
|
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(mockMarketReport) }),
|
|
);
|
|
await page.route('**/api/v1/analytics/heatmap**', (route) =>
|
|
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(mockHeatmap) }),
|
|
);
|
|
await page.route('**/api/v1/analytics/district-stats**', (route) =>
|
|
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(mockDistrictStats) }),
|
|
);
|
|
await page.route('**/api/v1/analytics/price-trend**', (route) =>
|
|
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(mockTrends) }),
|
|
);
|
|
});
|
|
|
|
test('renders analytics page with city selector', async ({ page }) => {
|
|
await page.goto('/analytics');
|
|
|
|
// City selector buttons should be visible
|
|
await expect(page.getByRole('button', { name: /Ho Chi Minh/i })).toBeVisible({ timeout: 10000 });
|
|
await expect(page.getByRole('button', { name: /Ha Noi/i })).toBeVisible();
|
|
await expect(page.getByRole('button', { name: /Da Nang/i })).toBeVisible();
|
|
});
|
|
|
|
test('displays tabs for different views', async ({ page }) => {
|
|
await page.goto('/analytics');
|
|
|
|
await expect(page.getByRole('tab', { name: /Tổng quan/i })).toBeVisible({ timeout: 10000 });
|
|
await expect(page.getByRole('tab', { name: /Xu hướng giá/i })).toBeVisible();
|
|
});
|
|
|
|
test('switches city when selector clicked', async ({ page }) => {
|
|
await page.goto('/analytics');
|
|
await expect(page.getByRole('button', { name: /Ha Noi/i })).toBeVisible({ timeout: 10000 });
|
|
|
|
await page.getByRole('button', { name: /Ha Noi/i }).click();
|
|
|
|
// The Ha Noi button should now appear selected/active
|
|
// Page should re-fetch data for the new city
|
|
await expect(page.getByRole('button', { name: /Ha Noi/i })).toBeVisible();
|
|
});
|
|
|
|
test('handles empty data gracefully', async ({ page }) => {
|
|
await page.route('**/api/v1/analytics/market-report**', (route) =>
|
|
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ districts: [] }) }),
|
|
);
|
|
await page.route('**/api/v1/analytics/heatmap**', (route) =>
|
|
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ dataPoints: [] }) }),
|
|
);
|
|
|
|
await page.goto('/analytics');
|
|
|
|
// Page should still render without crashing
|
|
await expect(page.getByRole('button', { name: /Ho Chi Minh/i })).toBeVisible({ timeout: 10000 });
|
|
});
|
|
});
|