375bcfb0fa
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.
471 lines
16 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|