import { test, expect } from '@playwright/test'; /** * E2E coverage for the listing inquiry modal (TEC-2751 / TEC-2738.10). * * The backend route is `POST /api/v1/inquiries` and is guarded by JwtAuthGuard. * The web app talks to it via `apiClient.post('/inquiries', ...)`, so the * request URL contains `/inquiries` — we intercept it and stub both the * profile fetch (to make the user look authenticated) and the inquiry POST. */ const mockListing = { id: 'listing-1', transactionType: 'SALE', priceVND: '5000000000', pricePerM2: 66666667, rentPriceMonthly: null, commissionPct: 2.5, 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 Sài Gòn.', 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: 2022, legalStatus: 'Sổ hồng', projectName: 'Vinhomes Central Park', amenities: ['Hồ bơi'], media: [{ id: 'm1', url: '/placeholder.jpg', type: 'IMAGE', order: 0 }], }, seller: { id: 's1', fullName: 'Nguyen Van A', phone: '0912345678' }, agent: { id: 'a1', agency: 'GoodGo Realty', licenseNumber: 'AGT-001' }, }; const mockProfile = { id: 'user-1', email: 'buyer@example.com', fullName: 'Buyer Test', phone: '0911222333', role: 'USER', }; test.describe('Listing inquiry modal', () => { test.beforeEach(async ({ page }) => { await page.route('**/listings/listing-1', (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(mockListing), }), ); }); test('opens the inquiry modal when clicking "Nhắn tin"', async ({ page }) => { await page.goto('/listings/listing-1'); await expect( page.getByRole('heading', { name: 'Căn hộ cao cấp Quận 1' }), ).toBeVisible({ timeout: 10000 }); await page.getByRole('button', { name: /Nhan tin/i }).click(); await expect( page.getByRole('heading', { name: /Nhắn tin cho người bán/ }), ).toBeVisible(); await expect(page.getByLabel(/Nội dung tin nhắn/)).toBeVisible(); await expect(page.getByLabel(/Số điện thoại/)).toBeVisible(); }); test('shows validation errors when fields are missing or invalid', async ({ page }) => { await page.goto('/listings/listing-1'); await page.getByRole('button', { name: /Nhan tin/i }).click(); // Submit empty form — zod should flag both fields. await page.getByRole('button', { name: 'Gửi tin nhắn' }).click(); await expect(page.getByText('Vui lòng nhập nội dung tin nhắn')).toBeVisible(); // Provide message but an obviously-invalid phone. await page.getByLabel(/Nội dung tin nhắn/).fill('Tôi quan tâm tin đăng này.'); await page.getByLabel(/Số điện thoại/).fill('123'); await page.getByRole('button', { name: 'Gửi tin nhắn' }).click(); await expect( page.getByText(/Vui lòng nhập số điện thoại hợp lệ|Số điện thoại không hợp lệ/), ).toBeVisible(); }); test('submits the inquiry and calls POST /api/v1/inquiries (201)', async ({ page, context, }) => { // Mark the user as authenticated for the client-side check in auth-store. await context.addCookies([ { name: 'goodgo_authenticated', value: '1', url: 'http://localhost:3000', }, ]); // Stub the profile load so useAuthStore.isAuthenticated flips to true. await page.route('**/auth/me', (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(mockProfile), }), ); let inquiryRequestBody: Record | null = null; await page.route('**/inquiries', async (route) => { if (route.request().method() !== 'POST') { return route.fallback(); } inquiryRequestBody = route.request().postDataJSON() as Record; await route.fulfill({ status: 201, contentType: 'application/json', body: JSON.stringify({ id: 'inq-1', listingId: 'listing-1', listingTitle: mockListing.property.title, userId: mockProfile.id, userName: mockProfile.fullName, userPhone: mockProfile.phone, message: 'Tôi quan tâm tin đăng này.', phone: '0911222333', isRead: false, createdAt: new Date().toISOString(), }), }); }); await page.goto('/listings/listing-1'); await page.getByRole('button', { name: /Nhan tin/i }).click(); await page.getByLabel(/Nội dung tin nhắn/).fill('Tôi quan tâm tin đăng này.'); // Phone pre-fills from the mocked profile; overwrite to ensure stability. await page.getByLabel(/Số điện thoại/).fill('0911222333'); const [request] = await Promise.all([ page.waitForRequest( (req) => req.url().includes('/inquiries') && req.method() === 'POST', ), page.getByRole('button', { name: 'Gửi tin nhắn' }).click(), ]); expect(request.postDataJSON()).toMatchObject({ listingId: 'listing-1', message: 'Tôi quan tâm tin đăng này.', phone: '0911222333', }); // Modal should close after success. await expect( page.getByRole('heading', { name: /Nhắn tin cho người bán/ }), ).toBeHidden(); // Sonner success toast appears. await expect(page.getByText('Đã gửi thành công!')).toBeVisible(); expect(inquiryRequestBody).not.toBeNull(); }); test('redirects anonymous users to /login on submit', async ({ page }) => { await page.goto('/listings/listing-1'); await page.getByRole('button', { name: /Nhan tin/i }).click(); await page.getByLabel(/Nội dung tin nhắn/).fill('Tôi quan tâm tin đăng này.'); await page.getByLabel(/Số điện thoại/).fill('0911222333'); await Promise.all([ page.waitForURL(/\/login/), page.getByRole('button', { name: 'Gửi tin nhắn' }).click(), ]); await expect(page).toHaveURL(/\/login/); }); });