Files
flights_web/tests/e2e-angular/ru-ru/form-validation.spec.ts
T
gnezim 375bcfb0fa Add e2e test suite from flights-front with Angular API mocks
Copies Playwright e2e tests (58 specs, 300+ tests) designed for cross-app
testing. Adapts API mocks to match real Aeroflot dictionary format (title
objects with multilingual keys), adds board/schedule/days endpoint mocks,
and provides Angular-specific Playwright config on port 4203.
2026-04-15 23:07:44 +03:00

471 lines
16 KiB
TypeScript

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);
});
});
});