Files
goodgo-platform/e2e/web/agents.spec.ts
Ho Ngoc Hai 3a9e44758c fix(web): unwrap CacheMetaInterceptor envelope + dev port migration + homepage diacritic
Several fixes discovered while smoke-testing the homepage under the new
port layout (web 3200 / api 3201) to avoid clashing with a sibling project:

- analytics-api: add `unwrap<T>()` helper for the `{ data, cacheMeta }`
  envelope the backend CacheMetaInterceptor appends to every
  `/analytics/*` response. Apply to all 9 analytics methods. Without this
  `data.activeCount` (etc.) were `undefined`, crashing KpiStrip with
  `TypeError: Cannot read properties of undefined (reading 'toLocaleString')`.
- public page: hard-coded `city = 'Ho Chi Minh'` returned 0 rows because
  the DB stores `'Hồ Chí Minh'` and the SQL filter is case-insensitive but
  not diacritic-insensitive. Use the accented spelling.
- use-analytics hooks: add `useAuthedAnalytics()` gate so unauthenticated
  visitors on public routes no longer fire 401s from analytics queries.
- next.config.js CSP: add localhost:3200/3201 (http + ws) to connect-src so
  the web origin can reach the relocated API. Without this fetches hit
  `TypeError: Failed to fetch` on login.
- .claude/launch.json + package.json: web → 3200, api → 3201 (was 3000/3001,
  conflicting with the sibling psyforge project also using 3000).
- Minor follow-ups from parallel QA work on this branch (analytics modules,
  notifications gateway, auth test fixtures, trending-areas handler + DTO
  + tests, a few E2E smoke specs).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 16:54:44 +07:00

189 lines
5.6 KiB
TypeScript

/**
* Agents Page E2E Tests
*
* Tests the agent profile page at /agents/[id].
* The app does not have a public agent listing page, only individual agent profiles.
* Tests use route mocking to avoid API dependency.
*/
import { test, expect } from '@playwright/test';
const mockAgent = {
id: 'agent-1',
fullName: 'Nguyễn Văn Minh',
avatarUrl: null,
phone: '0912345678',
email: 'minh@goodgo.vn',
agency: 'GoodGo Realty',
licenseNumber: 'GPHN-2025-001',
bio: 'Chuyên viên tư vấn bất động sản khu vực Quận 7 và Quận 2 với hơn 5 năm kinh nghiệm.',
qualityScore: 88,
totalDeals: 45,
isVerified: true,
serviceAreas: ['Quận 7', 'Quận 2', 'Nhà Bè'],
memberSince: '2023-06-15T00:00:00Z',
activeListings: [
{
id: 'listing-1',
transactionType: 'SALE',
priceVND: '5000000000',
status: 'ACTIVE',
property: {
id: 'prop-1',
title: 'Căn hộ cao cấp Quận 7',
propertyType: 'APARTMENT',
address: '123 Nguyễn Thị Thập',
district: 'Quận 7',
city: 'Hồ Chí Minh',
areaM2: 75,
bedrooms: 2,
bathrooms: 2,
imageUrl: null,
},
},
],
avgReviewRating: 4.8,
totalReviews: 12,
};
const mockReviews = {
data: [
{
id: 'review-1',
userId: 'user-1',
userName: 'Trần Thị B',
targetType: 'AGENT',
targetId: 'agent-1',
rating: 5,
comment: 'Môi giới tận tình, hỗ trợ nhiệt tình.',
createdAt: '2026-03-01T00:00:00Z',
},
],
stats: {
targetType: 'AGENT',
targetId: 'agent-1',
averageRating: 4.8,
totalReviews: 12,
distribution: { 5: 10, 4: 2 },
},
};
test.describe('Agent Profile Page', () => {
test.beforeEach(async ({ page }) => {
await page.route('**/agents/agent-1', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockAgent),
}),
);
await page.route('**/agents/agent-1/reviews**', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockReviews),
}),
);
});
test('renders agent name and verified badge', async ({ page }) => {
await page.goto('/agents/agent-1');
await expect(page.getByText('Nguyễn Văn Minh')).toBeVisible({ timeout: 10_000 });
});
test('shows agent agency and contact info', async ({ page }) => {
await page.goto('/agents/agent-1');
await expect(page.getByText('Nguyễn Văn Minh')).toBeVisible({ timeout: 10_000 });
await expect(page.getByText(/GoodGo Realty/)).toBeVisible();
});
test('shows active listings section', async ({ page }) => {
await page.goto('/agents/agent-1');
await expect(page.getByText('Nguyễn Văn Minh')).toBeVisible({ timeout: 10_000 });
// Listing should appear
await expect(page.getByText(/Căn hộ cao cấp Quận 7/)).toBeVisible();
});
test('has breadcrumb back to homepage', async ({ page }) => {
await page.goto('/agents/agent-1');
await expect(page.getByText('Nguyễn Văn Minh')).toBeVisible({ timeout: 10_000 });
await expect(page.getByRole('link', { name: /Trang chủ/i })).toBeVisible();
});
test('renders without critical console errors', async ({ page }) => {
const criticalErrors: string[] = [];
page.on('console', (msg) => {
if (msg.type() === 'error') {
const text = msg.text();
if (
text.includes('mapbox') ||
text.includes('NEXT_PUBLIC') ||
text.includes('net::ERR') ||
text.includes('Failed to load resource')
) {
return;
}
criticalErrors.push(text);
}
});
await page.goto('/agents/agent-1');
await page.waitForLoadState('networkidle', { timeout: 15_000 }).catch(() => {});
expect(criticalErrors).toHaveLength(0);
});
test('handles 404 for unknown agent gracefully', async ({ page }) => {
await page.route('**/agents/nonexistent**', (route) =>
route.fulfill({ status: 404, body: JSON.stringify({ message: 'Not found' }) }),
);
const res = await page.goto('/agents/nonexistent-agent-id');
const status = res?.status();
if (status && status >= 500) {
throw new Error(`Agent page returned ${status} for unknown ID (expected 404 or redirect)`);
}
});
});
test.describe('Agent Profile — Responsive', () => {
const viewports = [
{ label: '375px (mobile)', width: 375, height: 667 },
{ label: '768px (tablet)', width: 768, height: 1024 },
{ label: '1280px (laptop)', width: 1280, height: 800 },
{ label: '1920px (desktop)', width: 1920, height: 1080 },
];
for (const vp of viewports) {
test(`renders at ${vp.label}`, async ({ page }) => {
await page.route('**/agents/agent-1', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockAgent),
}),
);
await page.route('**/agents/agent-1/reviews**', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockReviews),
}),
);
await page.setViewportSize({ width: vp.width, height: vp.height });
await page.goto('/agents/agent-1');
await expect(page.getByText('Nguyễn Văn Minh')).toBeVisible({ timeout: 10_000 });
// No horizontal overflow (layout break indicator)
const bodyWidth = await page.evaluate(() => document.body.scrollWidth);
const viewportWidth = await page.evaluate(() => window.innerWidth);
expect(bodyWidth).toBeLessThanOrEqual(viewportWidth + 2); // 2px tolerance
});
}
});