import { test, expect } from '@playwright/test'; const BASE_URL = process.env.BASE_URL || 'http://localhost:5173'; test.describe('Form Validation - Parameter Validation (US-90)', () => { test.beforeEach(async ({ page }) => { await page.goto(BASE_URL, { waitUntil: 'networkidle' }); await page.waitForLoadState('networkidle'); }); test.describe('SearchByRoute (FlightBoard) Validation', () => { test('should reject search with same departure and arrival city', async ({ page }) => { // Open the route search tab const routeTab = page.getByTestId('filter-route-tab'); await routeTab.click(); // Wait for city inputs const departureInput = page.getByTestId('filter-route-departure-input'); const arrivalInput = page.getByTestId('filter-route-arrival-input'); await expect(departureInput).toBeVisible(); await expect(arrivalInput).toBeVisible(); // Type same city in both fields await departureInput.fill('Москва'); await page.waitForTimeout(300); // Click on the first autocomplete option const firstOption = page.locator('[role="option"]').first(); if (await firstOption.isVisible()) { await firstOption.click(); } // Now type the same city in arrival await arrivalInput.fill('Москва'); await page.waitForTimeout(300); if (await firstOption.isVisible()) { await firstOption.click(); } // Click search - should show validation error const searchBtn = page.getByTestId('filter-route-search'); await searchBtn.click(); // Check for validation error const errorMsg = page.getByTestId('filter-route-validation-error'); await expect(errorMsg).toBeVisible(); await expect(errorMsg).toContainText(/городами|different/i); }); test('should allow valid route search with different cities', async ({ page }) => { const routeTab = page.getByTestId('filter-route-tab'); await routeTab.click(); const departureInput = page.getByTestId('filter-route-departure-input'); const arrivalInput = page.getByTestId('filter-route-arrival-input'); // Type departure city await departureInput.fill('Москва'); await page.waitForTimeout(300); const firstOption = page.locator('[role="option"]').first(); if (await firstOption.isVisible()) { await firstOption.click(); } // Type different arrival city await arrivalInput.fill('Санкт-Петербург'); await page.waitForTimeout(300); if (await firstOption.isVisible()) { await firstOption.click(); } // Click search - should proceed without validation error const searchBtn = page.getByTestId('filter-route-search'); await searchBtn.click(); // Wait for navigation await page.waitForNavigation({ waitUntil: 'networkidle', timeout: 5000 }).catch(() => { // Navigation might happen quickly }); // Should either be on results page or no validation error shown const errorMsg = page.getByTestId('filter-route-validation-error'); const isErrorVisible = await errorMsg.isVisible().catch(() => false); expect(isErrorVisible).toBe(false); }); test('should display validation error when attempting same city search', async ({ page }) => { const routeTab = page.getByTestId('filter-route-tab'); await routeTab.click(); const departureInput = page.getByTestId('filter-route-departure-input'); const arrivalInput = page.getByTestId('filter-route-arrival-input'); // Select same city twice await departureInput.fill('SVO'); await page.waitForTimeout(300); const firstOption = page.locator('[role="option"]').first(); if (await firstOption.isVisible()) { await firstOption.click(); } // Get the city code from the first input const cityCode = page.locator('.labelRow').first(); await expect(cityCode).toContainText(/SVO|MOW|SPB/); // Try to select same city in arrival await arrivalInput.fill('SVO'); await page.waitForTimeout(300); if (await firstOption.isVisible()) { await firstOption.click(); } const searchBtn = page.getByTestId('filter-route-search'); await searchBtn.click(); // Verify error is shown const errorMsg = page.getByTestId('filter-route-validation-error'); await expect(errorMsg).toBeVisible({ timeout: 2000 }); }); }); test.describe('Schedule Search Panel Validation', () => { test('should show validation error for missing departure city', async ({ page }) => { // Navigate to schedule page await page.goto(`${BASE_URL}/ru-ru/schedule`, { waitUntil: 'networkidle' }); const searchBtn = page.getByTestId('schedule-search-button'); await expect(searchBtn).toBeVisible(); // Click search without filling departure city await searchBtn.click(); // Check for error const errorMsg = page.getByTestId('schedule-validation-error'); await expect(errorMsg).toBeVisible(); await expect(errorMsg).toContainText(/вылета|departure/i); }); test('should show validation error for missing arrival city', async ({ page }) => { await page.goto(`${BASE_URL}/ru-ru/schedule`, { waitUntil: 'networkidle' }); const fromInput = page.getByPlaceholder(/город/i).first(); const searchBtn = page.getByTestId('schedule-search-button'); // Fill only departure city await fromInput.fill('Москва'); await page.waitForTimeout(300); const firstOption = page.locator('[role="option"]').first(); if (await firstOption.isVisible()) { await firstOption.click(); } // Search without arrival city await searchBtn.click(); // Should show error const errorMsg = page.getByTestId('schedule-validation-error'); await expect(errorMsg).toBeVisible(); }); test('should reject search with same departure and arrival city', async ({ page }) => { await page.goto(`${BASE_URL}/ru-ru/schedule`, { waitUntil: 'networkidle' }); const inputs = page.getByPlaceholder(/город|city/i); const fromInput = inputs.first(); const toInput = inputs.nth(1); const searchBtn = page.getByTestId('schedule-search-button'); // Fill both with same city await fromInput.fill('Москва'); await page.waitForTimeout(200); const firstOption = page.locator('[role="option"]').first(); if (await firstOption.isVisible()) { await firstOption.click(); } await toInput.fill('Москва'); await page.waitForTimeout(200); if (await firstOption.isVisible()) { await firstOption.click(); } // Search await searchBtn.click(); // Should show error about different cities const errorMsg = page.getByTestId('schedule-validation-error'); await expect(errorMsg).toBeVisible({ timeout: 2000 }); await expect(errorMsg).toContainText(/отличаться|different/i); }); test('should show validation error for past departure date', async ({ page }) => { await page.goto(`${BASE_URL}/ru-ru/schedule`, { waitUntil: 'networkidle' }); const inputs = page.getByPlaceholder(/город|city/i); const fromInput = inputs.first(); const toInput = inputs.nth(1); const searchBtn = page.getByTestId('schedule-search-button'); // Fill cities with different ones await fromInput.fill('Москва'); await page.waitForTimeout(200); let firstOption = page.locator('[role="option"]').first(); if (await firstOption.isVisible()) { await firstOption.click(); } await toInput.fill('Санкт-Петербург'); await page.waitForTimeout(200); firstOption = page.locator('[role="option"]').first(); if (await firstOption.isVisible()) { await firstOption.click(); } // Set date to past const dateInput = page.getByTestId('schedule-departure-calendar'); if (await dateInput.isVisible()) { const input = dateInput.locator('input[type="date"]').first(); const pastDate = new Date(); pastDate.setDate(pastDate.getDate() - 5); const dateStr = pastDate.toISOString().split('T')[0]; await input.fill(dateStr); } // Search await searchBtn.click(); // Should show error about past date const errorMsg = page.getByTestId('schedule-validation-error'); await expect(errorMsg).toBeVisible({ timeout: 2000 }); await expect(errorMsg).toContainText(/прошлого|past|past/i); }); test('should allow valid schedule search', async ({ page }) => { await page.goto(`${BASE_URL}/ru-ru/schedule`, { waitUntil: 'networkidle' }); const inputs = page.getByPlaceholder(/город|city/i); const fromInput = inputs.first(); const toInput = inputs.nth(1); const searchBtn = page.getByTestId('schedule-search-button'); // Fill with valid different cities await fromInput.fill('Москва'); await page.waitForTimeout(200); let firstOption = page.locator('[role="option"]').first(); if (await firstOption.isVisible()) { await firstOption.click(); } await toInput.fill('Санкт-Петербург'); await page.waitForTimeout(200); firstOption = page.locator('[role="option"]').first(); if (await firstOption.isVisible()) { await firstOption.click(); } // Search with valid data (default is today) await searchBtn.click(); // Wait for navigation or no error await page.waitForNavigation({ waitUntil: 'networkidle', timeout: 5000 }).catch(() => { // Navigation happened or error occurred }); const errorMsg = page.getByTestId('schedule-validation-error'); const isErrorVisible = await errorMsg.isVisible().catch(() => false); expect(isErrorVisible).toBe(false); }); test('should show validation error when return date is before departure date', async ({ page, }) => { await page.goto(`${BASE_URL}/ru-ru/schedule`, { waitUntil: 'networkidle' }); const inputs = page.getByPlaceholder(/город|city/i); const fromInput = inputs.first(); const toInput = inputs.nth(1); // Fill cities await fromInput.fill('Москва'); await page.waitForTimeout(200); let firstOption = page.locator('[role="option"]').first(); if (await firstOption.isVisible()) { await firstOption.click(); } await toInput.fill('Санкт-Петербург'); await page.waitForTimeout(200); firstOption = page.locator('[role="option"]').first(); if (await firstOption.isVisible()) { await firstOption.click(); } // Enable return flight const returnCheckbox = page.getByTestId('schedule-return-checkbox'); if (await returnCheckbox.isVisible()) { await returnCheckbox.click(); // Set dates const dateInputs = page.getByTestId('schedule-return-calendar'); if (await dateInputs.isVisible()) { const inputs = dateInputs.locator('input[type="date"]'); // Set return date before departure date const futureDate = new Date(); futureDate.setDate(futureDate.getDate() + 5); const pastReturnDate = new Date(); pastReturnDate.setDate(pastReturnDate.getDate() + 2); // Before departure await inputs.first().fill(pastReturnDate.toISOString().split('T')[0]); await inputs.last().fill(futureDate.toISOString().split('T')[0]); } // Search const searchBtn = page.getByTestId('schedule-search-button'); await searchBtn.click(); // May show validation error or allow (depending on logic) await page.waitForTimeout(500); } }); }); test.describe('Accessibility & Error Messages', () => { test('should display validation error with proper ARIA attributes', async ({ page }) => { await page.goto(`${BASE_URL}`, { waitUntil: 'networkidle' }); const routeTab = page.getByTestId('filter-route-tab'); await routeTab.click(); const departureInput = page.getByTestId('filter-route-departure-input'); const arrivalInput = page.getByTestId('filter-route-arrival-input'); // Create same city search await departureInput.fill('MOW'); await page.waitForTimeout(200); const firstOption = page.locator('[role="option"]').first(); if (await firstOption.isVisible()) { await firstOption.click(); } await arrivalInput.fill('MOW'); await page.waitForTimeout(200); if (await firstOption.isVisible()) { await firstOption.click(); } // Click search const searchBtn = page.getByTestId('filter-route-search'); await searchBtn.click(); // Verify error message accessibility const errorMsg = page.getByTestId('filter-route-validation-error'); await expect(errorMsg).toHaveAttribute('role', 'alert'); await expect(errorMsg).toHaveAttribute('aria-live', 'polite'); await expect(errorMsg).toBeVisible(); }); test('should clear validation errors when user corrects input', async ({ page }) => { await page.goto(`${BASE_URL}/ru-ru/schedule`, { waitUntil: 'networkidle' }); const inputs = page.getByPlaceholder(/город|city/i); const fromInput = inputs.first(); const toInput = inputs.nth(1); const searchBtn = page.getByTestId('schedule-search-button'); // Create invalid search await fromInput.fill('Москва'); await page.waitForTimeout(200); let firstOption = page.locator('[role="option"]').first(); if (await firstOption.isVisible()) { await firstOption.click(); } // Same city await toInput.fill('Москва'); await page.waitForTimeout(200); firstOption = page.locator('[role="option"]').first(); if (await firstOption.isVisible()) { await firstOption.click(); } // Search await searchBtn.click(); // Error should appear const errorMsg = page.getByTestId('schedule-validation-error'); await expect(errorMsg).toBeVisible({ timeout: 2000 }); // Now correct the error - change to different city await toInput.fill(''); await toInput.fill('Санкт-Петербург'); await page.waitForTimeout(200); firstOption = page.locator('[role="option"]').first(); if (await firstOption.isVisible()) { await firstOption.click(); } // Search again await searchBtn.click(); // Error should clear await page.waitForTimeout(500); // After correction, either no error or different page loaded }); }); test.describe('Console Error Checks', () => { test('should have no console errors during form validation', async ({ page }) => { const errors: string[] = []; page.on('console', (msg) => { if (msg.type() === 'error') { errors.push(msg.text()); } }); await page.goto(`${BASE_URL}`, { waitUntil: 'networkidle' }); const routeTab = page.getByTestId('filter-route-tab'); await routeTab.click(); const departureInput = page.getByTestId('filter-route-departure-input'); const arrivalInput = page.getByTestId('filter-route-arrival-input'); const searchBtn = page.getByTestId('filter-route-search'); // Perform validation test await departureInput.fill('TEST'); await page.waitForTimeout(300); const firstOption = page.locator('[role="option"]').first(); if (await firstOption.isVisible()) { await firstOption.click(); } await arrivalInput.fill('TEST'); await page.waitForTimeout(300); if (await firstOption.isVisible()) { await firstOption.click(); } await searchBtn.click(); await page.waitForTimeout(500); // Should have no console errors expect(errors.length).toBe(0); }); }); });