/** * Smoke suite — Web * * Runs after every deploy to verify critical frontend pages load and * render key UI elements. Tagged with @smoke for targeted execution: * * npx playwright test --project=web --grep @smoke * * Tests avoid user interactions that require a live API/auth session so * they remain fast and resilient in staging environments. */ import { test, expect } from '@playwright/test'; // ── Homepage ────────────────────────────────────────────────────────────────── test('@smoke homepage loads', async ({ page }) => { await page.goto('/'); await expect(page).toHaveTitle(/.+/); await expect(page.locator('main')).toBeVisible({ timeout: 10_000 }); await expect(page.getByText(/GGI HCM|Top biến động giá|Khu vực xu hướng/i).first()).toBeVisible({ timeout: 10_000, }); }); // ── Auth pages ──────────────────────────────────────────────────────────────── test('@smoke login page renders form', async ({ page }) => { await page.goto('/login'); await expect(page.getByRole('heading', { name: /đăng nhập/i })).toBeVisible(); await expect(page.getByLabel(/số điện thoại/i)).toBeVisible(); await expect(page.getByLabel(/mật khẩu/i)).toBeVisible(); await expect(page.getByRole('button', { name: /đăng nhập/i })).toBeVisible(); }); test('@smoke register page renders form', async ({ page }) => { await page.goto('/register'); await expect(page.getByRole('heading', { name: /đăng ký/i })).toBeVisible(); await expect(page.getByLabel(/số điện thoại/i)).toBeVisible(); }); // ── Listings ────────────────────────────────────────────────────────────────── test('@smoke listings page loads without JS errors', async ({ page }) => { const errors: string[] = []; page.on('pageerror', (err) => errors.push(err.message)); await page.goto('/listings'); // Allow time for async data loading await page.waitForLoadState('networkidle', { timeout: 15_000 }).catch(() => { // networkidle may not be reached if polling; continue }); // Page must not show an unhandled error boundary const errorHeading = page.getByRole('heading', { name: /500|something went wrong|lỗi/i }); await expect(errorHeading).not.toBeVisible(); // Filter JS errors that are not related to missing env vars in test const fatalErrors = errors.filter( (e) => !e.includes('NEXT_PUBLIC') && !e.includes('mapbox'), ); expect(fatalErrors).toHaveLength(0); }); // ── Search ──────────────────────────────────────────────────────────────────── test('@smoke search page is accessible', async ({ page }) => { await page.goto('/search?q=apartment'); await expect(page).not.toHaveTitle(/404|not found/i); // Be lenient — search service may be unavailable in staging await page.waitForTimeout(3000); // Just confirm no crash const errorBoundary = page.getByRole('heading', { name: /500|server error/i }); await expect(errorBoundary).not.toBeVisible(); }); // ── Listing Detail ──────────────────────────────────────────────────────────── test('@smoke listing detail page handles missing id gracefully', async ({ page }) => { const res = await page.goto('/listings/nonexistent-smoke-test-id'); // Should render 404 page, not crash with 500 const status = res?.status(); if (status && status >= 500) { throw new Error(`Listing detail returned ${status} for unknown ID (expected 404)`); } }); // ── Static / infra ──────────────────────────────────────────────────────────── test('@smoke no console errors on login page', async ({ page }) => { const consoleErrors: string[] = []; page.on('console', (msg) => { if (msg.type() === 'error') consoleErrors.push(msg.text()); }); await page.goto('/login'); await page.waitForLoadState('domcontentloaded'); // Filter noise from third-party scripts / mapbox in test env const meaningful = consoleErrors.filter( (e) => !e.includes('mapbox') && !e.includes('NEXT_PUBLIC') && !e.includes('Failed to load resource') && !e.includes('net::ERR'), ); expect(meaningful).toHaveLength(0); });