import { test, expect } from '@playwright/test'; test.describe('Error Handling (US-85, US-86, US-88) - React ru-ru', () => { // Collect console errors for all tests let consoleErrors: string[] = []; test.beforeEach(async ({ page }) => { // Clear previous errors consoleErrors = []; // Set Russian locale await page.addInitScript(() => { Object.defineProperty(navigator, 'language', { get: () => 'ru-RU', }); Object.defineProperty(navigator, 'languages', { get: () => ['ru-RU', 'ru'], }); }); // Collect console errors throughout test page.on('console', (msg) => { if (msg.type() === 'error') { consoleErrors.push(msg.text()); } }); }); test.afterEach(async ({ page }) => { // Clean up all routes await page.unroute('**/*'); // Close any dialogs await page.keyboard.press('Escape').catch(() => {}); // Verify no unexpected console errors occurred const unexpectedErrors = consoleErrors.filter( (msg) => !msg.includes('Failed to fetch') && !msg.includes('Network error') && !msg.includes('404') && !msg.includes('500'), ); if (unexpectedErrors.length > 0) { console.warn('Unexpected console errors:', unexpectedErrors); } }); test('US-85: 404 Not Found - navigate to invalid route and verify Russian error page', async ({ page, }) => { // Navigate to non-existent route await page.goto('http://localhost:3001/ru-ru/invalid-route-xyz'); // Verify NotFoundPage renders with 404 heading visible const notFoundHeading = page.locator('h1:has-text("404")'); await expect(notFoundHeading).toBeVisible(); // Verify Russian title is displayed const rusTitle = page.locator('text=Страница не найдена'); await expect(rusTitle).toBeVisible({ timeout: 5000 }); // Verify Russian description is displayed const rusDescription = page.locator('text=/Извините|не существует/'); await expect(rusDescription).toBeVisible(); // Verify home link exists and is clickable const homeLink = page.getByRole('link', { name: /На главную|Home/i }); await expect(homeLink).toBeVisible(); // Verify no unexpected console errors expect( consoleErrors.filter((e) => !e.includes('404') && !e.includes('Failed to fetch')), ).toHaveLength(0); }); test('US-85: 404 Not Found - home link navigates back to onlineboard', async ({ page }) => { // Navigate to invalid route await page.goto('http://localhost:3001/ru-ru/not-found-test'); // Verify 404 page appears with Russian text await expect(page.locator('h1:has-text("404")')).toBeVisible(); await expect(page.locator('text=Страница не найдена')).toBeVisible(); // Click home link to navigate back const homeLink = page.getByRole('link', { name: /На главную|Home/i }); await homeLink.click(); // Verify navigation returns to home page await page.waitForURL(/\/ru-ru\/(onlineboard|flights)?/); // Wait for page to fully load await page.waitForLoadState('networkidle'); // Verify page content loaded (should show main flight board) const mainContent = page.locator('main[role="main"]'); await expect(mainContent).toBeVisible({ timeout: 5000 }); }); test('US-86: 500 Server Error - HTTP 500 response renders error page with Russian text', async ({ page, }) => { // Set up route to respond with actual HTTP 500 status code let requestCount = 0; await page.route('**/api/**', (route) => { requestCount++; if (requestCount === 1) { // Respond with actual HTTP 500 route.respond({ status: 500, contentType: 'application/json', body: JSON.stringify({ error: 'Server Error' }), }); } else { route.continue(); } }); // Navigate to page that requires API await page.goto('http://localhost:3001/ru-ru/onlineboard', { waitUntil: 'domcontentloaded', }); // Wait for error handling to occur await page.waitForTimeout(2000); // Verify ServerErrorPage renders with 500 heading visible const serverErrorHeading = page.locator('h1:has-text("500")'); await expect(serverErrorHeading).toBeVisible({ timeout: 5000 }); // Verify Russian error title is displayed const rusTitle = page.locator('text=Ошибка сервера'); await expect(rusTitle).toBeVisible(); // Verify Russian description is displayed const rusDescription = page.locator('text=/К сожалению|произошла ошибка/'); await expect(rusDescription).toBeVisible(); // Verify error page has role="alert" for accessibility const alertMain = page.locator('main[role="alert"]'); await expect(alertMain).toBeVisible(); }); test('US-86: 500 Server Error - "Try Again" button reloads and retries API', async ({ page }) => { let requestCount = 0; let secondRequestMade = false; // Set up route to fail first request, succeed on second await page.route('**/api/**', (route) => { requestCount++; if (requestCount === 1) { // First request: respond with HTTP 500 route.respond({ status: 500, contentType: 'application/json', body: JSON.stringify({ error: 'Server Error' }), }); } else { // Second request: succeed secondRequestMade = true; route.continue(); } }); // Navigate to page await page.goto('http://localhost:3001/ru-ru/onlineboard', { waitUntil: 'domcontentloaded', }); // Wait for error page to render await page.waitForTimeout(1500); // Verify 500 error page is visible await expect(page.locator('h1:has-text("500")')).toBeVisible(); await expect(page.locator('text=Ошибка сервера')).toBeVisible(); // Find and click "Try Again" button (should trigger page reload) const tryAgainButton = page.getByRole('button', { name: /Try Again|Перезагрузить/i }); await expect(tryAgainButton).toBeVisible(); // Track if new request is made after button click const requestsBeforeClick = requestCount; await tryAgainButton.click(); // Wait for new request to be made await page.waitForTimeout(1500); // Verify error page is hidden after retry await expect(page.locator('h1:has-text("500")')).toBeHidden({ timeout: 5000 }); }); test('US-86: 500 Server Error - "Go Home" link navigates away from error', async ({ page }) => { // Set up route to respond with HTTP 500 await page.route('**/api/**', (route) => { route.respond({ status: 500, contentType: 'application/json', body: JSON.stringify({ error: 'Server Error' }), }); }); // Navigate to page await page.goto('http://localhost:3001/ru-ru/onlineboard', { waitUntil: 'domcontentloaded', }); // Wait for error page to render await page.waitForTimeout(1500); // Verify 500 error page is visible await expect(page.locator('h1:has-text("500")')).toBeVisible(); // Find and click "Go Home" link const goHomeLink = page.getByRole('link', { name: /Go Home|На главную/i }); await expect(goHomeLink).toBeVisible(); await goHomeLink.click(); // Verify navigation happens (should go to home route) await page.waitForURL(/\/ru-ru\/(|flights|onlineboard)/); }); test('US-88: Timeout Detection - indicator appears after 30 seconds of waiting', async ({ page, }) => { // Set up route to delay response beyond 30 second timeout await page.route('**/api/**', async (route) => { await new Promise((resolve) => setTimeout(resolve, 32000)); await route.continue().catch(() => {}); }); // Track when timeout indicator appears const startTime = Date.now(); let timeoutAppearedAt: number | null = null; // Navigate and allow slow load const navigationPromise = page.goto('http://localhost:3001/ru-ru/onlineboard', { waitUntil: 'domcontentloaded', timeout: 35000, }); // Wait for timeout indicator to appear (should be around 30 seconds) // Start checking at 25 seconds to catch it await page.waitForTimeout(25000); // Poll for timeout indicator appearance every 500ms for up to 10 seconds for (let i = 0; i < 20; i++) { const indicator = page.locator('[role="alert"]').filter({ hasText: /timeout|истекло|time|истек/i, }); if (await indicator.isVisible().catch(() => false)) { timeoutAppearedAt = Date.now() - startTime; break; } await page.waitForTimeout(500); } // Verify timeout indicator appeared within expected window (28-32 seconds = 30±2s tolerance) expect(timeoutAppearedAt).not.toBeNull(); if (timeoutAppearedAt !== null) { expect(timeoutAppearedAt).toBeGreaterThanOrEqual(28000); // 28 seconds expect(timeoutAppearedAt).toBeLessThanOrEqual(32000); // 32 seconds } // Verify Russian timeout text is visible const rusTimeout = page.locator('text=/Истекло время|Запрос занял/'); await expect(rusTimeout).toBeVisible({ timeout: 5000 }); // Allow navigation to complete await navigationPromise.catch(() => {}); }); test('US-88: Timeout Detection - retry button exists and re-executes search', async ({ page, }) => { let firstRequestCompleted = false; let retryRequestMade = false; // Set up route to delay first request await page.route('**/api/**', async (route) => { if (!firstRequestCompleted) { firstRequestCompleted = true; // Delay first request beyond timeout await new Promise((resolve) => setTimeout(resolve, 32000)); await route.continue().catch(() => {}); } else { // Subsequent requests succeed immediately retryRequestMade = true; await route.continue(); } }); // Navigate to page const navigationPromise = page.goto('http://localhost:3001/ru-ru/onlineboard', { waitUntil: 'domcontentloaded', timeout: 35000, }); // Wait for timeout to appear and retry button to be available await page.waitForTimeout(31000); // Find retry button in timeout indicator const retryButton = page .locator('[role="alert"]') .filter({ hasText: /Повторить|retry/i }) .locator('button'); const retryVisible = await retryButton.isVisible().catch(() => false); if (retryVisible) { // Click retry button await retryButton.click({ timeout: 5000 }); // Wait briefly for new request to be made await page.waitForTimeout(1500); // Verify timeout indicator is hidden after retry await expect( page.locator('[role="alert"]').filter({ hasText: /timeout|истекло|time/i }), ).toBeHidden({ timeout: 5000 }); } // Allow navigation to complete await navigationPromise.catch(() => {}); }); });