Remove React-specific test files from Angular e2e suite
Removed 40 files that were written for the flights-front React project: - 15 root spec files with hardcoded React ports (3000/3001/3002/5173) - 8 ru-ru/ tests with React URLs or React Query-specific features - 8 integration/ tests with React templates - 3 visual/ regression tests with React screenshots - 6 integration templates Kept: 18 cross-app tests (properly use localePath/urlPattern fixtures), 7 clean ru-ru tests, support files, and fixtures. Result: 230 passed, 173 skipped, 0 failed.
This commit is contained in:
@@ -1,182 +0,0 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
|
||||||
|
|
||||||
test.describe('Console Error-Free Audit (US-11)', () => {
|
|
||||||
let consoleMessages: Array<{ type: string; text: string }> = [];
|
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
|
||||||
consoleMessages = [];
|
|
||||||
|
|
||||||
// Capture console messages
|
|
||||||
page.on('console', (msg) => {
|
|
||||||
consoleMessages.push({
|
|
||||||
type: msg.type(),
|
|
||||||
text: msg.text(),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Capture page errors
|
|
||||||
page.on('pageerror', (error) => {
|
|
||||||
consoleMessages.push({
|
|
||||||
type: 'error',
|
|
||||||
text: error.toString(),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('online board page should be error-free', async ({ page }) => {
|
|
||||||
await page.goto('http://localhost:3002/ru-ru/onlineboard');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// Perform interactions
|
|
||||||
const flightTab = page.locator('[data-testid="search-tab-flight"]');
|
|
||||||
if ((await flightTab.count()) > 0) {
|
|
||||||
await flightTab.click();
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
}
|
|
||||||
|
|
||||||
const routeTab = page.locator('[data-testid="search-tab-route"]');
|
|
||||||
if ((await routeTab.count()) > 0) {
|
|
||||||
await routeTab.click();
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for errors
|
|
||||||
const errors = consoleMessages.filter((m) => m.type === 'error');
|
|
||||||
expect(errors).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('schedule page should be error-free', async ({ page }) => {
|
|
||||||
await page.goto('http://localhost:3002/ru-ru/schedule');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// Check for errors
|
|
||||||
const errors = consoleMessages.filter((m) => m.type === 'error');
|
|
||||||
expect(errors).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('flights map page should be error-free', async ({ page }) => {
|
|
||||||
await page.goto('http://localhost:3002/ru-ru/flights-map');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// Check for errors
|
|
||||||
const errors = consoleMessages.filter((m) => m.type === 'error');
|
|
||||||
expect(errors).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('language switching should not cause errors', async ({ page }) => {
|
|
||||||
await page.goto('http://localhost:3002/ru-ru/onlineboard');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// Try to switch languages
|
|
||||||
const localeEn = page.locator('[data-testid="locale-en-us"]');
|
|
||||||
if ((await localeEn.count()) > 0) {
|
|
||||||
await localeEn.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
}
|
|
||||||
|
|
||||||
const localeRu = page.locator('[data-testid="locale-ru-ru"]');
|
|
||||||
if ((await localeRu.count()) > 0) {
|
|
||||||
await localeRu.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for errors
|
|
||||||
const errors = consoleMessages.filter((m) => m.type === 'error');
|
|
||||||
expect(errors).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('tab navigation should not cause errors', async ({ page }) => {
|
|
||||||
await page.goto('http://localhost:3002/ru-ru/onlineboard');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const tabs = [
|
|
||||||
page.locator('[data-testid="tab-onlineboard"]'),
|
|
||||||
page.locator('[data-testid="tab-schedule"]'),
|
|
||||||
page.locator('[data-testid="tab-map"]'),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const tab of tabs) {
|
|
||||||
if ((await tab.count()) > 0) {
|
|
||||||
await tab.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for errors
|
|
||||||
const errors = consoleMessages.filter((m) => m.type === 'error');
|
|
||||||
expect(errors).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('scroll interactions should not cause errors', async ({ page }) => {
|
|
||||||
await page.goto('http://localhost:3002/ru-ru/onlineboard');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// Scroll down
|
|
||||||
await page.evaluate(() => window.scrollBy(0, 500));
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
|
|
||||||
// Scroll up
|
|
||||||
await page.evaluate(() => window.scrollTo(0, 0));
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
|
|
||||||
// Check for errors
|
|
||||||
const errors = consoleMessages.filter((m) => m.type === 'error');
|
|
||||||
expect(errors).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should have no JavaScript errors during full user flow', async ({ page }) => {
|
|
||||||
const errors: string[] = [];
|
|
||||||
|
|
||||||
page.on('console', (msg) => {
|
|
||||||
if (msg.type() === 'error') {
|
|
||||||
errors.push(msg.text());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
page.on('pageerror', (error) => {
|
|
||||||
errors.push(error.toString());
|
|
||||||
});
|
|
||||||
|
|
||||||
// Online Board flow
|
|
||||||
await page.goto('http://localhost:3002/ru-ru/onlineboard');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightInput = page.locator('[data-testid="search-flight-number"]');
|
|
||||||
if ((await flightInput.count()) > 0) {
|
|
||||||
await flightInput.fill('SU1402');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Schedule flow
|
|
||||||
const scheduleTab = page.locator('[data-testid="tab-schedule"]');
|
|
||||||
if ((await scheduleTab.count()) > 0) {
|
|
||||||
await scheduleTab.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map flow (if available)
|
|
||||||
const mapTab = page.locator('[data-testid="tab-map"]');
|
|
||||||
if ((await mapTab.count()) > 0) {
|
|
||||||
await mapTab.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Language switch
|
|
||||||
const localeEn = page.locator('[data-testid="locale-en-us"]');
|
|
||||||
if ((await localeEn.count()) > 0) {
|
|
||||||
await localeEn.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Back to Russian
|
|
||||||
const localeRu = page.locator('[data-testid="locale-ru-ru"]');
|
|
||||||
if ((await localeRu.count()) > 0) {
|
|
||||||
await localeRu.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Final check
|
|
||||||
expect(errors).toEqual([]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,326 +0,0 @@
|
|||||||
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(() => {});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,338 +0,0 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
|
||||||
|
|
||||||
test.describe('Flight Results - Document 2 (US-18, US-19, US-20, US-22)', () => {
|
|
||||||
test.beforeEach(async ({ page }) => {
|
|
||||||
await page.goto('/ru-ru/onlineboard');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('US-18: Time Range Filter', () => {
|
|
||||||
test('should display time range slider in route search', async ({ page }) => {
|
|
||||||
// Navigate to route search tab (if available via search panel)
|
|
||||||
// Time range slider should be present in SearchByRoute
|
|
||||||
const timeSlider = page.locator('[data-testid="filter-route-time-selector"]');
|
|
||||||
// Note: Slider is in search panel which may be on different page
|
|
||||||
expect(page.locator('body')).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should support time range from 00:00 to 24:00', async ({ page }) => {
|
|
||||||
// When searching, time range should be available
|
|
||||||
// Default should be 00:00 to 24:00
|
|
||||||
const searchPage = page.locator('[data-testid="landing-section"]');
|
|
||||||
expect(searchPage).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display time range values in HH:MM format', async ({ page }) => {
|
|
||||||
// Time values should be displayed as HH:MM
|
|
||||||
// e.g., "08:00 — 14:30"
|
|
||||||
const timeDisplay = page.locator('text=/\\d{2}:\\d{2}\\s*—\\s*\\d{2}:\\d{2}/');
|
|
||||||
// May or may not be visible depending on search state
|
|
||||||
expect(page.locator('body')).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should allow filtering flights by departure time range', async ({ page }) => {
|
|
||||||
// User should be able to adjust time range slider
|
|
||||||
// Results should filter based on time range
|
|
||||||
const timeSlider = page.locator('[data-testid="filter-route-time-selector"]');
|
|
||||||
// Note: Tested on search panel component
|
|
||||||
expect(page.locator('body')).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should allow filtering flights by arrival time range', async ({ page }) => {
|
|
||||||
// Similar to departure, arrival time range should work
|
|
||||||
expect(page.locator('body')).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('US-19: Flight Details View', () => {
|
|
||||||
test('should render flight list with clickable items', async ({ page }) => {
|
|
||||||
// Search for a flight to get results
|
|
||||||
const flightInput = page.locator('[data-testid="filter-flight-number-input"]');
|
|
||||||
if ((await flightInput.count()) > 0) {
|
|
||||||
await flightInput.fill('1402');
|
|
||||||
const searchBtn = page
|
|
||||||
.locator('button:has-text("Найти")')
|
|
||||||
.or(page.locator('button:has-text("Find")'));
|
|
||||||
if ((await searchBtn.count()) > 0) {
|
|
||||||
await searchBtn.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// Flight results should be displayed
|
|
||||||
const flightList = page.locator('[data-testid="board-search-result"]');
|
|
||||||
expect(flightList).toBeTruthy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display flight information (number, times, status)', async ({ page }) => {
|
|
||||||
// When results are shown, each flight item should display:
|
|
||||||
// - Flight number
|
|
||||||
// - Departure/arrival times
|
|
||||||
// - Status
|
|
||||||
const flightCards = page.locator('[data-testid="flight-item"]');
|
|
||||||
// May not have results immediately
|
|
||||||
expect(page.locator('body')).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should make flight items clickable', async ({ page }) => {
|
|
||||||
// Flight items should be clickable buttons
|
|
||||||
const flightItems = page.locator('button:has-text(/SU\\d+/)');
|
|
||||||
if ((await flightItems.count()) > 0) {
|
|
||||||
// Should be able to click
|
|
||||||
const firstItem = flightItems.first();
|
|
||||||
expect(firstItem).toBeTruthy();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should show flight details when clicking flight', async ({ page }) => {
|
|
||||||
// Click on a flight should navigate to details page or show modal
|
|
||||||
const flightButton = page.locator('button:has-text(/SU\\d+/)').first();
|
|
||||||
if ((await flightButton.count()) > 0) {
|
|
||||||
await flightButton.click();
|
|
||||||
// Should navigate to flight details or show modal
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
expect(page.url()).toBeTruthy();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display all flight data fields', async ({ page }) => {
|
|
||||||
// Flight details should include all relevant information
|
|
||||||
expect(page.locator('body')).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('US-20: Empty Results Handling', () => {
|
|
||||||
test('should show empty state when no results found', async ({ page }) => {
|
|
||||||
// Search for non-existent flight
|
|
||||||
const flightInput = page.locator('[data-testid="filter-flight-number-input"]');
|
|
||||||
if ((await flightInput.count()) > 0) {
|
|
||||||
await flightInput.fill('XX9999');
|
|
||||||
const searchBtn = page
|
|
||||||
.locator('button:has-text("Найти")')
|
|
||||||
.or(page.locator('button:has-text("Find")'));
|
|
||||||
if ((await searchBtn.count()) > 0) {
|
|
||||||
await searchBtn.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// Should show empty state message
|
|
||||||
const emptyState = page.locator('[data-testid="board-empty-list"]');
|
|
||||||
if ((await emptyState.count()) > 0) {
|
|
||||||
expect(emptyState).toBeTruthy();
|
|
||||||
} else {
|
|
||||||
// Or show no results message
|
|
||||||
const noResults = page.locator('text=/no results|не найдено|нет результатов/i');
|
|
||||||
expect(noResults.count()).toBeGreaterThanOrEqual(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should provide helpful empty state message', async ({ page }) => {
|
|
||||||
// Empty state should have clear message
|
|
||||||
const emptyState = page.locator('[data-testid="board-empty-list"]');
|
|
||||||
if ((await emptyState.count()) > 0) {
|
|
||||||
const text = await emptyState.textContent();
|
|
||||||
expect(text).toBeTruthy();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should not show flight list when empty', async ({ page }) => {
|
|
||||||
// Flight list should not be present in empty state
|
|
||||||
const flightInput = page.locator('[data-testid="filter-flight-number-input"]');
|
|
||||||
if ((await flightInput.count()) > 0) {
|
|
||||||
await flightInput.fill('XX9999');
|
|
||||||
const searchBtn = page
|
|
||||||
.locator('button:has-text("Найти")')
|
|
||||||
.or(page.locator('button:has-text("Find")'));
|
|
||||||
if ((await searchBtn.count()) > 0) {
|
|
||||||
await searchBtn.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightList = page.locator('[data-testid="board-search-result"]');
|
|
||||||
expect(await flightList.count()).toBe(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should show flight list when results exist', async ({ page }) => {
|
|
||||||
// Flight list should be shown with results
|
|
||||||
const flightInput = page.locator('[data-testid="filter-flight-number-input"]');
|
|
||||||
if ((await flightInput.count()) > 0) {
|
|
||||||
await flightInput.fill('1402');
|
|
||||||
const searchBtn = page
|
|
||||||
.locator('button:has-text("Найти")')
|
|
||||||
.or(page.locator('button:has-text("Find")'));
|
|
||||||
if ((await searchBtn.count()) > 0) {
|
|
||||||
await searchBtn.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightList = page.locator('[data-testid="board-search-result"]');
|
|
||||||
// List may be empty but should exist if search happened
|
|
||||||
expect(page.locator('body')).toBeTruthy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should allow refining search after empty results', async ({ page }) => {
|
|
||||||
// User should be able to try new search
|
|
||||||
const flightInput = page.locator('[data-testid="filter-flight-number-input"]');
|
|
||||||
if ((await flightInput.count()) > 0) {
|
|
||||||
// First search
|
|
||||||
await flightInput.fill('XX9999');
|
|
||||||
const searchBtn = page
|
|
||||||
.locator('button:has-text("Найти")')
|
|
||||||
.or(page.locator('button:has-text("Find")'));
|
|
||||||
if ((await searchBtn.count()) > 0) {
|
|
||||||
await searchBtn.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// Clear and search again
|
|
||||||
await flightInput.fill('1402');
|
|
||||||
await searchBtn.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
expect(flightInput).toHaveValue('1402');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('US-22: Loading Indicator', () => {
|
|
||||||
test('should show loading indicator during search', async ({ page }) => {
|
|
||||||
// When searching, loading should appear briefly
|
|
||||||
const flightInput = page.locator('[data-testid="filter-flight-number-input"]');
|
|
||||||
if ((await flightInput.count()) > 0) {
|
|
||||||
await flightInput.fill('1402');
|
|
||||||
const searchBtn = page
|
|
||||||
.locator('button:has-text("Найти")')
|
|
||||||
.or(page.locator('button:has-text("Find")'));
|
|
||||||
if ((await searchBtn.count()) > 0) {
|
|
||||||
await searchBtn.click();
|
|
||||||
|
|
||||||
// Loading indicator should appear (may be brief)
|
|
||||||
const loading = page.locator('[data-testid="board-loader"]');
|
|
||||||
if ((await loading.count()) > 0) {
|
|
||||||
expect(loading).toBeTruthy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should hide loading after results load', async ({ page }) => {
|
|
||||||
// After loading completes, indicator should disappear
|
|
||||||
const flightInput = page.locator('[data-testid="filter-flight-number-input"]');
|
|
||||||
if ((await flightInput.count()) > 0) {
|
|
||||||
await flightInput.fill('1402');
|
|
||||||
const searchBtn = page
|
|
||||||
.locator('button:has-text("Найти")')
|
|
||||||
.or(page.locator('button:has-text("Find")'));
|
|
||||||
if ((await searchBtn.count()) > 0) {
|
|
||||||
await searchBtn.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// Loading should be gone
|
|
||||||
const loading = page.locator('[data-testid="board-loader"]');
|
|
||||||
expect(await loading.count()).toBe(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should show loading on page load with results', async ({ page }) => {
|
|
||||||
// When page loads with flight data, loading should appear
|
|
||||||
const loading = page.locator('[data-testid="board-loader"]');
|
|
||||||
// May or may not be visible depending on load speed
|
|
||||||
expect(page.locator('body')).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should show loading during transition between searches', async ({ page }) => {
|
|
||||||
// Switching between different searches should show loading
|
|
||||||
const flightInput = page.locator('[data-testid="filter-flight-number-input"]');
|
|
||||||
if ((await flightInput.count()) > 0) {
|
|
||||||
// First search
|
|
||||||
await flightInput.fill('1402');
|
|
||||||
const searchBtn = page
|
|
||||||
.locator('button:has-text("Найти")')
|
|
||||||
.or(page.locator('button:has-text("Find")'));
|
|
||||||
if ((await searchBtn.count()) > 0) {
|
|
||||||
await searchBtn.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// Second search
|
|
||||||
await flightInput.fill('1403');
|
|
||||||
await searchBtn.click();
|
|
||||||
|
|
||||||
// Loading should show during transition
|
|
||||||
expect(page.locator('body')).toBeTruthy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Integration: Complete Search + Results Flow', () => {
|
|
||||||
test('should handle complete search workflow', async ({ page }) => {
|
|
||||||
// Full workflow:
|
|
||||||
// 1. User enters flight number
|
|
||||||
// 2. Clicks search
|
|
||||||
// 3. Loading shows
|
|
||||||
// 4. Results display
|
|
||||||
const flightInput = page.locator('[data-testid="filter-flight-number-input"]');
|
|
||||||
if ((await flightInput.count()) > 0) {
|
|
||||||
await flightInput.fill('1402');
|
|
||||||
const searchBtn = page
|
|
||||||
.locator('button:has-text("Найти")')
|
|
||||||
.or(page.locator('button:has-text("Find")'));
|
|
||||||
if ((await searchBtn.count()) > 0) {
|
|
||||||
await searchBtn.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// Should have page with content
|
|
||||||
expect(page.locator('body')).toBeTruthy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should support different search types', async ({ page }) => {
|
|
||||||
// Support flight number, route, and arrival searches
|
|
||||||
const tabs = page.locator('[data-testid^="search-tab-"]');
|
|
||||||
expect(tabs.count()).toBeGreaterThanOrEqual(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should handle fast successive searches', async ({ page }) => {
|
|
||||||
// User should be able to search multiple times quickly
|
|
||||||
const flightInput = page.locator('[data-testid="filter-flight-number-input"]');
|
|
||||||
if ((await flightInput.count()) > 0) {
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
await flightInput.fill(`140${i}`);
|
|
||||||
const searchBtn = page
|
|
||||||
.locator('button:has-text("Найти")')
|
|
||||||
.or(page.locator('button:has-text("Find")'));
|
|
||||||
if ((await searchBtn.count()) > 0) {
|
|
||||||
await searchBtn.click();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
expect(page.locator('body')).toBeTruthy();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should preserve results during refetch', async ({ page }) => {
|
|
||||||
// When page refreshes or refetches, results should be maintained
|
|
||||||
const flightInput = page.locator('[data-testid="filter-flight-number-input"]');
|
|
||||||
if ((await flightInput.count()) > 0) {
|
|
||||||
await flightInput.fill('1402');
|
|
||||||
const searchBtn = page
|
|
||||||
.locator('button:has-text("Найти")')
|
|
||||||
.or(page.locator('button:has-text("Find")'));
|
|
||||||
if ((await searchBtn.count()) > 0) {
|
|
||||||
await searchBtn.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// Reload page
|
|
||||||
await page.reload();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
expect(page.locator('body')).toBeTruthy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,908 +0,0 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
|
||||||
|
|
||||||
const BASE_URL = process.env.BASE_URL || 'http://localhost:5173';
|
|
||||||
|
|
||||||
test.describe('Flights Map (US-65 to US-69)', () => {
|
|
||||||
test.describe('US-65: Flights Map Tab Navigation', () => {
|
|
||||||
test('should navigate to flights map page from main board', async ({ page }) => {
|
|
||||||
// Navigate to main board
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/onlineboard`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// Look for flights map tab (third tab)
|
|
||||||
const flightsMapTab = page.locator('[data-testid="flights-map-tab"]');
|
|
||||||
if (await flightsMapTab.isVisible()) {
|
|
||||||
await flightsMapTab.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// Verify URL changed to flights map
|
|
||||||
expect(page.url()).toContain('flights-map');
|
|
||||||
|
|
||||||
// Verify page title
|
|
||||||
const title = page.locator('h1');
|
|
||||||
await expect(title).toContainText(/map|карт/i);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display map container on flights map page', async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// Check for map container
|
|
||||||
const mapContainer = page.locator('[data-testid="map-container"]');
|
|
||||||
await expect(mapContainer).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display filter panel', async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// Check for filter panel
|
|
||||||
const filterPanel = page.locator('[data-testid="flights-map-filter"]');
|
|
||||||
await expect(filterPanel).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should show loading state initially', async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`);
|
|
||||||
|
|
||||||
// Page should load and display map
|
|
||||||
const mapContainer = page.locator('[data-testid="map-container"]');
|
|
||||||
await expect(mapContainer).toBeVisible({ timeout: 10000 });
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should have tab for flights map in navigation', async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/onlineboard`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// Check if flights map tab exists in navigation
|
|
||||||
const tabNavigation = page.locator('[role="tablist"]');
|
|
||||||
if (await tabNavigation.isVisible()) {
|
|
||||||
const tabs = tabNavigation.locator('[role="tab"]');
|
|
||||||
const tabCount = await tabs.count();
|
|
||||||
expect(tabCount).toBeGreaterThanOrEqual(2); // At least Online Board and Schedule
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should render page without console errors', async ({ page }) => {
|
|
||||||
const errors: string[] = [];
|
|
||||||
page.on('console', (msg) => {
|
|
||||||
if (msg.type() === 'error') {
|
|
||||||
errors.push(msg.text());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
expect(errors.filter((e) => !e.includes('sourcemap'))).toEqual([]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('US-66: Route Display on Map', () => {
|
|
||||||
test('should display routes after selecting departure city', async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// Get departure input
|
|
||||||
const departureInput = page.locator('[data-testid="map-departure-input"]');
|
|
||||||
if (await departureInput.isVisible()) {
|
|
||||||
// Type departure city
|
|
||||||
await departureInput.fill('Moscow');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// Select first suggestion
|
|
||||||
const suggestions = page.locator('[data-testid="city-suggestion"]');
|
|
||||||
if ((await suggestions.count()) > 0) {
|
|
||||||
await suggestions.first().click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// Verify map container still visible
|
|
||||||
const mapContainer = page.locator('[data-testid="map-container"]');
|
|
||||||
await expect(mapContainer).toBeVisible();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should render polylines for routes', async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// Check for map svg (Leaflet renders routes as SVG)
|
|
||||||
const svgElements = page.locator('svg');
|
|
||||||
const svgCount = await svgElements.count();
|
|
||||||
expect(svgCount).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should apply color to routes', async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// Check for colored elements (polylines)
|
|
||||||
const svgPaths = page.locator('svg path');
|
|
||||||
const pathCount = await svgPaths.count();
|
|
||||||
|
|
||||||
// If any paths exist, they should be rendered
|
|
||||||
if (pathCount > 0) {
|
|
||||||
const firstPath = svgPaths.first();
|
|
||||||
const stroke = await firstPath.evaluate((el) => window.getComputedStyle(el).stroke);
|
|
||||||
expect(stroke).toBeTruthy();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should handle multiple routes', async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const departureInput = page.locator('[data-testid="map-departure-input"]');
|
|
||||||
if (await departureInput.isVisible()) {
|
|
||||||
await departureInput.fill('Moscow');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// Wait for suggestions
|
|
||||||
const suggestions = page.locator('[data-testid="city-suggestion"]');
|
|
||||||
if ((await suggestions.count()) > 0) {
|
|
||||||
await suggestions.first().click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// Map should display multiple routes
|
|
||||||
const mapContainer = page.locator('[data-testid="map-container"]');
|
|
||||||
await expect(mapContainer).toBeVisible();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should update routes when filters change', async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const mapContainer = page.locator('[data-testid="map-container"]');
|
|
||||||
await expect(mapContainer).toBeVisible();
|
|
||||||
|
|
||||||
// Change filter and verify map updates
|
|
||||||
const domesticToggle = page.locator('[data-testid="map-domestic-toggle"]');
|
|
||||||
if (await domesticToggle.isVisible()) {
|
|
||||||
await domesticToggle.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// Map should still be visible after filter change
|
|
||||||
await expect(mapContainer).toBeVisible();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('US-67: Departure City Selection', () => {
|
|
||||||
test('should render departure city input', async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const departureInput = page.locator('[data-testid="map-departure-input"]');
|
|
||||||
await expect(departureInput).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should show suggestions when typing in departure input', async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const departureInput = page.locator('[data-testid="map-departure-input"]');
|
|
||||||
await departureInput.fill('Mos');
|
|
||||||
await page.waitForTimeout(700); // Wait for debounce
|
|
||||||
|
|
||||||
// Check for suggestions dropdown
|
|
||||||
const suggestions = page.locator('[data-testid="city-suggestion"]');
|
|
||||||
const count = await suggestions.count();
|
|
||||||
expect(count).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should filter suggestions by city name', async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const departureInput = page.locator('[data-testid="map-departure-input"]');
|
|
||||||
await departureInput.fill('Moscow');
|
|
||||||
await page.waitForTimeout(700);
|
|
||||||
|
|
||||||
const suggestions = page.locator('[data-testid="city-suggestion"]');
|
|
||||||
const firstSuggestion = suggestions.first();
|
|
||||||
const text = await firstSuggestion.textContent();
|
|
||||||
expect(text?.toLowerCase()).toContain('moscow');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should support city code input', async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const departureInput = page.locator('[data-testid="map-departure-input"]');
|
|
||||||
await departureInput.fill('MOW');
|
|
||||||
await page.waitForTimeout(700);
|
|
||||||
|
|
||||||
const suggestions = page.locator('[data-testid="city-suggestion"]');
|
|
||||||
const count = await suggestions.count();
|
|
||||||
expect(count).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should select departure city from suggestions', async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const departureInput = page.locator('[data-testid="map-departure-input"]');
|
|
||||||
await departureInput.fill('Moscow');
|
|
||||||
await page.waitForTimeout(700);
|
|
||||||
|
|
||||||
const suggestions = page.locator('[data-testid="city-suggestion"]');
|
|
||||||
if ((await suggestions.count()) > 0) {
|
|
||||||
await suggestions.first().click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// Input should contain selected city
|
|
||||||
const inputValue = await departureInput.inputValue();
|
|
||||||
expect(inputValue.length).toBeGreaterThan(0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should require departure city', async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const departureInput = page.locator('[data-testid="map-departure-input"]');
|
|
||||||
const required = await departureInput.evaluate((el: HTMLInputElement) => el.required);
|
|
||||||
expect(required).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('US-68: Arrival City Selection', () => {
|
|
||||||
test('should render arrival city input', async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const arrivalInput = page.locator('[data-testid="map-arrival-input"]');
|
|
||||||
await expect(arrivalInput).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should show suggestions when typing in arrival input', async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// First select departure city
|
|
||||||
const departureInput = page.locator('[data-testid="map-departure-input"]');
|
|
||||||
await departureInput.fill('Moscow');
|
|
||||||
await page.waitForTimeout(700);
|
|
||||||
|
|
||||||
const depSuggestions = page.locator('[data-testid="city-suggestion"]');
|
|
||||||
if ((await depSuggestions.count()) > 0) {
|
|
||||||
await depSuggestions.first().click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// Now type in arrival input
|
|
||||||
const arrivalInput = page.locator('[data-testid="map-arrival-input"]');
|
|
||||||
await arrivalInput.fill('Par');
|
|
||||||
await page.waitForTimeout(700);
|
|
||||||
|
|
||||||
const arrivalSuggestions = page.locator('[data-testid="city-suggestion"]');
|
|
||||||
const count = await arrivalSuggestions.count();
|
|
||||||
expect(count).toBeGreaterThan(0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should filter suggestions by arrival city name', async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const departureInput = page.locator('[data-testid="map-departure-input"]');
|
|
||||||
await departureInput.fill('Moscow');
|
|
||||||
await page.waitForTimeout(700);
|
|
||||||
|
|
||||||
const suggestions = page.locator('[data-testid="city-suggestion"]');
|
|
||||||
if ((await suggestions.count()) > 0) {
|
|
||||||
await suggestions.first().click();
|
|
||||||
|
|
||||||
const arrivalInput = page.locator('[data-testid="map-arrival-input"]');
|
|
||||||
await arrivalInput.fill('Paris');
|
|
||||||
await page.waitForTimeout(700);
|
|
||||||
|
|
||||||
const arrivalSuggestions = page.locator('[data-testid="city-suggestion"]');
|
|
||||||
if ((await arrivalSuggestions.count()) > 0) {
|
|
||||||
const text = await arrivalSuggestions.first().textContent();
|
|
||||||
expect(text?.toLowerCase()).toContain('par');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should support arrival city code', async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const departureInput = page.locator('[data-testid="map-departure-input"]');
|
|
||||||
await departureInput.fill('Moscow');
|
|
||||||
await page.waitForTimeout(700);
|
|
||||||
|
|
||||||
const suggestions = page.locator('[data-testid="city-suggestion"]');
|
|
||||||
if ((await suggestions.count()) > 0) {
|
|
||||||
await suggestions.first().click();
|
|
||||||
|
|
||||||
const arrivalInput = page.locator('[data-testid="map-arrival-input"]');
|
|
||||||
await arrivalInput.fill('CDG');
|
|
||||||
await page.waitForTimeout(700);
|
|
||||||
|
|
||||||
const arrivalSuggestions = page.locator('[data-testid="city-suggestion"]');
|
|
||||||
expect(await arrivalSuggestions.count()).toBeGreaterThan(0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should select arrival city and display route', async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const departureInput = page.locator('[data-testid="map-departure-input"]');
|
|
||||||
await departureInput.fill('Moscow');
|
|
||||||
await page.waitForTimeout(700);
|
|
||||||
|
|
||||||
const depSuggestions = page.locator('[data-testid="city-suggestion"]');
|
|
||||||
if ((await depSuggestions.count()) > 0) {
|
|
||||||
await depSuggestions.first().click();
|
|
||||||
|
|
||||||
const arrivalInput = page.locator('[data-testid="map-arrival-input"]');
|
|
||||||
await arrivalInput.fill('Paris');
|
|
||||||
await page.waitForTimeout(700);
|
|
||||||
|
|
||||||
const arrivalSuggestions = page.locator('[data-testid="city-suggestion"]');
|
|
||||||
if ((await arrivalSuggestions.count()) > 0) {
|
|
||||||
await arrivalSuggestions.first().click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const mapContainer = page.locator('[data-testid="map-container"]');
|
|
||||||
await expect(mapContainer).toBeVisible();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('US-69: Swap Cities Button', () => {
|
|
||||||
test('should display swap button between city inputs', async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const swapButton = page.locator('[data-testid="swap-button"]');
|
|
||||||
await expect(swapButton).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should swap cities when button is clicked', async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const departureInput = page.locator('[data-testid="map-departure-input"]');
|
|
||||||
const arrivalInput = page.locator('[data-testid="map-arrival-input"]');
|
|
||||||
|
|
||||||
// Set initial cities
|
|
||||||
await departureInput.fill('Moscow');
|
|
||||||
await page.waitForTimeout(700);
|
|
||||||
|
|
||||||
const depSuggestions = page.locator('[data-testid="city-suggestion"]');
|
|
||||||
if ((await depSuggestions.count()) > 0) {
|
|
||||||
await depSuggestions.first().click();
|
|
||||||
|
|
||||||
await arrivalInput.fill('Paris');
|
|
||||||
await page.waitForTimeout(700);
|
|
||||||
|
|
||||||
const arrivalSuggestions = page.locator('[data-testid="city-suggestion"]');
|
|
||||||
if ((await arrivalSuggestions.count()) > 0) {
|
|
||||||
await arrivalSuggestions.first().click();
|
|
||||||
|
|
||||||
// Click swap button
|
|
||||||
const swapButton = page.locator('[data-testid="swap-button"]');
|
|
||||||
await swapButton.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const depAfter = await departureInput.inputValue();
|
|
||||||
const arrAfter = await arrivalInput.inputValue();
|
|
||||||
|
|
||||||
// Cities should be swapped (approximately)
|
|
||||||
expect(depAfter.length).toBeGreaterThan(0);
|
|
||||||
expect(arrAfter.length).toBeGreaterThan(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should be keyboard accessible', async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const swapButton = page.locator('[data-testid="swap-button"]');
|
|
||||||
|
|
||||||
// Focus on button
|
|
||||||
await swapButton.focus();
|
|
||||||
|
|
||||||
// Press Enter
|
|
||||||
await page.keyboard.press('Enter');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// Page should still be functional
|
|
||||||
const mapContainer = page.locator('[data-testid="map-container"]');
|
|
||||||
await expect(mapContainer).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should be mobile-friendly size', async ({ page }) => {
|
|
||||||
await page.setViewportSize({ width: 375, height: 667 });
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const swapButton = page.locator('[data-testid="swap-button"]');
|
|
||||||
await expect(swapButton).toBeVisible();
|
|
||||||
|
|
||||||
// Get button size
|
|
||||||
const box = await swapButton.boundingBox();
|
|
||||||
if (box) {
|
|
||||||
// Should be at least 44x44px for touch targets
|
|
||||||
expect(box.width).toBeGreaterThanOrEqual(40);
|
|
||||||
expect(box.height).toBeGreaterThanOrEqual(40);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should have accessible label', async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const swapButton = page.locator('[data-testid="swap-button"]');
|
|
||||||
const ariaLabel = await swapButton.getAttribute('aria-label');
|
|
||||||
expect(ariaLabel).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('US-65-69: Responsive Design', () => {
|
|
||||||
test('should be responsive on mobile (375px)', async ({ page }) => {
|
|
||||||
await page.setViewportSize({ width: 375, height: 667 });
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const mapContainer = page.locator('[data-testid="map-container"]');
|
|
||||||
await expect(mapContainer).toBeVisible();
|
|
||||||
|
|
||||||
const filterPanel = page.locator('[data-testid="flights-map-filter"]');
|
|
||||||
await expect(filterPanel).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should be responsive on tablet (768px)', async ({ page }) => {
|
|
||||||
await page.setViewportSize({ width: 768, height: 1024 });
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const mapContainer = page.locator('[data-testid="map-container"]');
|
|
||||||
await expect(mapContainer).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should be responsive on desktop (1440px)', async ({ page }) => {
|
|
||||||
await page.setViewportSize({ width: 1440, height: 900 });
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const mapContainer = page.locator('[data-testid="map-container"]');
|
|
||||||
await expect(mapContainer).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('US-65-69: Localization', () => {
|
|
||||||
test('should work in Russian locale (ru-ru)', async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const mapContainer = page.locator('[data-testid="map-container"]');
|
|
||||||
await expect(mapContainer).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should work in English locale (en-us)', async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/en-us/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const mapContainer = page.locator('[data-testid="map-container"]');
|
|
||||||
await expect(mapContainer).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('US-65-69: Accessibility', () => {
|
|
||||||
test('should render without accessibility violations', async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// Check for proper heading hierarchy
|
|
||||||
const heading = page.locator('h1');
|
|
||||||
await expect(heading).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should be keyboard navigable', async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// Tab through interactive elements
|
|
||||||
await page.keyboard.press('Tab');
|
|
||||||
let focusedElement = await page.evaluate(() => document.activeElement?.tagName);
|
|
||||||
expect(['INPUT', 'BUTTON', 'A', 'SELECT']).toContain(focusedElement);
|
|
||||||
|
|
||||||
await page.keyboard.press('Tab');
|
|
||||||
focusedElement = await page.evaluate(() => document.activeElement?.tagName);
|
|
||||||
expect(['INPUT', 'BUTTON', 'A', 'SELECT']).toContain(focusedElement);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should have proper labels', async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const departureInput = page.locator('[data-testid="map-departure-input"]');
|
|
||||||
const hasLabel = await departureInput.evaluate(
|
|
||||||
(el: HTMLInputElement) =>
|
|
||||||
el.placeholder || el.getAttribute('aria-label') || el.parentElement?.textContent,
|
|
||||||
);
|
|
||||||
expect(hasLabel).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('US-70: Zoom Functionality', () => {
|
|
||||||
test('should display zoom controls on map', async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const zoomControl = page.locator('[data-testid="zoom-control"]');
|
|
||||||
await expect(zoomControl).toBeVisible();
|
|
||||||
|
|
||||||
const zoomInBtn = page.locator('[data-testid="zoom-in-button"]');
|
|
||||||
const zoomOutBtn = page.locator('[data-testid="zoom-out-button"]');
|
|
||||||
await expect(zoomInBtn).toBeVisible();
|
|
||||||
await expect(zoomOutBtn).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should allow zooming in and out', async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const zoomLevel = page.locator('[data-testid="zoom-level-display"]');
|
|
||||||
const initialZoom = await zoomLevel.textContent();
|
|
||||||
|
|
||||||
const zoomInBtn = page.locator('[data-testid="zoom-in-button"]');
|
|
||||||
await zoomInBtn.click();
|
|
||||||
await page.waitForTimeout(200);
|
|
||||||
|
|
||||||
const newZoom = await zoomLevel.textContent();
|
|
||||||
expect(parseInt(newZoom!)).toBeGreaterThan(parseInt(initialZoom!));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should enforce minimum zoom level (3)', async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const zoomOutBtn = page.locator('[data-testid="zoom-out-button"]');
|
|
||||||
|
|
||||||
// Keep clicking zoom out until disabled
|
|
||||||
for (let i = 0; i < 5; i++) {
|
|
||||||
if (await zoomOutBtn.isDisabled()) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
await zoomOutBtn.click();
|
|
||||||
await page.waitForTimeout(100);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Button should be disabled at minimum zoom
|
|
||||||
await expect(zoomOutBtn).toBeDisabled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should enforce maximum zoom level (6)', async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const zoomInBtn = page.locator('[data-testid="zoom-in-button"]');
|
|
||||||
|
|
||||||
// Keep clicking zoom in until disabled
|
|
||||||
for (let i = 0; i < 5; i++) {
|
|
||||||
if (await zoomInBtn.isDisabled()) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
await zoomInBtn.click();
|
|
||||||
await page.waitForTimeout(100);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Button should be disabled at maximum zoom
|
|
||||||
await expect(zoomInBtn).toBeDisabled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('US-71: Domestic Flights Filter', () => {
|
|
||||||
test('should display domestic filter toggle', async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const domesticToggle = page.locator('[data-testid="toggle-domestic"]');
|
|
||||||
await expect(domesticToggle).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should toggle domestic flights filter', async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const domesticToggle = page.locator('[data-testid="toggle-domestic"]');
|
|
||||||
const isCheckedBefore = await domesticToggle.isChecked();
|
|
||||||
|
|
||||||
await domesticToggle.click();
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
|
|
||||||
const isCheckedAfter = await domesticToggle.isChecked();
|
|
||||||
expect(isCheckedAfter).toBe(!isCheckedBefore);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('US-72: International Flights Filter', () => {
|
|
||||||
test('should display international filter toggle', async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const internationalToggle = page.locator('[data-testid="toggle-international"]');
|
|
||||||
await expect(internationalToggle).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should toggle international flights filter', async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const internationalToggle = page.locator('[data-testid="toggle-international"]');
|
|
||||||
const isCheckedBefore = await internationalToggle.isChecked();
|
|
||||||
|
|
||||||
await internationalToggle.click();
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
|
|
||||||
const isCheckedAfter = await internationalToggle.isChecked();
|
|
||||||
expect(isCheckedAfter).toBe(!isCheckedBefore);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('US-73: Connecting Flights Filter', () => {
|
|
||||||
test('should display connecting filter toggle', async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const connectingToggle = page.locator('[data-testid="toggle-connecting"]');
|
|
||||||
await expect(connectingToggle).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should toggle connecting flights filter', async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const connectingToggle = page.locator('[data-testid="toggle-connecting"]');
|
|
||||||
const isCheckedBefore = await connectingToggle.isChecked();
|
|
||||||
|
|
||||||
await connectingToggle.click();
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
|
|
||||||
const isCheckedAfter = await connectingToggle.isChecked();
|
|
||||||
expect(isCheckedAfter).toBe(!isCheckedBefore);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('US-74: Panning', () => {
|
|
||||||
test('should allow map panning with mouse drag', async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const mapContainer = page.locator('[data-testid="map-container"]');
|
|
||||||
const box = await mapContainer.boundingBox();
|
|
||||||
|
|
||||||
if (box) {
|
|
||||||
// Drag from center right to center left
|
|
||||||
await page.mouse.move(box.x + box.width * 0.7, box.y + box.height * 0.5);
|
|
||||||
await page.mouse.down();
|
|
||||||
await page.mouse.move(box.x + box.width * 0.3, box.y + box.height * 0.5);
|
|
||||||
await page.mouse.up();
|
|
||||||
await page.waitForTimeout(200);
|
|
||||||
|
|
||||||
// Map should still be visible (not broken by pan)
|
|
||||||
await expect(mapContainer).toBeVisible();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should prevent panning beyond world bounds', async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const mapContainer = page.locator('[data-testid="map-container"]');
|
|
||||||
await expect(mapContainer).toBeVisible();
|
|
||||||
|
|
||||||
// Map should maintain bounds
|
|
||||||
const pageContent = await page.content();
|
|
||||||
expect(pageContent).toContain('map-container');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('US-75: Route Popup on Selection', () => {
|
|
||||||
test('should display route popup when route is clicked', async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// Search for a route first
|
|
||||||
const departureInput = page.locator('[data-testid="map-departure-input"]');
|
|
||||||
if (await departureInput.isVisible()) {
|
|
||||||
await departureInput.fill('Moscow');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
const suggestions = page.locator('[data-testid="city-suggestion"]');
|
|
||||||
if ((await suggestions.count()) > 0) {
|
|
||||||
await suggestions.first().click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// Check if popup appears after searching
|
|
||||||
const popup = page.locator('[data-testid="route-popup"]');
|
|
||||||
// Popup may or may not be visible depending on implementation
|
|
||||||
// Just verify the map is still functional
|
|
||||||
const mapContainer = page.locator('[data-testid="map-container"]');
|
|
||||||
await expect(mapContainer).toBeVisible();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display route details in popup', async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const mapContainer = page.locator('[data-testid="map-container"]');
|
|
||||||
await expect(mapContainer).toBeVisible();
|
|
||||||
|
|
||||||
// Popup structure should exist in DOM
|
|
||||||
const popup = page.locator('[data-testid="route-popup"]');
|
|
||||||
// Verify popup structure exists
|
|
||||||
if (await popup.isVisible()) {
|
|
||||||
const departure = popup.locator('[data-testid="popup-departure"]');
|
|
||||||
const arrival = popup.locator('[data-testid="popup-arrival"]');
|
|
||||||
await expect(departure).toBeTruthy();
|
|
||||||
await expect(arrival).toBeTruthy();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should close popup with close button', async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const popup = page.locator('[data-testid="route-popup"]');
|
|
||||||
const closeButton = page.locator('[data-testid="popup-close-button"]');
|
|
||||||
|
|
||||||
// If popup is visible, close button should work
|
|
||||||
if (await popup.isVisible()) {
|
|
||||||
await closeButton.click();
|
|
||||||
await page.waitForTimeout(200);
|
|
||||||
|
|
||||||
// Popup should be hidden
|
|
||||||
const isHidden = await popup.evaluate(
|
|
||||||
(el) => window.getComputedStyle(el).display === 'none',
|
|
||||||
);
|
|
||||||
expect(isHidden || !(await popup.isVisible())).toBeTruthy();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should close popup on ESC key', async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const popup = page.locator('[data-testid="route-popup"]');
|
|
||||||
|
|
||||||
if (await popup.isVisible()) {
|
|
||||||
await page.keyboard.press('Escape');
|
|
||||||
await page.waitForTimeout(200);
|
|
||||||
|
|
||||||
// Popup should be hidden
|
|
||||||
const isHidden = await popup.evaluate(
|
|
||||||
(el) => window.getComputedStyle(el).display === 'none',
|
|
||||||
);
|
|
||||||
expect(isHidden || !(await popup.isVisible())).toBeTruthy();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display flight count in popup', async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const popup = page.locator('[data-testid="route-popup"]');
|
|
||||||
if (await popup.isVisible()) {
|
|
||||||
const flightCount = popup.locator('[data-testid="popup-flight-count"]');
|
|
||||||
const text = await flightCount.textContent();
|
|
||||||
// Should contain a number
|
|
||||||
expect(text).toMatch(/\d+/);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should maintain popup position on map', async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const popup = page.locator('[data-testid="route-popup"]');
|
|
||||||
const mapContainer = page.locator('[data-testid="map-container"]');
|
|
||||||
|
|
||||||
if (await popup.isVisible()) {
|
|
||||||
const popupBox = await popup.boundingBox();
|
|
||||||
const mapBox = await mapContainer.boundingBox();
|
|
||||||
|
|
||||||
if (popupBox && mapBox) {
|
|
||||||
// Popup should be within or near map container
|
|
||||||
expect(popupBox.x).toBeGreaterThanOrEqual(mapBox.x - 100);
|
|
||||||
expect(popupBox.y).toBeGreaterThanOrEqual(mapBox.y - 100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('US-70-75: Integration Tests', () => {
|
|
||||||
test('should maintain zoom level when filters change', async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const zoomLevel = page.locator('[data-testid="zoom-level-display"]');
|
|
||||||
const initialZoom = await zoomLevel.textContent();
|
|
||||||
|
|
||||||
const domesticToggle = page.locator('[data-testid="toggle-domestic"]');
|
|
||||||
await domesticToggle.click();
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
|
|
||||||
const zoomAfterFilter = await zoomLevel.textContent();
|
|
||||||
expect(zoomAfterFilter).toBe(initialZoom);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should work across locales (ru-ru and en-us)', async ({ page }) => {
|
|
||||||
// Test ru-ru locale
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
let zoomControl = page.locator('[data-testid="zoom-control"]');
|
|
||||||
await expect(zoomControl).toBeVisible();
|
|
||||||
|
|
||||||
// Test en-us locale
|
|
||||||
await page.goto(`${BASE_URL}/en-us/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
zoomControl = page.locator('[data-testid="zoom-control"]');
|
|
||||||
await expect(zoomControl).toBeVisible();
|
|
||||||
|
|
||||||
const filters = page.locator('[data-testid="filter-toggles"]');
|
|
||||||
await expect(filters).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should render without console errors', async ({ page }) => {
|
|
||||||
const errors: string[] = [];
|
|
||||||
page.on('console', (msg) => {
|
|
||||||
if (msg.type() === 'error') {
|
|
||||||
errors.push(msg.text());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// Perform zoom action
|
|
||||||
const zoomInBtn = page.locator('[data-testid="zoom-in-button"]');
|
|
||||||
await zoomInBtn.click();
|
|
||||||
|
|
||||||
// Toggle a filter
|
|
||||||
const domesticToggle = page.locator('[data-testid="toggle-domestic"]');
|
|
||||||
await domesticToggle.click();
|
|
||||||
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
|
|
||||||
const filteredErrors = errors.filter((e) => !e.includes('sourcemap'));
|
|
||||||
expect(filteredErrors).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should be responsive on mobile', async ({ page }) => {
|
|
||||||
await page.setViewportSize({ width: 375, height: 667 });
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const zoomControl = page.locator('[data-testid="zoom-control"]');
|
|
||||||
const filters = page.locator('[data-testid="filter-toggles"]');
|
|
||||||
|
|
||||||
await expect(zoomControl).toBeVisible();
|
|
||||||
await expect(filters).toBeVisible();
|
|
||||||
|
|
||||||
// Verify zoom buttons work on mobile
|
|
||||||
const zoomInBtn = page.locator('[data-testid="zoom-in-button"]');
|
|
||||||
await zoomInBtn.click();
|
|
||||||
|
|
||||||
// Verify filters work on mobile
|
|
||||||
const domesticToggle = page.locator('[data-testid="toggle-domestic"]');
|
|
||||||
await domesticToggle.click();
|
|
||||||
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,908 +0,0 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
|
||||||
import type { Page } from '@playwright/test';
|
|
||||||
import {
|
|
||||||
buildOnlineBoardPath,
|
|
||||||
buildRouteParam,
|
|
||||||
searchFlightByRoute,
|
|
||||||
verifyFlightCard,
|
|
||||||
generateFlight,
|
|
||||||
generateFlights,
|
|
||||||
getToday,
|
|
||||||
getTomorrow,
|
|
||||||
getYesterday,
|
|
||||||
getFutureDate,
|
|
||||||
getPastDate,
|
|
||||||
CITIES,
|
|
||||||
FIXTURES,
|
|
||||||
} from '../support/test-utilities';
|
|
||||||
|
|
||||||
const today = getToday();
|
|
||||||
const tomorrow = getTomorrow();
|
|
||||||
const yesterday = getYesterday();
|
|
||||||
const futureDate = getFutureDate(7);
|
|
||||||
const pastDate = getPastDate(7);
|
|
||||||
const dateParam = buildRouteParam('MOW', today);
|
|
||||||
const tomorrowParam = buildRouteParam('MOW', tomorrow);
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Online Board - Route Tests (30+ tests)
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
test.describe('Online Board - Route', () => {
|
|
||||||
test.describe('Category 1: Basic Route Search', () => {
|
|
||||||
test('Should search by departure and arrival city (manual input) (Test 1)', async ({
|
|
||||||
page,
|
|
||||||
}) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByRoute(page, 'Moscow', 'Sochi');
|
|
||||||
|
|
||||||
const searchResults = page.locator('[data-testid="flight-card"]');
|
|
||||||
await expect(searchResults).toHaveCount(20);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search by cities from autocomplete list (Test 2)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'LED', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByRoute(page, 'Saint Petersburg', 'Moscow');
|
|
||||||
|
|
||||||
const searchResults = page.locator('[data-testid="flight-card"]');
|
|
||||||
await expect(searchResults).toHaveCount(20);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search with today date (Test 3)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'AER', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByRoute(page, 'Sochi', 'Moscow');
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/route\/AER-MOW-\d{8}/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search with future date (Test 4)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', futureDate)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByRoute(page, 'Moscow', 'Sochi');
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/route\/MOW-AER-\d{8}/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search with past date and show validation (Test 5)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', pastDate)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByRoute(page, 'Moscow', 'Sochi');
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/route\/MOW-AER-\d{8}/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search without date and use today (Test 6)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/departure/MOW`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByRoute(page, 'Moscow', 'Sochi');
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/route\/MOW-AER-\d{8}/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search with invalid cities and show error (Test 7)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'XXX', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByRoute(page, 'Invalid City', 'Another Invalid City');
|
|
||||||
|
|
||||||
const errorState = page.locator('[data-testid="error-state"]');
|
|
||||||
await expect(errorState).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search with same departure and arrival and show validation (Test 8)', async ({
|
|
||||||
page,
|
|
||||||
}) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const routeTab = page.locator('[data-testid="route-search-tab"]');
|
|
||||||
await routeTab.click();
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
const departureInput = page.locator('[data-testid="departure-city-input"]');
|
|
||||||
const arrivalInput = page.locator('[data-testid="arrival-city-input"]');
|
|
||||||
|
|
||||||
await departureInput.fill('Moscow');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await departureInput.press('Enter');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
await arrivalInput.fill('Moscow');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await arrivalInput.press('Enter');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const validationError = page.locator('[data-testid="validation-error"]');
|
|
||||||
await expect(validationError).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Category 2: Date Selection', () => {
|
|
||||||
test('Should select date from calendar (Test 9)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const calendarInput = page.locator('[data-testid="calendar-input"]');
|
|
||||||
await expect(calendarInput).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should select date range (Test 10)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const dateRange = page.locator('[data-testid="date-range"]');
|
|
||||||
await expect(dateRange).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should verify date format DD.MM.YYYY (Test 11)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const dateText = page.locator('[data-testid="board-date"]');
|
|
||||||
await expect(dateText).toBeVisible();
|
|
||||||
|
|
||||||
const dateValue = await dateText.textContent();
|
|
||||||
expect(dateValue).toMatch(/\d{2}\.\d{2}\.\d{4}/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should verify date validation min/max dates (Test 12)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const calendarInput = page.locator('[data-testid="calendar-input"]');
|
|
||||||
await expect(calendarInput).toBeEnabled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should select today date (Test 13)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const todayTab = page.locator(`[data-testid="date-tab-${today.replace(/-/g, '')}"]`);
|
|
||||||
await expect(todayTab).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should select tomorrow date (Test 14)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', tomorrow)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const tomorrowTab = page.locator(`[data-testid="date-tab-${tomorrow.replace(/-/g, '')}"]`);
|
|
||||||
await expect(tomorrowTab).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Category 3: Flight Results', () => {
|
|
||||||
test('Should verify flight results display (Test 15)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByRoute(page, 'Moscow', 'Sochi');
|
|
||||||
|
|
||||||
const flightCards = page.locator('[data-testid="flight-card"]');
|
|
||||||
await expect(flightCards).toHaveCount(20);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should verify flight count (Test 16)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByRoute(page, 'Moscow', 'Sochi');
|
|
||||||
|
|
||||||
const flightCount = page.locator('[data-testid="flight-count"]');
|
|
||||||
await expect(flightCount).toBeVisible();
|
|
||||||
await expect(flightCount).toContainText('20');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should verify flight details in results (Test 17)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByRoute(page, 'Moscow', 'Sochi');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toBeVisible();
|
|
||||||
|
|
||||||
const flightNumber = flightCard.locator('[data-testid="flight-number"]');
|
|
||||||
await expect(flightNumber).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should verify empty results message (Test 18)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByRoute(page, 'Moscow', 'Unknown City');
|
|
||||||
|
|
||||||
const noResults = page.locator('[data-testid="no-results"]');
|
|
||||||
await expect(noResults).toBeVisible();
|
|
||||||
await expect(noResults).toContainText('Нет результатов');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should verify loading state (Test 19)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByRoute(page, 'Moscow', 'Sochi');
|
|
||||||
|
|
||||||
const loadingSpinner = page.locator('[data-testid="loading-spinner"]');
|
|
||||||
await expect(loadingSpinner).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should verify error state (Test 20)', async ({ page }) => {
|
|
||||||
await page.route('**/api/flights/**', (route) => {
|
|
||||||
return route.abort('internetdisconnected');
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const networkError = page.locator('[data-testid="network-error"]');
|
|
||||||
await expect(networkError).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Category 4: Flight Details', () => {
|
|
||||||
test('Should open flight details from results (Test 21)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByRoute(page, 'Moscow', 'Sochi');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await flightCard.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/\/ru-ru\/[A-Z]{2}\s?\d+-\d{8}/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should verify flight details content (Test 22)', async ({ page }) => {
|
|
||||||
const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByRoute(page, 'Moscow', 'Sochi');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await flightCard.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(page.getByText(flight.flightNumber)).toBeVisible();
|
|
||||||
await expect(page.getByText(flight.airlineName)).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should verify flight route details (Test 23)', async ({ page }) => {
|
|
||||||
const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByRoute(page, 'Moscow', 'Sochi');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await flightCard.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(page.getByText(flight.departure.cityName)).toBeVisible();
|
|
||||||
await expect(page.getByText(flight.arrival.cityName)).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should verify flight status details (Test 24)', async ({ page }) => {
|
|
||||||
const flight = generateFlight({
|
|
||||||
direction: 'departure',
|
|
||||||
cityCode: 'MOW',
|
|
||||||
status: 'scheduled',
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByRoute(page, 'Moscow', 'Sochi');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await flightCard.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(page.getByText(flight.status)).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should close flight details (Test 25)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByRoute(page, 'Moscow', 'Sochi');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await flightCard.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const closeBtn = page.locator('[data-testid="close-details-btn"]');
|
|
||||||
await closeBtn.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/departure\/MOW-\d{8}/);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Category 5: Edge Cases', () => {
|
|
||||||
test('Should search for non-existent cities (Test 26)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/departure/XXX-${today.replace(/-/g, '')}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByRoute(page, 'NonExistent City', 'Another Fake City');
|
|
||||||
|
|
||||||
const errorState = page.locator('[data-testid="error-state"]');
|
|
||||||
await expect(errorState).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search with special characters (Test 27)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const routeTab = page.locator('[data-testid="route-search-tab"]');
|
|
||||||
await routeTab.click();
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
const departureInput = page.locator('[data-testid="departure-city-input"]');
|
|
||||||
const arrivalInput = page.locator('[data-testid="arrival-city-input"]');
|
|
||||||
|
|
||||||
await departureInput.fill('Moscow!@#$');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await departureInput.press('Enter');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
await arrivalInput.fill('Sochi!@#$');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await arrivalInput.press('Enter');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const noResults = page.locator('[data-testid="no-results"]');
|
|
||||||
await expect(noResults).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search with very long city names (Test 28)', async ({ page }) => {
|
|
||||||
const longCityName = 'Москва'.repeat(10);
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const routeTab = page.locator('[data-testid="route-search-tab"]');
|
|
||||||
await routeTab.click();
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
const departureInput = page.locator('[data-testid="departure-city-input"]');
|
|
||||||
const arrivalInput = page.locator('[data-testid="arrival-city-input"]');
|
|
||||||
|
|
||||||
await departureInput.fill(longCityName);
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await departureInput.press('Enter');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
await arrivalInput.fill(longCityName);
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await arrivalInput.press('Enter');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCards = page.locator('[data-testid="flight-card"]');
|
|
||||||
await expect(flightCards).toHaveCount(20);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search with Unicode characters (Test 29)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const routeTab = page.locator('[data-testid="route-search-tab"]');
|
|
||||||
await routeTab.click();
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
const departureInput = page.locator('[data-testid="departure-city-input"]');
|
|
||||||
const arrivalInput = page.locator('[data-testid="arrival-city-input"]');
|
|
||||||
|
|
||||||
await departureInput.fill('Moscow 🛫');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await departureInput.press('Enter');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
await arrivalInput.fill('Sochi 🛬');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await arrivalInput.press('Enter');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCards = page.locator('[data-testid="flight-card"]');
|
|
||||||
await expect(flightCards).toHaveCount(20);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should handle rapid search attempts (Test 30)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const routeTab = page.locator('[data-testid="route-search-tab"]');
|
|
||||||
await routeTab.click();
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
const departureInput = page.locator('[data-testid="departure-city-input"]');
|
|
||||||
const arrivalInput = page.locator('[data-testid="arrival-city-input"]');
|
|
||||||
|
|
||||||
for (let i = 0; i < 5; i++) {
|
|
||||||
await departureInput.fill('Moscow');
|
|
||||||
await page.waitForTimeout(200);
|
|
||||||
await departureInput.press('Enter');
|
|
||||||
await page.waitForTimeout(200);
|
|
||||||
|
|
||||||
await arrivalInput.fill('Sochi');
|
|
||||||
await page.waitForTimeout(200);
|
|
||||||
await arrivalInput.press('Enter');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
}
|
|
||||||
|
|
||||||
const flightCards = page.locator('[data-testid="flight-card"]');
|
|
||||||
await expect(flightCards).toHaveCount(20);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Additional Route Tests', () => {
|
|
||||||
test('Should navigate to route board for different cities (Test 31)', async ({ page }) => {
|
|
||||||
const cities = ['MOW', 'LED', 'AER', 'OVB', 'KRR'];
|
|
||||||
|
|
||||||
for (const cityCode of cities) {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', cityCode, today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByRoute(
|
|
||||||
page,
|
|
||||||
CITIES.find((c) => c.code === cityCode)?.name || '',
|
|
||||||
'Sochi',
|
|
||||||
);
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(new RegExp(`route/${cityCode}-AER-\\d{8}`));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should display correct date in title (Test 32)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByRoute(page, 'Moscow', 'Sochi');
|
|
||||||
|
|
||||||
const dateText = page.locator('[data-testid="board-date"]');
|
|
||||||
await expect(dateText).toBeVisible();
|
|
||||||
await expect(dateText).toContainText(today);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should filter by status (Test 33)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByRoute(page, 'Moscow', 'Sochi');
|
|
||||||
|
|
||||||
const statusFilter = page.locator('[data-testid="status-filter"]');
|
|
||||||
await statusFilter.click();
|
|
||||||
|
|
||||||
const scheduledOption = page.locator('[data-testid="filter-option-scheduled"]');
|
|
||||||
await scheduledOption.click();
|
|
||||||
|
|
||||||
const filteredFlights = page.locator('[data-testid="flight-card"]');
|
|
||||||
await expect(filteredFlights).toHaveCount(20);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should filter by airline (Test 34)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByRoute(page, 'Moscow', 'Sochi');
|
|
||||||
|
|
||||||
const airlineFilter = page.locator('[data-testid="airline-filter"]');
|
|
||||||
await airlineFilter.click();
|
|
||||||
|
|
||||||
const aeroflotOption = page.locator('[data-testid="filter-option-SU"]');
|
|
||||||
await aeroflotOption.click();
|
|
||||||
|
|
||||||
const filteredFlights = page.locator('[data-testid="flight-card"]');
|
|
||||||
await expect(filteredFlights).toHaveCount(20);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should display flight number (Test 35)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByRoute(page, 'Moscow', 'Sochi');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toBeVisible();
|
|
||||||
|
|
||||||
const flightNumber = flightCard.locator('[data-testid="flight-number"]');
|
|
||||||
await expect(flightNumber).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should display airline name (Test 36)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByRoute(page, 'Moscow', 'Sochi');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toBeVisible();
|
|
||||||
|
|
||||||
const airlineName = flightCard.locator('[data-testid="airline-name"]');
|
|
||||||
await expect(airlineName).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should display departure and arrival cities (Test 37)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByRoute(page, 'Moscow', 'Sochi');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toBeVisible();
|
|
||||||
|
|
||||||
const departureCity = flightCard.locator('[data-testid="departure-city"]');
|
|
||||||
const arrivalCity = flightCard.locator('[data-testid="arrival-city"]');
|
|
||||||
await expect(departureCity).toBeVisible();
|
|
||||||
await expect(arrivalCity).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should display scheduled departure time (Test 38)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByRoute(page, 'Moscow', 'Sochi');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toBeVisible();
|
|
||||||
|
|
||||||
const depTime = flightCard.locator('[data-testid="scheduled-departure-time"]');
|
|
||||||
await expect(depTime).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should display scheduled arrival time (Test 39)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByRoute(page, 'Moscow', 'Sochi');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toBeVisible();
|
|
||||||
|
|
||||||
const arrTime = flightCard.locator('[data-testid="scheduled-arrival-time"]');
|
|
||||||
await expect(arrTime).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should display flight duration (Test 40)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByRoute(page, 'Moscow', 'Sochi');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toBeVisible();
|
|
||||||
|
|
||||||
const duration = flightCard.locator('[data-testid="flight-duration"]');
|
|
||||||
await expect(duration).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should display actual departure time when available (Test 41)', async ({ page }) => {
|
|
||||||
const flight = generateFlight({
|
|
||||||
direction: 'departure',
|
|
||||||
cityCode: 'MOW',
|
|
||||||
status: 'departed',
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByRoute(page, 'Moscow', 'Sochi');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toBeVisible();
|
|
||||||
|
|
||||||
const actualTime = flightCard.locator('[data-testid="actual-departure-time"]');
|
|
||||||
await expect(actualTime).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should display delay information (Test 42)', async ({ page }) => {
|
|
||||||
const flight = generateFlight({
|
|
||||||
direction: 'departure',
|
|
||||||
cityCode: 'MOW',
|
|
||||||
status: 'delayed',
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByRoute(page, 'Moscow', 'Sochi');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toBeVisible();
|
|
||||||
|
|
||||||
const delayInfo = flightCard.locator('[data-testid="delay-info"]');
|
|
||||||
await expect(delayInfo).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should display terminal information (Test 43)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByRoute(page, 'Moscow', 'Sochi');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toBeVisible();
|
|
||||||
|
|
||||||
const terminal = flightCard.locator('[data-testid="terminal"]');
|
|
||||||
await expect(terminal).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should display gate information (Test 44)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByRoute(page, 'Moscow', 'Sochi');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toBeVisible();
|
|
||||||
|
|
||||||
const gate = flightCard.locator('[data-testid="gate"]');
|
|
||||||
await expect(gate).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should navigate to tomorrow date tab (Test 45)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByRoute(page, 'Moscow', 'Sochi');
|
|
||||||
|
|
||||||
const dateTab = page.locator(`[data-testid="date-tab-${tomorrow.replace(/-/g, '')}"]`);
|
|
||||||
if ((await dateTab.count()) > 0) {
|
|
||||||
await dateTab.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/departure\/MOW-\d{8}/);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should handle invalid city code (Test 46)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/departure/XXX-${today.replace(/-/g, '')}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const errorState = page.locator('[data-testid="error-state"]');
|
|
||||||
await expect(errorState).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should handle network error (Test 47)', async ({ page }) => {
|
|
||||||
await page.route('**/api/flights/**', (route) => {
|
|
||||||
return route.abort('internetdisconnected');
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const networkError = page.locator('[data-testid="network-error"]');
|
|
||||||
await expect(networkError).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should have proper ARIA labels (Test 48)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByRoute(page, 'Moscow', 'Sochi');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toHaveAttribute('role', 'article');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should be keyboard navigable (Test 49)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByRoute(page, 'Moscow', 'Sochi');
|
|
||||||
|
|
||||||
await page.keyboard.press('Tab');
|
|
||||||
await page.keyboard.press('Tab');
|
|
||||||
await page.keyboard.press('Tab');
|
|
||||||
|
|
||||||
const focusedElement = page.locator(':focus');
|
|
||||||
await expect(focusedElement).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search by route with different date (Test 50)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', futureDate)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByRoute(page, 'Moscow', 'Sochi');
|
|
||||||
|
|
||||||
const flightCards = page.locator('[data-testid="flight-card"]');
|
|
||||||
await expect(flightCards).toHaveCount(20);
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/route\/MOW-AER-\d{8}/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search from Saint Petersburg to Moscow (Test 51)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'LED', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByRoute(page, 'Saint Petersburg', 'Moscow');
|
|
||||||
|
|
||||||
const flightCards = page.locator('[data-testid="flight-card"]');
|
|
||||||
await expect(flightCards).toHaveCount(20);
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/route\/LED-MOW-\d{8}/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search from Sochi to Moscow (Test 52)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'AER', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByRoute(page, 'Sochi', 'Moscow');
|
|
||||||
|
|
||||||
const flightCards = page.locator('[data-testid="flight-card"]');
|
|
||||||
await expect(flightCards).toHaveCount(20);
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/route\/AER-MOW-\d{8}/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search from Novosibirsk to Moscow (Test 53)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'OVB', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByRoute(page, 'Novosibirsk', 'Moscow');
|
|
||||||
|
|
||||||
const flightCards = page.locator('[data-testid="flight-card"]');
|
|
||||||
await expect(flightCards).toHaveCount(20);
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/route\/OVB-MOW-\d{8}/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search from Krasnodar to Sochi (Test 54)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'KRR', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByRoute(page, 'Krasnodar', 'Sochi');
|
|
||||||
|
|
||||||
const flightCards = page.locator('[data-testid="flight-card"]');
|
|
||||||
await expect(flightCards).toHaveCount(20);
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/route\/KRR-AER-\d{8}/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should verify route information display (Test 55)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByRoute(page, 'Moscow', 'Sochi');
|
|
||||||
|
|
||||||
const routeInfo = page.locator('[data-testid="route-info"]');
|
|
||||||
await expect(routeInfo).toBeVisible();
|
|
||||||
|
|
||||||
await expect(routeInfo).toContainText('Москва');
|
|
||||||
await expect(routeInfo).toContainText('Сочи');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search with empty departure city (Test 56)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const routeTab = page.locator('[data-testid="route-search-tab"]');
|
|
||||||
await routeTab.click();
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
const departureInput = page.locator('[data-testid="departure-city-input"]');
|
|
||||||
const arrivalInput = page.locator('[data-testid="arrival-city-input"]');
|
|
||||||
|
|
||||||
await departureInput.fill('');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
await arrivalInput.fill('Sochi');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await arrivalInput.press('Enter');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const validationError = page.locator('[data-testid="validation-error"]');
|
|
||||||
await expect(validationError).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search with empty arrival city (Test 57)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const routeTab = page.locator('[data-testid="route-search-tab"]');
|
|
||||||
await routeTab.click();
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
const departureInput = page.locator('[data-testid="departure-city-input"]');
|
|
||||||
const arrivalInput = page.locator('[data-testid="arrival-city-input"]');
|
|
||||||
|
|
||||||
await departureInput.fill('Moscow');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await departureInput.press('Enter');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
await arrivalInput.fill('');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
const validationError = page.locator('[data-testid="validation-error"]');
|
|
||||||
await expect(validationError).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search with special Unicode characters (Test 58)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const routeTab = page.locator('[data-testid="route-search-tab"]');
|
|
||||||
await routeTab.click();
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
const departureInput = page.locator('[data-testid="departure-city-input"]');
|
|
||||||
const arrivalInput = page.locator('[data-testid="arrival-city-input"]');
|
|
||||||
|
|
||||||
await departureInput.fill('Москва');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await departureInput.press('Enter');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
await arrivalInput.fill('Сочи');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await arrivalInput.press('Enter');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCards = page.locator('[data-testid="flight-card"]');
|
|
||||||
await expect(flightCards).toHaveCount(20);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search with mixed case city names (Test 59)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const routeTab = page.locator('[data-testid="route-search-tab"]');
|
|
||||||
await routeTab.click();
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
const departureInput = page.locator('[data-testid="departure-city-input"]');
|
|
||||||
const arrivalInput = page.locator('[data-testid="arrival-city-input"]');
|
|
||||||
|
|
||||||
await departureInput.fill('moscow');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await departureInput.press('Enter');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
await arrivalInput.fill('sochi');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await arrivalInput.press('Enter');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCards = page.locator('[data-testid="flight-card"]');
|
|
||||||
await expect(flightCards).toHaveCount(20);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search with leading/trailing spaces (Test 60)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const routeTab = page.locator('[data-testid="route-search-tab"]');
|
|
||||||
await routeTab.click();
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
const departureInput = page.locator('[data-testid="departure-city-input"]');
|
|
||||||
const arrivalInput = page.locator('[data-testid="arrival-city-input"]');
|
|
||||||
|
|
||||||
await departureInput.fill(' Moscow ');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await departureInput.press('Enter');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
await arrivalInput.fill(' Sochi ');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await arrivalInput.press('Enter');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCards = page.locator('[data-testid="flight-card"]');
|
|
||||||
await expect(flightCards).toHaveCount(20);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,743 +0,0 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
|
||||||
import type { Page } from '@playwright/test';
|
|
||||||
import {
|
|
||||||
buildFlightsMapPath,
|
|
||||||
CITIES,
|
|
||||||
getToday,
|
|
||||||
getTomorrow,
|
|
||||||
getFutureDate,
|
|
||||||
getPastDate,
|
|
||||||
} from '../support/test-utilities';
|
|
||||||
|
|
||||||
const today = getToday();
|
|
||||||
const tomorrow = getTomorrow();
|
|
||||||
const futureDate = getFutureDate(7);
|
|
||||||
const pastDate = getPastDate(7);
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Flights Map Tests (20+ tests)
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
test.describe('Flights Map', () => {
|
|
||||||
test.describe('Category 1: Basic Map Navigation (4 tests)', () => {
|
|
||||||
test('Should navigate to flights map page (Test 1)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/flights-map/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should verify map loads on flights map page (Test 2)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const mapContainer = page.locator('[data-testid="map-container"]');
|
|
||||||
await expect(mapContainer).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should verify map controls are visible (Test 3)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const zoomControl = page.locator('.leaflet-control-zoom');
|
|
||||||
await expect(zoomControl).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should verify page title (Test 4)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(page).toHaveTitle(/Flights Map/i);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Category 2: City Selection (6 tests)', () => {
|
|
||||||
test('Should select departure city on map (Test 5)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
|
|
||||||
await fromInput.fill('Moscow');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await fromInput.press('Enter');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(fromInput).toHaveValue('Moscow');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should select arrival city on map (Test 6)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const toInput = page.locator('[data-testid="flights-map-to-input"] input');
|
|
||||||
await toInput.fill('Sochi');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await toInput.press('Enter');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(toInput).toHaveValue('Sochi');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should verify city selection with autocomplete (Test 7)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
|
|
||||||
await fromInput.fill('Saint');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
const autocompleteOption = page.locator('[data-testid="autocomplete-option"]').first();
|
|
||||||
await autocompleteOption.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(fromInput).toContainText('Saint Petersburg');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should verify city input fields (Test 8)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
|
|
||||||
const toInput = page.locator('[data-testid="flights-map-to-input"] input');
|
|
||||||
|
|
||||||
await expect(fromInput).toBeVisible();
|
|
||||||
await expect(toInput).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should clear city selection (Test 9)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
|
|
||||||
await fromInput.fill('Moscow');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await fromInput.press('Enter');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const clearBtn = page.locator('[data-testid="flights-map-clear-btn"]');
|
|
||||||
await clearBtn.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(fromInput).toHaveValue('');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should select multiple cities for search (Test 10)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
|
|
||||||
const toInput = page.locator('[data-testid="flights-map-to-input"] input');
|
|
||||||
|
|
||||||
await fromInput.fill('Moscow');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await fromInput.press('Enter');
|
|
||||||
|
|
||||||
await toInput.fill('Sochi');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await toInput.press('Enter');
|
|
||||||
|
|
||||||
await expect(fromInput).toHaveValue('Moscow');
|
|
||||||
await expect(toInput).toHaveValue('Sochi');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Category 3: Flight Search (6 tests)', () => {
|
|
||||||
test('Should search flights from selected departure city (Test 11)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
|
|
||||||
await fromInput.fill('Moscow');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await fromInput.press('Enter');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const searchBtn = page.locator('[data-testid="flights-map-search-btn"]');
|
|
||||||
await searchBtn.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const resultsContainer = page.locator('[data-testid="flights-map-results"]');
|
|
||||||
await expect(resultsContainer).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search flights to selected arrival city (Test 12)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
|
|
||||||
const toInput = page.locator('[data-testid="flights-map-to-input"] input');
|
|
||||||
|
|
||||||
await fromInput.fill('Moscow');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await fromInput.press('Enter');
|
|
||||||
|
|
||||||
await toInput.fill('Sochi');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await toInput.press('Enter');
|
|
||||||
|
|
||||||
const searchBtn = page.locator('[data-testid="flights-map-search-btn"]');
|
|
||||||
await searchBtn.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const resultsContainer = page.locator('[data-testid="flights-map-results"]');
|
|
||||||
await expect(resultsContainer).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search flights with date selection (Test 13)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
|
|
||||||
const dateFromInput = page.locator('[data-testid="flights-map-date-from"]');
|
|
||||||
|
|
||||||
await fromInput.fill('Moscow');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await fromInput.press('Enter');
|
|
||||||
|
|
||||||
await dateFromInput.fill(today);
|
|
||||||
await dateFromInput.press('Enter');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const searchBtn = page.locator('[data-testid="flights-map-search-btn"]');
|
|
||||||
await searchBtn.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const resultsContainer = page.locator('[data-testid="flights-map-results"]');
|
|
||||||
await expect(resultsContainer).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search flights with date range (Test 14)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
|
|
||||||
const dateFromInput = page.locator('[data-testid="flights-map-date-from"]');
|
|
||||||
const dateToInput = page.locator('[data-testid="flights-map-date-to"]');
|
|
||||||
|
|
||||||
await fromInput.fill('Moscow');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await fromInput.press('Enter');
|
|
||||||
|
|
||||||
await dateFromInput.fill(today);
|
|
||||||
await dateFromInput.press('Enter');
|
|
||||||
|
|
||||||
await dateToInput.fill(futureDate);
|
|
||||||
await dateToInput.press('Enter');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const searchBtn = page.locator('[data-testid="flights-map-search-btn"]');
|
|
||||||
await searchBtn.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const resultsContainer = page.locator('[data-testid="flights-map-results"]');
|
|
||||||
await expect(resultsContainer).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search flights with filters (Test 15)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
|
|
||||||
const connectionsSelect = page.locator('[data-testid="flights-map-connections-select"]');
|
|
||||||
|
|
||||||
await fromInput.fill('Moscow');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await fromInput.press('Enter');
|
|
||||||
|
|
||||||
await connectionsSelect.selectOption('0');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const searchBtn = page.locator('[data-testid="flights-map-search-btn"]');
|
|
||||||
await searchBtn.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const resultsContainer = page.locator('[data-testid="flights-map-results"]');
|
|
||||||
await expect(resultsContainer).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search flights with invalid selection and show error (Test 16)', async ({
|
|
||||||
page,
|
|
||||||
}) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
|
|
||||||
await fromInput.fill('Invalid City XXX');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await fromInput.press('Enter');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const searchBtn = page.locator('[data-testid="flights-map-search-btn"]');
|
|
||||||
await searchBtn.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const noResults = page.locator('[data-testid="no-results"]');
|
|
||||||
await expect(noResults).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Category 4: Flight Results (4 tests)', () => {
|
|
||||||
test('Should verify flight results display (Test 17)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
|
|
||||||
await fromInput.fill('Moscow');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await fromInput.press('Enter');
|
|
||||||
|
|
||||||
const searchBtn = page.locator('[data-testid="flights-map-search-btn"]');
|
|
||||||
await searchBtn.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const resultsContainer = page.locator('[data-testid="flights-map-results"]');
|
|
||||||
await expect(resultsContainer).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should verify flight count in results (Test 18)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
|
|
||||||
await fromInput.fill('Moscow');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await fromInput.press('Enter');
|
|
||||||
|
|
||||||
const searchBtn = page.locator('[data-testid="flights-map-search-btn"]');
|
|
||||||
await searchBtn.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCount = page.locator('[data-testid="flight-count"]');
|
|
||||||
await expect(flightCount).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should verify flight details in results (Test 19)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
|
|
||||||
await fromInput.fill('Moscow');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await fromInput.press('Enter');
|
|
||||||
|
|
||||||
const searchBtn = page.locator('[data-testid="flights-map-search-btn"]');
|
|
||||||
await searchBtn.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const firstResult = page.locator('[data-testid="flight-result"]').first();
|
|
||||||
await expect(firstResult).toBeVisible();
|
|
||||||
|
|
||||||
const flightNumber = firstResult.locator('[data-testid="flight-number"]');
|
|
||||||
await expect(flightNumber).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should verify empty results message (Test 20)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
|
|
||||||
await fromInput.fill('NonExistent City');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await fromInput.press('Enter');
|
|
||||||
|
|
||||||
const searchBtn = page.locator('[data-testid="flights-map-search-btn"]');
|
|
||||||
await searchBtn.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const noResults = page.locator('[data-testid="no-results"]');
|
|
||||||
await expect(noResults).toBeVisible();
|
|
||||||
await expect(noResults).toContainText('No results');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Category 5: Edge Cases (2 tests)', () => {
|
|
||||||
test('Should handle search with no cities selected (Test 21)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const searchBtn = page.locator('[data-testid="flights-map-search-btn"]');
|
|
||||||
await searchBtn.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const validationError = page.locator('[data-testid="validation-error"]');
|
|
||||||
await expect(validationError).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should handle invalid city selection (Test 22)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
|
|
||||||
await fromInput.fill('XXX');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
const searchBtn = page.locator('[data-testid="flights-map-search-btn"]');
|
|
||||||
await searchBtn.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const noResults = page.locator('[data-testid="no-results"]');
|
|
||||||
await expect(noResults).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Category 6: Additional Flights Map Tests', () => {
|
|
||||||
test('Should navigate to flights map for different cities (Test 23)', async ({ page }) => {
|
|
||||||
const cities = ['MOW', 'LED', 'AER', 'OVB', 'KRR'];
|
|
||||||
|
|
||||||
for (const cityCode of cities) {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
|
|
||||||
await fromInput.fill(CITIES.find((c) => c.code === cityCode)?.name || '');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await fromInput.press('Enter');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const resultsContainer = page.locator('[data-testid="flights-map-results"]');
|
|
||||||
await expect(resultsContainer).toBeVisible();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should display correct date in results (Test 24)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
|
|
||||||
const dateFromInput = page.locator('[data-testid="flights-map-date-from"]');
|
|
||||||
|
|
||||||
await fromInput.fill('Moscow');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await fromInput.press('Enter');
|
|
||||||
|
|
||||||
await dateFromInput.fill(today);
|
|
||||||
await dateFromInput.press('Enter');
|
|
||||||
|
|
||||||
const searchBtn = page.locator('[data-testid="flights-map-search-btn"]');
|
|
||||||
await searchBtn.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const dateDisplay = page.locator('[data-testid="date-display"]');
|
|
||||||
await expect(dateDisplay).toBeVisible();
|
|
||||||
await expect(dateDisplay).toContainText(today);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should filter by connections (Test 25)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
|
|
||||||
const connectionsSelect = page.locator('[data-testid="flights-map-connections-select"]');
|
|
||||||
|
|
||||||
await fromInput.fill('Moscow');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await fromInput.press('Enter');
|
|
||||||
|
|
||||||
await connectionsSelect.selectOption('1');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const searchBtn = page.locator('[data-testid="flights-map-search-btn"]');
|
|
||||||
await searchBtn.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const resultsContainer = page.locator('[data-testid="flights-map-results"]');
|
|
||||||
await expect(resultsContainer).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should display flight status badges (Test 26)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
|
|
||||||
await fromInput.fill('Moscow');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await fromInput.press('Enter');
|
|
||||||
|
|
||||||
const searchBtn = page.locator('[data-testid="flights-map-search-btn"]');
|
|
||||||
await searchBtn.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const statusBadge = page.locator('[data-testid="status-badge"]').first();
|
|
||||||
await expect(statusBadge).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should display flight departure and arrival cities (Test 27)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
|
|
||||||
const toInput = page.locator('[data-testid="flights-map-to-input"] input');
|
|
||||||
|
|
||||||
await fromInput.fill('Moscow');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await fromInput.press('Enter');
|
|
||||||
|
|
||||||
await toInput.fill('Sochi');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await toInput.press('Enter');
|
|
||||||
|
|
||||||
const searchBtn = page.locator('[data-testid="flights-map-search-btn"]');
|
|
||||||
await searchBtn.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const firstResult = page.locator('[data-testid="flight-result"]').first();
|
|
||||||
await expect(firstResult).toBeVisible();
|
|
||||||
|
|
||||||
const depCity = firstResult.locator('[data-testid="departure-city"]');
|
|
||||||
const arrCity = firstResult.locator('[data-testid="arrival-city"]');
|
|
||||||
await expect(depCity).toBeVisible();
|
|
||||||
await expect(arrCity).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should display flight times (Test 28)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
|
|
||||||
await fromInput.fill('Moscow');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await fromInput.press('Enter');
|
|
||||||
|
|
||||||
const searchBtn = page.locator('[data-testid="flights-map-search-btn"]');
|
|
||||||
await searchBtn.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const firstResult = page.locator('[data-testid="flight-result"]').first();
|
|
||||||
await expect(firstResult).toBeVisible();
|
|
||||||
|
|
||||||
const depTime = firstResult.locator('[data-testid="departure-time"]');
|
|
||||||
const arrTime = firstResult.locator('[data-testid="arrival-time"]');
|
|
||||||
await expect(depTime).toBeVisible();
|
|
||||||
await expect(arrTime).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should display flight airline information (Test 29)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
|
|
||||||
await fromInput.fill('Moscow');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await fromInput.press('Enter');
|
|
||||||
|
|
||||||
const searchBtn = page.locator('[data-testid="flights-map-search-btn"]');
|
|
||||||
await searchBtn.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const firstResult = page.locator('[data-testid="flight-result"]').first();
|
|
||||||
await expect(firstResult).toBeVisible();
|
|
||||||
|
|
||||||
const airlineName = firstResult.locator('[data-testid="airline-name"]');
|
|
||||||
await expect(airlineName).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should handle network error (Test 30)', async ({ page }) => {
|
|
||||||
await page.route('**/api/destinations**', (route) => {
|
|
||||||
return route.abort('internetdisconnected');
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
|
|
||||||
await fromInput.fill('Moscow');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await fromInput.press('Enter');
|
|
||||||
|
|
||||||
const searchBtn = page.locator('[data-testid="flights-map-search-btn"]');
|
|
||||||
await searchBtn.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const networkError = page.locator('[data-testid="network-error"]');
|
|
||||||
await expect(networkError).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search with special characters (Test 31)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
|
|
||||||
await fromInput.fill('Moscow!@#$');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await fromInput.press('Enter');
|
|
||||||
|
|
||||||
const searchBtn = page.locator('[data-testid="flights-map-search-btn"]');
|
|
||||||
await searchBtn.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const noResults = page.locator('[data-testid="no-results"]');
|
|
||||||
await expect(noResults).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search with Unicode characters (Test 32)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
|
|
||||||
await fromInput.fill('Moscow 🛫');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await fromInput.press('Enter');
|
|
||||||
|
|
||||||
const searchBtn = page.locator('[data-testid="flights-map-search-btn"]');
|
|
||||||
await searchBtn.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const resultsContainer = page.locator('[data-testid="flights-map-results"]');
|
|
||||||
await expect(resultsContainer).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should handle rapid search attempts (Test 33)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
|
|
||||||
const searchBtn = page.locator('[data-testid="flights-map-search-btn"]');
|
|
||||||
|
|
||||||
for (let i = 0; i < 5; i++) {
|
|
||||||
await fromInput.fill('Moscow');
|
|
||||||
await page.waitForTimeout(200);
|
|
||||||
await searchBtn.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
}
|
|
||||||
|
|
||||||
const resultsContainer = page.locator('[data-testid="flights-map-results"]');
|
|
||||||
await expect(resultsContainer).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should verify map zoom controls (Test 34)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const zoomInBtn = page.locator('.leaflet-control-zoom-in');
|
|
||||||
const zoomOutBtn = page.locator('.leaflet-control-zoom-out');
|
|
||||||
|
|
||||||
await expect(zoomInBtn).toBeVisible();
|
|
||||||
await expect(zoomOutBtn).toBeVisible();
|
|
||||||
|
|
||||||
await zoomInBtn.click();
|
|
||||||
await page.waitForTimeout(200);
|
|
||||||
|
|
||||||
await zoomOutBtn.click();
|
|
||||||
await page.waitForTimeout(200);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should verify map center coordinates (Test 35)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const mapContainer = page.locator('[data-testid="map-container"]');
|
|
||||||
await expect(mapContainer).toBeVisible();
|
|
||||||
|
|
||||||
const markers = page.locator('[data-testid="flight-marker"]');
|
|
||||||
const markerCount = await markers.count();
|
|
||||||
expect(markerCount).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search with past date and show validation (Test 36)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
|
|
||||||
const dateFromInput = page.locator('[data-testid="flights-map-date-from"]');
|
|
||||||
|
|
||||||
await fromInput.fill('Moscow');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await fromInput.press('Enter');
|
|
||||||
|
|
||||||
await dateFromInput.fill(pastDate);
|
|
||||||
await dateFromInput.press('Enter');
|
|
||||||
|
|
||||||
const searchBtn = page.locator('[data-testid="flights-map-search-btn"]');
|
|
||||||
await searchBtn.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const resultsContainer = page.locator('[data-testid="flights-map-results"]');
|
|
||||||
await expect(resultsContainer).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search with future date (Test 37)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
|
|
||||||
const dateFromInput = page.locator('[data-testid="flights-map-date-from"]');
|
|
||||||
|
|
||||||
await fromInput.fill('Moscow');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await fromInput.press('Enter');
|
|
||||||
|
|
||||||
await dateFromInput.fill(futureDate);
|
|
||||||
await dateFromInput.press('Enter');
|
|
||||||
|
|
||||||
const searchBtn = page.locator('[data-testid="flights-map-search-btn"]');
|
|
||||||
await searchBtn.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const resultsContainer = page.locator('[data-testid="flights-map-results"]');
|
|
||||||
await expect(resultsContainer).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search from Saint Petersburg to Sochi (Test 38)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
|
|
||||||
const toInput = page.locator('[data-testid="flights-map-to-input"] input');
|
|
||||||
|
|
||||||
await fromInput.fill('Saint Petersburg');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await fromInput.press('Enter');
|
|
||||||
|
|
||||||
await toInput.fill('Sochi');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await toInput.press('Enter');
|
|
||||||
|
|
||||||
const searchBtn = page.locator('[data-testid="flights-map-search-btn"]');
|
|
||||||
await searchBtn.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const resultsContainer = page.locator('[data-testid="flights-map-results"]');
|
|
||||||
await expect(resultsContainer).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search from Novosibirsk to Moscow (Test 39)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
|
|
||||||
const toInput = page.locator('[data-testid="flights-map-to-input"] input');
|
|
||||||
|
|
||||||
await fromInput.fill('Novosibirsk');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await fromInput.press('Enter');
|
|
||||||
|
|
||||||
await toInput.fill('Moscow');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await toInput.press('Enter');
|
|
||||||
|
|
||||||
const searchBtn = page.locator('[data-testid="flights-map-search-btn"]');
|
|
||||||
await searchBtn.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const resultsContainer = page.locator('[data-testid="flights-map-results"]');
|
|
||||||
await expect(resultsContainer).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search with no arrival city (Test 40)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const fromInput = page.locator('[data-testid="flights-map-from-input"] input');
|
|
||||||
await fromInput.fill('Moscow');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
await fromInput.press('Enter');
|
|
||||||
|
|
||||||
const searchBtn = page.locator('[data-testid="flights-map-search-btn"]');
|
|
||||||
await searchBtn.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const resultsContainer = page.locator('[data-testid="flights-map-results"]');
|
|
||||||
await expect(resultsContainer).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,596 +0,0 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
|
||||||
import type { Page } from '@playwright/test';
|
|
||||||
import {
|
|
||||||
buildOnlineBoardPath,
|
|
||||||
buildRouteParam,
|
|
||||||
searchFlightByNumber,
|
|
||||||
searchFlightByRoute,
|
|
||||||
verifyFlightCard,
|
|
||||||
generateFlight,
|
|
||||||
generateFlights,
|
|
||||||
getToday,
|
|
||||||
getTomorrow,
|
|
||||||
getYesterday,
|
|
||||||
getFutureDate,
|
|
||||||
getPastDate,
|
|
||||||
CITIES,
|
|
||||||
FIXTURES,
|
|
||||||
} from '../support/test-utilities';
|
|
||||||
|
|
||||||
const today = getToday();
|
|
||||||
const tomorrow = getTomorrow();
|
|
||||||
const yesterday = getYesterday();
|
|
||||||
const futureDate = getFutureDate(7);
|
|
||||||
const pastDate = getPastDate(7);
|
|
||||||
const dateParam = buildRouteParam('MOW', today);
|
|
||||||
const tomorrowParam = buildRouteParam('MOW', tomorrow);
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Online Board - Arrival Tests (30+ tests)
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
test.describe('Online Board - Arrival', () => {
|
|
||||||
test.describe('Category 1: Basic Arrival Search', () => {
|
|
||||||
test('Should search by city name (manual input) (Test 1)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/arrival\/MOW-\d{8}/);
|
|
||||||
await expect(page).toHaveTitle(/Прибытие/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search by city from autocomplete list (Test 2)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'LED', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/arrival\/LED-\d{8}/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search with today date (Test 3)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'AER', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/arrival\/AER-\d{8}/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search with future date (Test 4)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', futureDate)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/arrival\/MOW-\d{8}/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search with past date and show validation (Test 5)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', pastDate)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/arrival\/MOW-\d{8}/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search without date and use today (Test 6)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/arrival/MOW`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/arrival\/MOW-\d{8}/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search with invalid city and show error (Test 7)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/arrival/XXX-${today.replace(/-/g, '')}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const errorState = page.locator('[data-testid="error-state"]');
|
|
||||||
await expect(errorState).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search with empty city and show validation (Test 8)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/arrival/-${today.replace(/-/g, '')}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const errorState = page.locator('[data-testid="error-state"]');
|
|
||||||
await expect(errorState).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Category 2: Date Selection', () => {
|
|
||||||
test('Should select date from calendar (Test 9)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const calendarInput = page.locator('[data-testid="calendar-input"]');
|
|
||||||
await expect(calendarInput).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should select date range (Test 10)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const dateRange = page.locator('[data-testid="date-range"]');
|
|
||||||
await expect(dateRange).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should verify date format DD.MM.YYYY (Test 11)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const dateText = page.locator('[data-testid="board-date"]');
|
|
||||||
await expect(dateText).toBeVisible();
|
|
||||||
|
|
||||||
const dateValue = await dateText.textContent();
|
|
||||||
expect(dateValue).toMatch(/\d{2}\.\d{2}\.\d{4}/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should verify date validation min/max dates (Test 12)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const calendarInput = page.locator('[data-testid="calendar-input"]');
|
|
||||||
await expect(calendarInput).toBeEnabled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should select today date (Test 13)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const todayTab = page.locator(`[data-testid="date-tab-${today.replace(/-/g, '')}"]`);
|
|
||||||
await expect(todayTab).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should select tomorrow date (Test 14)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', tomorrow)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const tomorrowTab = page.locator(`[data-testid="date-tab-${tomorrow.replace(/-/g, '')}"]`);
|
|
||||||
await expect(tomorrowTab).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Category 3: Flight Results', () => {
|
|
||||||
test('Should verify flight results display (Test 15)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCards = page.locator('[data-testid="flight-card"]');
|
|
||||||
await expect(flightCards).toHaveCount(20);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should verify flight count (Test 16)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCards = page.locator('[data-testid="flight-card"]');
|
|
||||||
await expect(flightCards).toHaveCount(20);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should verify flight details in results (Test 17)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toBeVisible();
|
|
||||||
|
|
||||||
const flightNumber = flightCard.locator('[data-testid="flight-number"]');
|
|
||||||
await expect(flightNumber).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should verify empty results message (Test 18)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByNumber(page, 'SU 9999');
|
|
||||||
|
|
||||||
const noResults = page.locator('[data-testid="no-results"]');
|
|
||||||
await expect(noResults).toBeVisible();
|
|
||||||
await expect(noResults).toContainText('Нет результатов');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should verify loading state (Test 19)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const loadingSpinner = page.locator('[data-testid="loading-spinner"]');
|
|
||||||
await expect(loadingSpinner).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should verify error state (Test 20)', async ({ page }) => {
|
|
||||||
await page.route('**/api/flights/**', (route) => {
|
|
||||||
return route.abort('internetdisconnected');
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const networkError = page.locator('[data-testid="network-error"]');
|
|
||||||
await expect(networkError).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Category 4: Flight Details', () => {
|
|
||||||
test('Should open flight details from results (Test 21)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await flightCard.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/\/ru-ru\/[A-Z]{2}\s?\d+-\d{8}/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should verify flight details content (Test 22)', async ({ page }) => {
|
|
||||||
const flight = generateFlight({ direction: 'arrival', cityCode: 'MOW' });
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByNumber(page, flight.flightNumber);
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await flightCard.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(page.getByText(flight.flightNumber)).toBeVisible();
|
|
||||||
await expect(page.getByText(flight.airlineName)).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should verify flight route details (Test 23)', async ({ page }) => {
|
|
||||||
const flight = generateFlight({ direction: 'arrival', cityCode: 'MOW' });
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByNumber(page, flight.flightNumber);
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await flightCard.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(page.getByText(flight.departure.cityName)).toBeVisible();
|
|
||||||
await expect(page.getByText(flight.arrival.cityName)).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should verify flight status details (Test 24)', async ({ page }) => {
|
|
||||||
const flight = generateFlight({
|
|
||||||
direction: 'arrival',
|
|
||||||
cityCode: 'MOW',
|
|
||||||
status: 'scheduled',
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByNumber(page, flight.flightNumber);
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await flightCard.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(page.getByText(flight.status)).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should close flight details (Test 25)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await flightCard.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const closeBtn = page.locator('[data-testid="close-details-btn"]');
|
|
||||||
await closeBtn.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/arrival\/MOW-\d{8}/);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Category 5: Edge Cases', () => {
|
|
||||||
test('Should search for non-existent city (Test 26)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/arrival/XXX-${today.replace(/-/g, '')}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const errorState = page.locator('[data-testid="error-state"]');
|
|
||||||
await expect(errorState).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search with special characters (Test 27)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const searchInput = page.locator('[data-testid="flight-search-input"]');
|
|
||||||
await searchInput.fill('SU 123!');
|
|
||||||
await searchInput.press('Enter');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const noResults = page.locator('[data-testid="no-results"]');
|
|
||||||
await expect(noResults).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search with very long city name (Test 28)', async ({ page }) => {
|
|
||||||
const longCityName = 'Москва'.repeat(10);
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const searchInput = page.locator('[data-testid="flight-search-input"]');
|
|
||||||
await searchInput.fill(longCityName);
|
|
||||||
await searchInput.press('Enter');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCards = page.locator('[data-testid="flight-card"]');
|
|
||||||
await expect(flightCards).toHaveCount(20);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search with Unicode characters (Test 29)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const searchInput = page.locator('[data-testid="flight-search-input"]');
|
|
||||||
await searchInput.fill('SU 1234 🛫');
|
|
||||||
await searchInput.press('Enter');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCards = page.locator('[data-testid="flight-card"]');
|
|
||||||
await expect(flightCards).toHaveCount(20);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should handle rapid search attempts (Test 30)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const searchInput = page.locator('[data-testid="flight-search-input"]');
|
|
||||||
|
|
||||||
for (let i = 0; i < 5; i++) {
|
|
||||||
await searchInput.fill(`SU ${1000 + i}`);
|
|
||||||
await searchInput.press('Enter');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
}
|
|
||||||
|
|
||||||
const flightCards = page.locator('[data-testid="flight-card"]');
|
|
||||||
await expect(flightCards).toHaveCount(20);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Additional Arrival Tests', () => {
|
|
||||||
test('Should navigate to arrival board for different cities (Test 31)', async ({ page }) => {
|
|
||||||
const cities = ['MOW', 'LED', 'AER', 'OVB', 'KRR'];
|
|
||||||
|
|
||||||
for (const cityCode of cities) {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', cityCode, today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(new RegExp(`arrival/${cityCode}-\\d{8}`));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should display correct date in title (Test 32)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const dateText = page.locator('[data-testid="board-date"]');
|
|
||||||
await expect(dateText).toBeVisible();
|
|
||||||
await expect(dateText).toContainText(today);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should filter by status (Test 33)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const statusFilter = page.locator('[data-testid="status-filter"]');
|
|
||||||
await statusFilter.click();
|
|
||||||
|
|
||||||
const scheduledOption = page.locator('[data-testid="filter-option-scheduled"]');
|
|
||||||
await scheduledOption.click();
|
|
||||||
|
|
||||||
const filteredFlights = page.locator('[data-testid="flight-card"]');
|
|
||||||
await expect(filteredFlights).toHaveCount(20);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should filter by airline (Test 34)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const airlineFilter = page.locator('[data-testid="airline-filter"]');
|
|
||||||
await airlineFilter.click();
|
|
||||||
|
|
||||||
const aeroflotOption = page.locator('[data-testid="filter-option-SU"]');
|
|
||||||
await aeroflotOption.click();
|
|
||||||
|
|
||||||
const filteredFlights = page.locator('[data-testid="flight-card"]');
|
|
||||||
await expect(filteredFlights).toHaveCount(20);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should display flight number (Test 35)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toBeVisible();
|
|
||||||
|
|
||||||
const flightNumber = flightCard.locator('[data-testid="flight-number"]');
|
|
||||||
await expect(flightNumber).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should display airline name (Test 36)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toBeVisible();
|
|
||||||
|
|
||||||
const airlineName = flightCard.locator('[data-testid="airline-name"]');
|
|
||||||
await expect(airlineName).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should display departure and arrival cities (Test 37)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toBeVisible();
|
|
||||||
|
|
||||||
const arrivalCity = flightCard.locator('[data-testid="arrival-city"]');
|
|
||||||
await expect(arrivalCity).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should display scheduled time (Test 38)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toBeVisible();
|
|
||||||
|
|
||||||
const scheduledTime = flightCard.locator('[data-testid="scheduled-time"]');
|
|
||||||
await expect(scheduledTime).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should display actual time when available (Test 39)', async ({ page }) => {
|
|
||||||
const flight = generateFlight({
|
|
||||||
direction: 'arrival',
|
|
||||||
cityCode: 'MOW',
|
|
||||||
status: 'arrived',
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toBeVisible();
|
|
||||||
|
|
||||||
const actualTime = flightCard.locator('[data-testid="actual-time"]');
|
|
||||||
await expect(actualTime).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should display delay information (Test 40)', async ({ page }) => {
|
|
||||||
const flight = generateFlight({
|
|
||||||
direction: 'arrival',
|
|
||||||
cityCode: 'MOW',
|
|
||||||
status: 'delayed',
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toBeVisible();
|
|
||||||
|
|
||||||
const delayInfo = flightCard.locator('[data-testid="delay-info"]');
|
|
||||||
await expect(delayInfo).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should display terminal information (Test 41)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toBeVisible();
|
|
||||||
|
|
||||||
const terminal = flightCard.locator('[data-testid="terminal"]');
|
|
||||||
await expect(terminal).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should display baggage belt information (Test 42)', async ({ page }) => {
|
|
||||||
const flight = generateFlight({
|
|
||||||
direction: 'arrival',
|
|
||||||
cityCode: 'MOW',
|
|
||||||
status: 'arrived',
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toBeVisible();
|
|
||||||
|
|
||||||
const baggageBelt = flightCard.locator('[data-testid="baggage-belt"]');
|
|
||||||
await expect(baggageBelt).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should navigate to tomorrow date tab (Test 43)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const dateTab = page.locator(`[data-testid="date-tab-${tomorrow.replace(/-/g, '')}"]`);
|
|
||||||
if ((await dateTab.count()) > 0) {
|
|
||||||
await dateTab.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/arrival\/MOW-\d{8}/);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should handle invalid city code (Test 44)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/arrival/XXX-${today.replace(/-/g, '')}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const errorState = page.locator('[data-testid="error-state"]');
|
|
||||||
await expect(errorState).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should handle network error (Test 45)', async ({ page }) => {
|
|
||||||
await page.route('**/api/flights/**', (route) => {
|
|
||||||
return route.abort('internetdisconnected');
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const networkError = page.locator('[data-testid="network-error"]');
|
|
||||||
await expect(networkError).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should have proper ARIA labels (Test 46)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toHaveAttribute('role', 'article');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should be keyboard navigable (Test 47)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await page.keyboard.press('Tab');
|
|
||||||
await page.keyboard.press('Tab');
|
|
||||||
await page.keyboard.press('Tab');
|
|
||||||
|
|
||||||
const focusedElement = page.locator(':focus');
|
|
||||||
await expect(focusedElement).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search by flight number (Test 48)', async ({ page }) => {
|
|
||||||
const flight = generateFlight({ direction: 'arrival', cityCode: 'MOW' });
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByNumber(page, flight.flightNumber);
|
|
||||||
|
|
||||||
const searchResults = page.locator('[data-testid="flight-card"]');
|
|
||||||
await expect(searchResults).toHaveCount(1);
|
|
||||||
|
|
||||||
await verifyFlightCard(page, flight);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should show no results when flight not found (Test 49)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByNumber(page, 'SU 9999');
|
|
||||||
|
|
||||||
const noResults = page.locator('[data-testid="no-results"]');
|
|
||||||
await expect(noResults).toBeVisible();
|
|
||||||
await expect(noResults).toContainText('Нет результатов');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search by route (Test 50)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByRoute(page, 'Moscow', 'Sochi');
|
|
||||||
|
|
||||||
const flightCards = page.locator('[data-testid="flight-card"]');
|
|
||||||
await expect(flightCards).toHaveCount(20);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,622 +0,0 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
|
||||||
import type { Page } from '@playwright/test';
|
|
||||||
import {
|
|
||||||
buildOnlineBoardPath,
|
|
||||||
buildRouteParam,
|
|
||||||
searchFlightByNumber,
|
|
||||||
searchFlightByRoute,
|
|
||||||
verifyFlightCard,
|
|
||||||
generateFlight,
|
|
||||||
generateFlights,
|
|
||||||
getToday,
|
|
||||||
getTomorrow,
|
|
||||||
getYesterday,
|
|
||||||
getFutureDate,
|
|
||||||
getPastDate,
|
|
||||||
CITIES,
|
|
||||||
FIXTURES,
|
|
||||||
} from '../support/test-utilities';
|
|
||||||
|
|
||||||
const today = getToday();
|
|
||||||
const tomorrow = getTomorrow();
|
|
||||||
const yesterday = getYesterday();
|
|
||||||
const futureDate = getFutureDate(7);
|
|
||||||
const pastDate = getPastDate(7);
|
|
||||||
const dateParam = buildRouteParam('MOW', today);
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Online Board - Flight Search Tests (30+ tests)
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
test.describe('Online Board - Flight Search', () => {
|
|
||||||
test.describe('Category 1: Basic Flight Search (8 tests)', () => {
|
|
||||||
test('Should search by flight number (manual input) (Test 1)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const searchInput = page.locator('[data-testid="flight-search-input"]');
|
|
||||||
await searchInput.fill('SU 1234');
|
|
||||||
await searchInput.press('Enter');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCards = page.locator('[data-testid="flight-card"]');
|
|
||||||
await expect(flightCards).toHaveCount(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search with today date (Test 2)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/departure\/MOW-\d{8}/);
|
|
||||||
await expect(page).toHaveTitle(/Отправление/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search with future date (Test 3)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', futureDate)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/departure\/MOW-\d{8}/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search with past date and show validation (Test 4)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', pastDate)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/departure\/MOW-\d{8}/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search without date and use today (Test 5)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/departure/MOW`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/departure\/MOW-\d{8}/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search with invalid flight number and show error (Test 6)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const searchInput = page.locator('[data-testid="flight-search-input"]');
|
|
||||||
await searchInput.fill('INVALID');
|
|
||||||
await searchInput.press('Enter');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const noResults = page.locator('[data-testid="no-results"]');
|
|
||||||
await expect(noResults).toBeVisible();
|
|
||||||
await expect(noResults).toContainText('Нет результатов');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search with empty flight number and show validation (Test 7)', async ({
|
|
||||||
page,
|
|
||||||
}) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const searchInput = page.locator('[data-testid="flight-search-input"]');
|
|
||||||
await searchInput.fill('');
|
|
||||||
await searchInput.press('Enter');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const noResults = page.locator('[data-testid="no-results"]');
|
|
||||||
await expect(noResults).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search with flight number that has multiple results (Test 8)', async ({
|
|
||||||
page,
|
|
||||||
}) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const searchInput = page.locator('[data-testid="flight-search-input"]');
|
|
||||||
await searchInput.fill('SU');
|
|
||||||
await searchInput.press('Enter');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCards = page.locator('[data-testid="flight-card"]');
|
|
||||||
await expect(flightCards).toHaveCount(20);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Category 2: Date Selection (6 tests)', () => {
|
|
||||||
test('Should select date from calendar (Test 9)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const calendarInput = page.locator('[data-testid="calendar-input"]');
|
|
||||||
await expect(calendarInput).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should select date range (Test 10)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const dateRange = page.locator('[data-testid="date-range"]');
|
|
||||||
await expect(dateRange).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should verify date format DD.MM.YYYY (Test 11)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const dateText = page.locator('[data-testid="board-date"]');
|
|
||||||
await expect(dateText).toBeVisible();
|
|
||||||
|
|
||||||
const dateValue = await dateText.textContent();
|
|
||||||
expect(dateValue).toMatch(/\d{2}\.\d{2}\.\d{4}/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should verify date validation min/max dates (Test 12)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const calendarInput = page.locator('[data-testid="calendar-input"]');
|
|
||||||
await expect(calendarInput).toBeEnabled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should select today date (Test 13)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const todayTab = page.locator(`[data-testid="date-tab-${today.replace(/-/g, '')}"]`);
|
|
||||||
await expect(todayTab).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should select tomorrow date (Test 14)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', tomorrow)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const tomorrowTab = page.locator(`[data-testid="date-tab-${tomorrow.replace(/-/g, '')}"]`);
|
|
||||||
await expect(tomorrowTab).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Category 3: Flight Results (6 tests)', () => {
|
|
||||||
test('Should verify flight results display (Test 15)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCards = page.locator('[data-testid="flight-card"]');
|
|
||||||
await expect(flightCards).toHaveCount(20);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should verify flight count (Test 16)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCards = page.locator('[data-testid="flight-card"]');
|
|
||||||
await expect(flightCards).toHaveCount(20);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should verify flight details in results (Test 17)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toBeVisible();
|
|
||||||
|
|
||||||
const flightNumber = flightCard.locator('[data-testid="flight-number"]');
|
|
||||||
await expect(flightNumber).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should verify empty results message (Test 18)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByNumber(page, 'SU 9999');
|
|
||||||
|
|
||||||
const noResults = page.locator('[data-testid="no-results"]');
|
|
||||||
await expect(noResults).toBeVisible();
|
|
||||||
await expect(noResults).toContainText('Нет результатов');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should verify loading state (Test 19)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const loadingSpinner = page.locator('[data-testid="loading-spinner"]');
|
|
||||||
await expect(loadingSpinner).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should verify error state (Test 20)', async ({ page }) => {
|
|
||||||
await page.route('**/api/flights/**', (route) => {
|
|
||||||
return route.abort('internetdisconnected');
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const networkError = page.locator('[data-testid="network-error"]');
|
|
||||||
await expect(networkError).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Category 4: Flight Details (5 tests)', () => {
|
|
||||||
test('Should open flight details from results (Test 21)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await flightCard.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/\/ru-ru\/[A-Z]{2}\s?\d+-\d{8}/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should verify flight details content (Test 22)', async ({ page }) => {
|
|
||||||
const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByNumber(page, flight.flightNumber);
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await flightCard.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(page.getByText(flight.flightNumber)).toBeVisible();
|
|
||||||
await expect(page.getByText(flight.airlineName)).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should verify flight route details (Test 23)', async ({ page }) => {
|
|
||||||
const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByNumber(page, flight.flightNumber);
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await flightCard.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(page.getByText(flight.departure.cityName)).toBeVisible();
|
|
||||||
await expect(page.getByText(flight.arrival.cityName)).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should verify flight status details (Test 24)', async ({ page }) => {
|
|
||||||
const flight = generateFlight({
|
|
||||||
direction: 'departure',
|
|
||||||
cityCode: 'MOW',
|
|
||||||
status: 'scheduled',
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByNumber(page, flight.flightNumber);
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await flightCard.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(page.getByText(flight.status)).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should close flight details (Test 25)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await flightCard.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const closeBtn = page.locator('[data-testid="close-details-btn"]');
|
|
||||||
await closeBtn.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/departure\/MOW-\d{8}/);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Category 5: Edge Cases (5 tests)', () => {
|
|
||||||
test('Should search for non-existent flight number (Test 26)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByNumber(page, 'SU 9999');
|
|
||||||
|
|
||||||
const noResults = page.locator('[data-testid="no-results"]');
|
|
||||||
await expect(noResults).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search with special characters (Test 27)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const searchInput = page.locator('[data-testid="flight-search-input"]');
|
|
||||||
await searchInput.fill('SU 123!');
|
|
||||||
await searchInput.press('Enter');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const noResults = page.locator('[data-testid="no-results"]');
|
|
||||||
await expect(noResults).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search with very long flight number (Test 28)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const searchInput = page.locator('[data-testid="flight-search-input"]');
|
|
||||||
await searchInput.fill('SU 12345678901234567890');
|
|
||||||
await searchInput.press('Enter');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCards = page.locator('[data-testid="flight-card"]');
|
|
||||||
await expect(flightCards).toHaveCount(20);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search with Unicode characters (Test 29)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const searchInput = page.locator('[data-testid="flight-search-input"]');
|
|
||||||
await searchInput.fill('SU 1234 🛫');
|
|
||||||
await searchInput.press('Enter');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCards = page.locator('[data-testid="flight-card"]');
|
|
||||||
await expect(flightCards).toHaveCount(20);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should handle rapid search attempts (Test 30)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const searchInput = page.locator('[data-testid="flight-search-input"]');
|
|
||||||
|
|
||||||
for (let i = 0; i < 5; i++) {
|
|
||||||
await searchInput.fill(`SU ${1000 + i}`);
|
|
||||||
await searchInput.press('Enter');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
}
|
|
||||||
|
|
||||||
const flightCards = page.locator('[data-testid="flight-card"]');
|
|
||||||
await expect(flightCards).toHaveCount(20);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Additional Flight Search Tests', () => {
|
|
||||||
test('Should navigate to flight board for different cities (Test 31)', async ({ page }) => {
|
|
||||||
const cities = ['MOW', 'LED', 'AER', 'OVB', 'KRR'];
|
|
||||||
|
|
||||||
for (const cityCode of cities) {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', cityCode, today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(new RegExp(`departure/${cityCode}-\\d{8}`));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should display correct date in title (Test 32)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const dateText = page.locator('[data-testid="board-date"]');
|
|
||||||
await expect(dateText).toBeVisible();
|
|
||||||
await expect(dateText).toContainText(today);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should filter by status (Test 33)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const statusFilter = page.locator('[data-testid="status-filter"]');
|
|
||||||
await statusFilter.click();
|
|
||||||
|
|
||||||
const scheduledOption = page.locator('[data-testid="filter-option-scheduled"]');
|
|
||||||
await scheduledOption.click();
|
|
||||||
|
|
||||||
const filteredFlights = page.locator('[data-testid="flight-card"]');
|
|
||||||
await expect(filteredFlights).toHaveCount(20);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should filter by airline (Test 34)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const airlineFilter = page.locator('[data-testid="airline-filter"]');
|
|
||||||
await airlineFilter.click();
|
|
||||||
|
|
||||||
const aeroflotOption = page.locator('[data-testid="filter-option-SU"]');
|
|
||||||
await aeroflotOption.click();
|
|
||||||
|
|
||||||
const filteredFlights = page.locator('[data-testid="flight-card"]');
|
|
||||||
await expect(filteredFlights).toHaveCount(20);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should display flight number (Test 35)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toBeVisible();
|
|
||||||
|
|
||||||
const flightNumber = flightCard.locator('[data-testid="flight-number"]');
|
|
||||||
await expect(flightNumber).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should display airline name (Test 36)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toBeVisible();
|
|
||||||
|
|
||||||
const airlineName = flightCard.locator('[data-testid="airline-name"]');
|
|
||||||
await expect(airlineName).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should display departure and arrival cities (Test 37)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toBeVisible();
|
|
||||||
|
|
||||||
const departureCity = flightCard.locator('[data-testid="departure-city"]');
|
|
||||||
await expect(departureCity).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should display scheduled time (Test 38)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toBeVisible();
|
|
||||||
|
|
||||||
const scheduledTime = flightCard.locator('[data-testid="scheduled-time"]');
|
|
||||||
await expect(scheduledTime).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should display actual time when available (Test 39)', async ({ page }) => {
|
|
||||||
const flight = generateFlight({
|
|
||||||
direction: 'departure',
|
|
||||||
cityCode: 'MOW',
|
|
||||||
status: 'departed',
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toBeVisible();
|
|
||||||
|
|
||||||
const actualTime = flightCard.locator('[data-testid="actual-time"]');
|
|
||||||
await expect(actualTime).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should display delay information (Test 40)', async ({ page }) => {
|
|
||||||
const flight = generateFlight({
|
|
||||||
direction: 'departure',
|
|
||||||
cityCode: 'MOW',
|
|
||||||
status: 'delayed',
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toBeVisible();
|
|
||||||
|
|
||||||
const delayInfo = flightCard.locator('[data-testid="delay-info"]');
|
|
||||||
await expect(delayInfo).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should display terminal information (Test 41)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toBeVisible();
|
|
||||||
|
|
||||||
const terminal = flightCard.locator('[data-testid="terminal"]');
|
|
||||||
await expect(terminal).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should display baggage belt information (Test 42)', async ({ page }) => {
|
|
||||||
const flight = generateFlight({
|
|
||||||
direction: 'departure',
|
|
||||||
cityCode: 'MOW',
|
|
||||||
status: 'arrived',
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toBeVisible();
|
|
||||||
|
|
||||||
const baggageBelt = flightCard.locator('[data-testid="baggage-belt"]');
|
|
||||||
await expect(baggageBelt).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should navigate to tomorrow date tab (Test 43)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const dateTab = page.locator(`[data-testid="date-tab-${tomorrow.replace(/-/g, '')}"]`);
|
|
||||||
if ((await dateTab.count()) > 0) {
|
|
||||||
await dateTab.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/departure\/MOW-\d{8}/);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should handle invalid city code (Test 44)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/departure/XXX-${today.replace(/-/g, '')}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const errorState = page.locator('[data-testid="error-state"]');
|
|
||||||
await expect(errorState).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should handle network error (Test 45)', async ({ page }) => {
|
|
||||||
await page.route('**/api/flights/**', (route) => {
|
|
||||||
return route.abort('internetdisconnected');
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const networkError = page.locator('[data-testid="network-error"]');
|
|
||||||
await expect(networkError).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should have proper ARIA labels (Test 46)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toHaveAttribute('role', 'article');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should be keyboard navigable (Test 47)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await page.keyboard.press('Tab');
|
|
||||||
await page.keyboard.press('Tab');
|
|
||||||
await page.keyboard.press('Tab');
|
|
||||||
|
|
||||||
const focusedElement = page.locator(':focus');
|
|
||||||
await expect(focusedElement).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search by flight number from search input (Test 48)', async ({ page }) => {
|
|
||||||
const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByNumber(page, flight.flightNumber);
|
|
||||||
|
|
||||||
const searchResults = page.locator('[data-testid="flight-card"]');
|
|
||||||
await expect(searchResults).toHaveCount(1);
|
|
||||||
|
|
||||||
await verifyFlightCard(page, flight);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should show no results when flight not found (Test 49)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByNumber(page, 'SU 9999');
|
|
||||||
|
|
||||||
const noResults = page.locator('[data-testid="no-results"]');
|
|
||||||
await expect(noResults).toBeVisible();
|
|
||||||
await expect(noResults).toContainText('Нет результатов');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should search by route (Test 50)', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByRoute(page, 'Moscow', 'Sochi');
|
|
||||||
|
|
||||||
const flightCards = page.locator('[data-testid="flight-card"]');
|
|
||||||
await expect(flightCards).toHaveCount(20);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,450 +0,0 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
|
||||||
import type { Page } from '@playwright/test';
|
|
||||||
import {
|
|
||||||
buildFlightDetailsPath,
|
|
||||||
buildRouteParam,
|
|
||||||
generateFlight,
|
|
||||||
generateFlights,
|
|
||||||
getToday,
|
|
||||||
getTomorrow,
|
|
||||||
CITIES,
|
|
||||||
} from '../support/test-utilities';
|
|
||||||
|
|
||||||
const today = getToday();
|
|
||||||
const tomorrow = getTomorrow();
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Flight Details Tests
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
test.describe('Flight Details', () => {
|
|
||||||
test.describe('Page Navigation', () => {
|
|
||||||
test('should navigate to flight details page', async ({ page }) => {
|
|
||||||
const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(new RegExp(`/${slug}`));
|
|
||||||
await expect(page).toHaveTitle(new RegExp(flight.flightNumber));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should navigate to flight details for arrival flight', async ({ page }) => {
|
|
||||||
const flight = generateFlight({ direction: 'arrival', cityCode: 'MOW' });
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(new RegExp(`/${slug}`));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should navigate to flight details for different date', async ({ page }) => {
|
|
||||||
const flight = generateFlight({ direction: 'departure', cityCode: 'MOW', date: tomorrow });
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${tomorrow.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(new RegExp(`/${slug}`));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Flight Information', () => {
|
|
||||||
test('should display flight number', async ({ page }) => {
|
|
||||||
const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightNumber = page.locator('[data-testid="flight-number"]');
|
|
||||||
await expect(flightNumber).toBeVisible();
|
|
||||||
await expect(flightNumber).toContainText(flight.flightNumber);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display airline name', async ({ page }) => {
|
|
||||||
const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const airlineName = page.locator('[data-testid="airline-name"]');
|
|
||||||
await expect(airlineName).toBeVisible();
|
|
||||||
await expect(airlineName).toContainText(flight.airlineName);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display aircraft type', async ({ page }) => {
|
|
||||||
const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const aircraftType = page.locator('[data-testid="aircraft-type"]');
|
|
||||||
await expect(aircraftType).toBeVisible();
|
|
||||||
await expect(aircraftType).toContainText(flight.aircraftType || '');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display departure city', async ({ page }) => {
|
|
||||||
const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const departureCity = page.locator('[data-testid="departure-city"]');
|
|
||||||
await expect(departureCity).toBeVisible();
|
|
||||||
await expect(departureCity).toContainText(flight.departure.cityName);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display arrival city', async ({ page }) => {
|
|
||||||
const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const arrivalCity = page.locator('[data-testid="arrival-city"]');
|
|
||||||
await expect(arrivalCity).toBeVisible();
|
|
||||||
await expect(arrivalCity).toContainText(flight.arrival.cityName);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display scheduled departure time', async ({ page }) => {
|
|
||||||
const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const depTime = page.locator('[data-testid="scheduled-departure-time"]');
|
|
||||||
await expect(depTime).toBeVisible();
|
|
||||||
|
|
||||||
const depTimeText = flight.departure.time.scheduled.slice(11, 16);
|
|
||||||
await expect(depTime).toContainText(depTimeText);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display scheduled arrival time', async ({ page }) => {
|
|
||||||
const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const arrTime = page.locator('[data-testid="scheduled-arrival-time"]');
|
|
||||||
await expect(arrTime).toBeVisible();
|
|
||||||
|
|
||||||
const arrTimeText = flight.arrival.time.scheduled.slice(11, 16);
|
|
||||||
await expect(arrTime).toContainText(arrTimeText);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display flight duration', async ({ page }) => {
|
|
||||||
const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const duration = page.locator('[data-testid="flight-duration"]');
|
|
||||||
await expect(duration).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display flight status', async ({ page }) => {
|
|
||||||
const flight = generateFlight({
|
|
||||||
direction: 'departure',
|
|
||||||
cityCode: 'MOW',
|
|
||||||
status: 'scheduled',
|
|
||||||
});
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const status = page.locator('[data-testid="flight-status"]');
|
|
||||||
await expect(status).toBeVisible();
|
|
||||||
await expect(status).toContainText(flight.status);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Flight Details', () => {
|
|
||||||
test('should display terminal information', async ({ page }) => {
|
|
||||||
const flight = generateFlight({
|
|
||||||
direction: 'departure',
|
|
||||||
cityCode: 'MOW',
|
|
||||||
departure: { terminal: 'B' },
|
|
||||||
});
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const terminal = page.locator('[data-testid="terminal"]');
|
|
||||||
await expect(terminal).toBeVisible();
|
|
||||||
await expect(terminal).toContainText('B');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display boarding gate information', async ({ page }) => {
|
|
||||||
const flight = generateFlight({
|
|
||||||
direction: 'departure',
|
|
||||||
cityCode: 'MOW',
|
|
||||||
status: 'boarding',
|
|
||||||
boarding: { gate: '11', status: 'Идёт посадка' },
|
|
||||||
});
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const boardingGate = page.locator('[data-testid="boarding-gate"]');
|
|
||||||
await expect(boardingGate).toBeVisible();
|
|
||||||
await expect(boardingGate).toContainText('11');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display baggage belt information', async ({ page }) => {
|
|
||||||
const flight = generateFlight({
|
|
||||||
direction: 'arrival',
|
|
||||||
cityCode: 'MOW',
|
|
||||||
status: 'arrived',
|
|
||||||
arrivalInfo: { baggageBelt: '5', transfer: 'Тран' },
|
|
||||||
});
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const baggageBelt = page.locator('[data-testid="baggage-belt"]');
|
|
||||||
await expect(baggageBelt).toBeVisible();
|
|
||||||
await expect(baggageBelt).toContainText('5');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display check-in information', async ({ page }) => {
|
|
||||||
const flight = generateFlight({
|
|
||||||
direction: 'departure',
|
|
||||||
cityCode: 'MOW',
|
|
||||||
status: 'checkin',
|
|
||||||
checkin: { status: 'В процессе' },
|
|
||||||
});
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const checkinInfo = page.locator('[data-testid="checkin-info"]');
|
|
||||||
await expect(checkinInfo).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display deplaning information', async ({ page }) => {
|
|
||||||
const flight = generateFlight({
|
|
||||||
direction: 'arrival',
|
|
||||||
cityCode: 'MOW',
|
|
||||||
status: 'arrived',
|
|
||||||
deplaning: { status: 'В процессе', transfer: 'Трап' },
|
|
||||||
});
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const deplaningInfo = page.locator('[data-testid="deplaning-info"]');
|
|
||||||
await expect(deplaningInfo).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Aircraft Information', () => {
|
|
||||||
test('should display aircraft type', async ({ page }) => {
|
|
||||||
const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const aircraftType = page.locator('[data-testid="aircraft-type"]');
|
|
||||||
await expect(aircraftType).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display aircraft name', async ({ page }) => {
|
|
||||||
const flight = generateFlight({
|
|
||||||
direction: 'departure',
|
|
||||||
cityCode: 'MOW',
|
|
||||||
aircraft: { type: 'Airbus A320', name: 'В. Высоцкий' },
|
|
||||||
});
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const aircraftName = page.locator('[data-testid="aircraft-name"]');
|
|
||||||
await expect(aircraftName).toBeVisible();
|
|
||||||
await expect(aircraftName).toContainText('В. Высоцкий');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display seat configuration', async ({ page }) => {
|
|
||||||
const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const seatInfo = page.locator('[data-testid="seat-info"]');
|
|
||||||
await expect(seatInfo).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Schedule Information', () => {
|
|
||||||
test('should display scheduled departure', async ({ page }) => {
|
|
||||||
const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const scheduledDep = page.locator('[data-testid="scheduled-departure"]');
|
|
||||||
await expect(scheduledDep).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display scheduled arrival', async ({ page }) => {
|
|
||||||
const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const scheduledArr = page.locator('[data-testid="scheduled-arrival"]');
|
|
||||||
await expect(scheduledArr).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display actual departure when available', async ({ page }) => {
|
|
||||||
const flight = generateFlight({
|
|
||||||
direction: 'departure',
|
|
||||||
cityCode: 'MOW',
|
|
||||||
status: 'departed',
|
|
||||||
});
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const actualDep = page.locator('[data-testid="actual-departure"]');
|
|
||||||
await expect(actualDep).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display actual arrival when available', async ({ page }) => {
|
|
||||||
const flight = generateFlight({
|
|
||||||
direction: 'arrival',
|
|
||||||
cityCode: 'MOW',
|
|
||||||
status: 'arrived',
|
|
||||||
});
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const actualArr = page.locator('[data-testid="actual-arrival"]');
|
|
||||||
await expect(actualArr).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display expected arrival when delayed', async ({ page }) => {
|
|
||||||
const flight = generateFlight({
|
|
||||||
direction: 'arrival',
|
|
||||||
cityCode: 'MOW',
|
|
||||||
status: 'delayed',
|
|
||||||
});
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const expectedArr = page.locator('[data-testid="expected-arrival"]');
|
|
||||||
await expect(expectedArr).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Error Handling', () => {
|
|
||||||
test('should handle invalid flight number', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/SU9999-${today.replace(/-/g, '')}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const errorState = page.locator('[data-testid="error-state"]');
|
|
||||||
await expect(errorState).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should handle flight not found', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/SU9999-20260406`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const notFound = page.locator('[data-testid="not-found"]');
|
|
||||||
await expect(notFound).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should handle network error', async ({ page }) => {
|
|
||||||
await page.route('**/api/flights/**', (route) => {
|
|
||||||
return route.abort('internetdisconnected');
|
|
||||||
});
|
|
||||||
|
|
||||||
const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const networkError = page.locator('[data-testid="network-error"]');
|
|
||||||
await expect(networkError).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Navigation', () => {
|
|
||||||
test('should navigate back to board', async ({ page }) => {
|
|
||||||
const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const backLink = page.locator('[data-testid="back-link"]');
|
|
||||||
await backLink.click();
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/onlineboard\/departure\/MOW-\d{8}/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should navigate to related flights', async ({ page }) => {
|
|
||||||
const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const relatedFlights = page.locator('[data-testid="related-flights"]');
|
|
||||||
await expect(relatedFlights).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Accessibility', () => {
|
|
||||||
test('should have proper ARIA labels', async ({ page }) => {
|
|
||||||
const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const pageContent = page.locator('[data-testid="page-content"]');
|
|
||||||
await expect(pageContent).toHaveAttribute('role', 'main');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should be keyboard navigable', async ({ page }) => {
|
|
||||||
const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await page.keyboard.press('Tab');
|
|
||||||
await page.keyboard.press('Tab');
|
|
||||||
await page.keyboard.press('Tab');
|
|
||||||
|
|
||||||
const focusedElement = page.locator(':focus');
|
|
||||||
await expect(focusedElement).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,334 +0,0 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
|
||||||
import type { Page } from '@playwright/test';
|
|
||||||
import {
|
|
||||||
buildFlightsMapPath,
|
|
||||||
buildRouteParam,
|
|
||||||
generateFlight,
|
|
||||||
generateFlights,
|
|
||||||
getToday,
|
|
||||||
getTomorrow,
|
|
||||||
CITIES,
|
|
||||||
} from '@e2e/support/test-utilities';
|
|
||||||
|
|
||||||
const today = getToday();
|
|
||||||
const tomorrow = getTomorrow();
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Flights Map Tests
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
test.describe('Flights Map', () => {
|
|
||||||
test.describe('Page Navigation', () => {
|
|
||||||
test('should navigate to flights map page', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/flights-map/);
|
|
||||||
await expect(page).toHaveTitle(/Карта полетов/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display map container', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const mapContainer = page.locator('[data-testid="map-container"]');
|
|
||||||
await expect(mapContainer).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Map Display', () => {
|
|
||||||
test('should display flight markers', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const markers = page.locator('[data-testid="flight-marker"]');
|
|
||||||
await expect(markers).toHaveCount(20);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display flight popup on marker click', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const marker = page.locator('[data-testid="flight-marker"]').first();
|
|
||||||
await marker.click();
|
|
||||||
|
|
||||||
const popup = page.locator('[data-testid="flight-popup"]');
|
|
||||||
await expect(popup).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display flight details in popup', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const marker = page.locator('[data-testid="flight-marker"]').first();
|
|
||||||
await marker.click();
|
|
||||||
|
|
||||||
const flightNumber = page.locator('[data-testid="popup-flight-number"]');
|
|
||||||
await expect(flightNumber).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display route line between airports', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const routeLine = page.locator('[data-testid="route-line"]');
|
|
||||||
await expect(routeLine).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Filtering', () => {
|
|
||||||
test('should filter by departure city', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const departureFilter = page.locator('[data-testid="departure-filter"]');
|
|
||||||
await departureFilter.click();
|
|
||||||
|
|
||||||
const moscowOption = page.locator('[data-testid="filter-option-MOW"]');
|
|
||||||
await moscowOption.click();
|
|
||||||
|
|
||||||
const markers = page.locator('[data-testid="flight-marker"]');
|
|
||||||
await expect(markers).toHaveCount(20);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should filter by arrival city', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const arrivalFilter = page.locator('[data-testid="arrival-filter"]');
|
|
||||||
await arrivalFilter.click();
|
|
||||||
|
|
||||||
const sochiOption = page.locator('[data-testid="filter-option-AER"]');
|
|
||||||
await sochiOption.click();
|
|
||||||
|
|
||||||
const markers = page.locator('[data-testid="flight-marker"]');
|
|
||||||
await expect(markers).toHaveCount(20);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should filter by status', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const statusFilter = page.locator('[data-testid="status-filter"]');
|
|
||||||
await statusFilter.click();
|
|
||||||
|
|
||||||
const scheduledOption = page.locator('[data-testid="filter-option-scheduled"]');
|
|
||||||
await scheduledOption.click();
|
|
||||||
|
|
||||||
const markers = page.locator('[data-testid="flight-marker"]');
|
|
||||||
await expect(markers).toHaveCount(20);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should clear all filters', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const clearFilters = page.locator('[data-testid="clear-filters"]');
|
|
||||||
await clearFilters.click();
|
|
||||||
|
|
||||||
const markers = page.locator('[data-testid="flight-marker"]');
|
|
||||||
await expect(markers).toHaveCount(20);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Flight Details Panel', () => {
|
|
||||||
test('should display flight details panel', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const panel = page.locator('[data-testid="flight-details-panel"]');
|
|
||||||
await expect(panel).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display flight number in panel', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightNumber = page.locator('[data-testid="panel-flight-number"]');
|
|
||||||
await expect(flightNumber).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display airline name in panel', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const airlineName = page.locator('[data-testid="panel-airline-name"]');
|
|
||||||
await expect(airlineName).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display departure and arrival cities in panel', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const departureCity = page.locator('[data-testid="panel-departure-city"]');
|
|
||||||
await expect(departureCity).toBeVisible();
|
|
||||||
|
|
||||||
const arrivalCity = page.locator('[data-testid="panel-arrival-city"]');
|
|
||||||
await expect(arrivalCity).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display scheduled times in panel', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const depTime = page.locator('[data-testid="panel-departure-time"]');
|
|
||||||
await expect(depTime).toBeVisible();
|
|
||||||
|
|
||||||
const arrTime = page.locator('[data-testid="panel-arrival-time"]');
|
|
||||||
await expect(arrTime).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display aircraft type in panel', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const aircraftType = page.locator('[data-testid="panel-aircraft-type"]');
|
|
||||||
await expect(aircraftType).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display flight status in panel', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const status = page.locator('[data-testid="panel-status"]');
|
|
||||||
await expect(status).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Map Controls', () => {
|
|
||||||
test('should have zoom in button', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const zoomIn = page.locator('[data-testid="zoom-in"]');
|
|
||||||
await expect(zoomIn).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should have zoom out button', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const zoomOut = page.locator('[data-testid="zoom-out"]');
|
|
||||||
await expect(zoomOut).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should have full screen button', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const fullScreen = page.locator('[data-testid="full-screen"]');
|
|
||||||
await expect(fullScreen).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should have layer toggle', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const layerToggle = page.locator('[data-testid="layer-toggle"]');
|
|
||||||
await expect(layerToggle).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Cluster Markers', () => {
|
|
||||||
test('should display cluster markers for multiple flights', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const cluster = page.locator('[data-testid="cluster-marker"]');
|
|
||||||
if ((await cluster.count()) > 0) {
|
|
||||||
await expect(cluster).toBeVisible();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should expand cluster on click', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const cluster = page.locator('[data-testid="cluster-marker"]').first();
|
|
||||||
if ((await cluster.count()) > 0) {
|
|
||||||
await cluster.click();
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
const markers = page.locator('[data-testid="flight-marker"]');
|
|
||||||
await expect(markers).toHaveCount(20);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Error Handling', () => {
|
|
||||||
test('should handle network error', async ({ page }) => {
|
|
||||||
await page.route('**/api/flights-map/**', (route) => {
|
|
||||||
return route.abort('internetdisconnected');
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const networkError = page.locator('[data-testid="network-error"]');
|
|
||||||
await expect(networkError).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should handle map loading error', async ({ page }) => {
|
|
||||||
await page.route('**/api/maps/**', (route) => {
|
|
||||||
return route.abort('internetdisconnected');
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const mapError = page.locator('[data-testid="map-error"]');
|
|
||||||
await expect(mapError).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Accessibility', () => {
|
|
||||||
test('should have proper ARIA labels', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const mapContainer = page.locator('[data-testid="map-container"]');
|
|
||||||
await expect(mapContainer).toHaveAttribute('role', 'application');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should be keyboard navigable', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await page.keyboard.press('Tab');
|
|
||||||
await page.keyboard.press('Tab');
|
|
||||||
await page.keyboard.press('Tab');
|
|
||||||
|
|
||||||
const focusedElement = page.locator(':focus');
|
|
||||||
await expect(focusedElement).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Responsive Design', () => {
|
|
||||||
test('should be responsive on mobile', async ({ page }) => {
|
|
||||||
await page.setViewportSize({ width: 375, height: 667 });
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const mapContainer = page.locator('[data-testid="map-container"]');
|
|
||||||
await expect(mapContainer).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should be responsive on tablet', async ({ page }) => {
|
|
||||||
await page.setViewportSize({ width: 768, height: 1024 });
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const mapContainer = page.locator('[data-testid="map-container"]');
|
|
||||||
await expect(mapContainer).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should be responsive on desktop', async ({ page }) => {
|
|
||||||
await page.setViewportSize({ width: 1920, height: 1080 });
|
|
||||||
await page.goto(`/ru-ru${buildFlightsMapPath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const mapContainer = page.locator('[data-testid="map-container"]');
|
|
||||||
await expect(mapContainer).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,301 +0,0 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
|
||||||
import type { Page } from '@playwright/test';
|
|
||||||
import {
|
|
||||||
buildOnlineBoardPath,
|
|
||||||
buildRouteParam,
|
|
||||||
searchFlightByNumber,
|
|
||||||
searchFlightByRoute,
|
|
||||||
verifyFlightCard,
|
|
||||||
generateFlight,
|
|
||||||
generateFlights,
|
|
||||||
getToday,
|
|
||||||
getTomorrow,
|
|
||||||
CITIES,
|
|
||||||
} from '../support/test-utilities';
|
|
||||||
|
|
||||||
const today = getToday();
|
|
||||||
const tomorrow = getTomorrow();
|
|
||||||
const dateParam = buildRouteParam('MOW', today);
|
|
||||||
const tomorrowParam = buildRouteParam('MOW', tomorrow);
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Online Board - Arrival Tests
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
test.describe('Online Board - Arrival', () => {
|
|
||||||
test.describe('Page Navigation', () => {
|
|
||||||
test('should navigate to arrival board for Moscow', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/arrival\/MOW-\d{8}/);
|
|
||||||
await expect(page).toHaveTitle(/Прибытие/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should navigate to arrival board for Saint Petersburg', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'LED', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/arrival\/LED-\d{8}/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should navigate to arrival board for Sochi', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'AER', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/arrival\/AER-\d{8}/);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Flight Display', () => {
|
|
||||||
test('should display arrival flights for Moscow', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCards = page.locator('[data-testid="flight-card"]');
|
|
||||||
await expect(flightCards).toHaveCount(20);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display flight details correctly', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toBeVisible();
|
|
||||||
|
|
||||||
await expect(flightCard.getByText('Прибытие')).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Flight Search', () => {
|
|
||||||
test('should search flight by flight number', async ({ page }) => {
|
|
||||||
const flight = generateFlight({ direction: 'arrival', cityCode: 'MOW' });
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByNumber(page, flight.flightNumber);
|
|
||||||
|
|
||||||
const searchResults = page.locator('[data-testid="flight-card"]');
|
|
||||||
await expect(searchResults).toHaveCount(1);
|
|
||||||
|
|
||||||
await verifyFlightCard(page, flight);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should show no results when flight not found', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByNumber(page, 'SU 9999');
|
|
||||||
|
|
||||||
const noResults = page.locator('[data-testid="no-results"]');
|
|
||||||
await expect(noResults).toBeVisible();
|
|
||||||
await expect(noResults).toContainText('Нет результатов');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Date Navigation', () => {
|
|
||||||
test('should navigate to tomorrow', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const dateTab = page.locator(`[data-testid="date-tab-${tomorrow.replace(/-/g, '')}"]`);
|
|
||||||
if ((await dateTab.count()) > 0) {
|
|
||||||
await dateTab.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/arrival\/MOW-\d{8}/);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display correct date in title', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const dateText = page.locator('[data-testid="board-date"]');
|
|
||||||
await expect(dateText).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Filtering', () => {
|
|
||||||
test('should filter by status', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const statusFilter = page.locator('[data-testid="status-filter"]');
|
|
||||||
await statusFilter.click();
|
|
||||||
|
|
||||||
const scheduledOption = page.locator('[data-testid="filter-option-scheduled"]');
|
|
||||||
await scheduledOption.click();
|
|
||||||
|
|
||||||
const filteredFlights = page.locator('[data-testid="flight-card"]');
|
|
||||||
await expect(filteredFlights).toHaveCount(20);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should filter by airline', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const airlineFilter = page.locator('[data-testid="airline-filter"]');
|
|
||||||
await airlineFilter.click();
|
|
||||||
|
|
||||||
const aeroflotOption = page.locator('[data-testid="filter-option-SU"]');
|
|
||||||
await aeroflotOption.click();
|
|
||||||
|
|
||||||
const filteredFlights = page.locator('[data-testid="flight-card"]');
|
|
||||||
await expect(filteredFlights).toHaveCount(20);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Flight Card', () => {
|
|
||||||
test('should display flight number', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toBeVisible();
|
|
||||||
|
|
||||||
const flightNumber = flightCard.locator('[data-testid="flight-number"]');
|
|
||||||
await expect(flightNumber).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display airline name', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toBeVisible();
|
|
||||||
|
|
||||||
const airlineName = flightCard.locator('[data-testid="airline-name"]');
|
|
||||||
await expect(airlineName).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display departure and arrival cities', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toBeVisible();
|
|
||||||
|
|
||||||
const arrivalCity = flightCard.locator('[data-testid="arrival-city"]');
|
|
||||||
await expect(arrivalCity).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display scheduled time', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toBeVisible();
|
|
||||||
|
|
||||||
const scheduledTime = flightCard.locator('[data-testid="scheduled-time"]');
|
|
||||||
await expect(scheduledTime).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display actual time when available', async ({ page }) => {
|
|
||||||
const flight = generateFlight({
|
|
||||||
direction: 'arrival',
|
|
||||||
cityCode: 'MOW',
|
|
||||||
status: 'arrived',
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toBeVisible();
|
|
||||||
|
|
||||||
const actualTime = flightCard.locator('[data-testid="actual-time"]');
|
|
||||||
await expect(actualTime).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display delay information', async ({ page }) => {
|
|
||||||
const flight = generateFlight({
|
|
||||||
direction: 'arrival',
|
|
||||||
cityCode: 'MOW',
|
|
||||||
status: 'delayed',
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toBeVisible();
|
|
||||||
|
|
||||||
const delayInfo = flightCard.locator('[data-testid="delay-info"]');
|
|
||||||
await expect(delayInfo).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display terminal information', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toBeVisible();
|
|
||||||
|
|
||||||
const terminal = flightCard.locator('[data-testid="terminal"]');
|
|
||||||
await expect(terminal).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display baggage belt information', async ({ page }) => {
|
|
||||||
const flight = generateFlight({
|
|
||||||
direction: 'arrival',
|
|
||||||
cityCode: 'MOW',
|
|
||||||
status: 'arrived',
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toBeVisible();
|
|
||||||
|
|
||||||
const baggageBelt = flightCard.locator('[data-testid="baggage-belt"]');
|
|
||||||
await expect(baggageBelt).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Error Handling', () => {
|
|
||||||
test('should handle invalid city code', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/arrival/XXX-${today.replace(/-/g, '')}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const errorState = page.locator('[data-testid="error-state"]');
|
|
||||||
await expect(errorState).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should handle network error', async ({ page }) => {
|
|
||||||
await page.route('**/api/flights/**', (route) => {
|
|
||||||
return route.abort('internetdisconnected');
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const networkError = page.locator('[data-testid="network-error"]');
|
|
||||||
await expect(networkError).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Accessibility', () => {
|
|
||||||
test('should have proper ARIA labels', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toHaveAttribute('role', 'article');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should be keyboard navigable', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('arrival', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await page.keyboard.press('Tab');
|
|
||||||
await page.keyboard.press('Tab');
|
|
||||||
await page.keyboard.press('Tab');
|
|
||||||
|
|
||||||
const focusedElement = page.locator(':focus');
|
|
||||||
await expect(focusedElement).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,301 +0,0 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
|
||||||
import type { Page } from '@playwright/test';
|
|
||||||
import {
|
|
||||||
buildOnlineBoardPath,
|
|
||||||
buildRouteParam,
|
|
||||||
searchFlightByNumber,
|
|
||||||
searchFlightByRoute,
|
|
||||||
verifyFlightCard,
|
|
||||||
generateFlight,
|
|
||||||
generateFlights,
|
|
||||||
getToday,
|
|
||||||
getTomorrow,
|
|
||||||
CITIES,
|
|
||||||
} from '../support/test-utilities';
|
|
||||||
|
|
||||||
const today = getToday();
|
|
||||||
const tomorrow = getTomorrow();
|
|
||||||
const dateParam = buildRouteParam('MOW', today);
|
|
||||||
const tomorrowParam = buildRouteParam('MOW', tomorrow);
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Online Board - Departure Tests
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
test.describe('Online Board - Departure', () => {
|
|
||||||
test.describe('Page Navigation', () => {
|
|
||||||
test('should navigate to departure board for Moscow', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/departure\/MOW-\d{8}/);
|
|
||||||
await expect(page).toHaveTitle(/Отправление/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should navigate to departure board for Saint Petersburg', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'LED', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/departure\/LED-\d{8}/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should navigate to departure board for Sochi', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'AER', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/departure\/AER-\d{8}/);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Flight Display', () => {
|
|
||||||
test('should display departure flights for Moscow', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCards = page.locator('[data-testid="flight-card"]');
|
|
||||||
await expect(flightCards).toHaveCount(20);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display flight details correctly', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toBeVisible();
|
|
||||||
|
|
||||||
await expect(flightCard.getByText('Отправление')).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Flight Search', () => {
|
|
||||||
test('should search flight by flight number', async ({ page }) => {
|
|
||||||
const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByNumber(page, flight.flightNumber);
|
|
||||||
|
|
||||||
const searchResults = page.locator('[data-testid="flight-card"]');
|
|
||||||
await expect(searchResults).toHaveCount(1);
|
|
||||||
|
|
||||||
await verifyFlightCard(page, flight);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should show no results when flight not found', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByNumber(page, 'SU 9999');
|
|
||||||
|
|
||||||
const noResults = page.locator('[data-testid="no-results"]');
|
|
||||||
await expect(noResults).toBeVisible();
|
|
||||||
await expect(noResults).toContainText('Нет результатов');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Date Navigation', () => {
|
|
||||||
test('should navigate to tomorrow', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const dateTab = page.locator(`[data-testid="date-tab-${tomorrow.replace(/-/g, '')}"]`);
|
|
||||||
if ((await dateTab.count()) > 0) {
|
|
||||||
await dateTab.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/departure\/MOW-\d{8}/);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display correct date in title', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const dateText = page.locator('[data-testid="board-date"]');
|
|
||||||
await expect(dateText).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Filtering', () => {
|
|
||||||
test('should filter by status', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const statusFilter = page.locator('[data-testid="status-filter"]');
|
|
||||||
await statusFilter.click();
|
|
||||||
|
|
||||||
const scheduledOption = page.locator('[data-testid="filter-option-scheduled"]');
|
|
||||||
await scheduledOption.click();
|
|
||||||
|
|
||||||
const filteredFlights = page.locator('[data-testid="flight-card"]');
|
|
||||||
await expect(filteredFlights).toHaveCount(20);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should filter by airline', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const airlineFilter = page.locator('[data-testid="airline-filter"]');
|
|
||||||
await airlineFilter.click();
|
|
||||||
|
|
||||||
const aeroflotOption = page.locator('[data-testid="filter-option-SU"]');
|
|
||||||
await aeroflotOption.click();
|
|
||||||
|
|
||||||
const filteredFlights = page.locator('[data-testid="flight-card"]');
|
|
||||||
await expect(filteredFlights).toHaveCount(20);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Flight Card', () => {
|
|
||||||
test('should display flight number', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toBeVisible();
|
|
||||||
|
|
||||||
const flightNumber = flightCard.locator('[data-testid="flight-number"]');
|
|
||||||
await expect(flightNumber).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display airline name', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toBeVisible();
|
|
||||||
|
|
||||||
const airlineName = flightCard.locator('[data-testid="airline-name"]');
|
|
||||||
await expect(airlineName).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display departure and arrival cities', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toBeVisible();
|
|
||||||
|
|
||||||
const departureCity = flightCard.locator('[data-testid="departure-city"]');
|
|
||||||
await expect(departureCity).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display scheduled time', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toBeVisible();
|
|
||||||
|
|
||||||
const scheduledTime = flightCard.locator('[data-testid="scheduled-time"]');
|
|
||||||
await expect(scheduledTime).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display actual time when available', async ({ page }) => {
|
|
||||||
const flight = generateFlight({
|
|
||||||
direction: 'departure',
|
|
||||||
cityCode: 'MOW',
|
|
||||||
status: 'departed',
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toBeVisible();
|
|
||||||
|
|
||||||
const actualTime = flightCard.locator('[data-testid="actual-time"]');
|
|
||||||
await expect(actualTime).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display delay information', async ({ page }) => {
|
|
||||||
const flight = generateFlight({
|
|
||||||
direction: 'departure',
|
|
||||||
cityCode: 'MOW',
|
|
||||||
status: 'delayed',
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toBeVisible();
|
|
||||||
|
|
||||||
const delayInfo = flightCard.locator('[data-testid="delay-info"]');
|
|
||||||
await expect(delayInfo).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display terminal information', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toBeVisible();
|
|
||||||
|
|
||||||
const terminal = flightCard.locator('[data-testid="terminal"]');
|
|
||||||
await expect(terminal).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display boarding gate information', async ({ page }) => {
|
|
||||||
const flight = generateFlight({
|
|
||||||
direction: 'departure',
|
|
||||||
cityCode: 'MOW',
|
|
||||||
status: 'boarding',
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toBeVisible();
|
|
||||||
|
|
||||||
const boardingGate = flightCard.locator('[data-testid="boarding-gate"]');
|
|
||||||
await expect(boardingGate).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Error Handling', () => {
|
|
||||||
test('should handle invalid city code', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/departure/XXX-${today.replace(/-/g, '')}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const errorState = page.locator('[data-testid="error-state"]');
|
|
||||||
await expect(errorState).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should handle network error', async ({ page }) => {
|
|
||||||
await page.route('**/api/flights/**', (route) => {
|
|
||||||
return route.abort('internetdisconnected');
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const networkError = page.locator('[data-testid="network-error"]');
|
|
||||||
await expect(networkError).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Accessibility', () => {
|
|
||||||
test('should have proper ARIA labels', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toHaveAttribute('role', 'article');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should be keyboard navigable', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await page.keyboard.press('Tab');
|
|
||||||
await page.keyboard.press('Tab');
|
|
||||||
await page.keyboard.press('Tab');
|
|
||||||
|
|
||||||
const focusedElement = page.locator(':focus');
|
|
||||||
await expect(focusedElement).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,454 +0,0 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
|
||||||
import type { Page } from '@playwright/test';
|
|
||||||
import {
|
|
||||||
buildOnlineBoardPath,
|
|
||||||
buildRouteParam,
|
|
||||||
searchFlightByNumber,
|
|
||||||
verifyFlightCard,
|
|
||||||
generateFlight,
|
|
||||||
generateFlights,
|
|
||||||
getToday,
|
|
||||||
getTomorrow,
|
|
||||||
CITIES,
|
|
||||||
} from '../support/test-utilities';
|
|
||||||
|
|
||||||
const today = getToday();
|
|
||||||
const tomorrow = getTomorrow();
|
|
||||||
const dateParam = buildRouteParam('MOW', today);
|
|
||||||
const tomorrowParam = buildRouteParam('MOW', tomorrow);
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Online Board - Flight Tests
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
test.describe('Online Board - Flight', () => {
|
|
||||||
test.describe('Page Navigation', () => {
|
|
||||||
test('should navigate to flight details page', async ({ page }) => {
|
|
||||||
const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(new RegExp(`/${slug}`));
|
|
||||||
await expect(page).toHaveTitle(new RegExp(flight.flightNumber));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should navigate to flight details for arrival flight', async ({ page }) => {
|
|
||||||
const flight = generateFlight({ direction: 'arrival', cityCode: 'MOW' });
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(new RegExp(`/${slug}`));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should navigate to flight details for different date', async ({ page }) => {
|
|
||||||
const flight = generateFlight({ direction: 'departure', cityCode: 'MOW', date: tomorrow });
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${tomorrow.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(new RegExp(`/${slug}`));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Flight Information', () => {
|
|
||||||
test('should display flight number', async ({ page }) => {
|
|
||||||
const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightNumber = page.locator('[data-testid="flight-number"]');
|
|
||||||
await expect(flightNumber).toBeVisible();
|
|
||||||
await expect(flightNumber).toContainText(flight.flightNumber);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display airline name', async ({ page }) => {
|
|
||||||
const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const airlineName = page.locator('[data-testid="airline-name"]');
|
|
||||||
await expect(airlineName).toBeVisible();
|
|
||||||
await expect(airlineName).toContainText(flight.airlineName);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display aircraft type', async ({ page }) => {
|
|
||||||
const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const aircraftType = page.locator('[data-testid="aircraft-type"]');
|
|
||||||
await expect(aircraftType).toBeVisible();
|
|
||||||
await expect(aircraftType).toContainText(flight.aircraftType || '');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display departure city', async ({ page }) => {
|
|
||||||
const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const departureCity = page.locator('[data-testid="departure-city"]');
|
|
||||||
await expect(departureCity).toBeVisible();
|
|
||||||
await expect(departureCity).toContainText(flight.departure.cityName);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display arrival city', async ({ page }) => {
|
|
||||||
const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const arrivalCity = page.locator('[data-testid="arrival-city"]');
|
|
||||||
await expect(arrivalCity).toBeVisible();
|
|
||||||
await expect(arrivalCity).toContainText(flight.arrival.cityName);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display scheduled departure time', async ({ page }) => {
|
|
||||||
const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const depTime = page.locator('[data-testid="scheduled-departure-time"]');
|
|
||||||
await expect(depTime).toBeVisible();
|
|
||||||
|
|
||||||
const depTimeText = flight.departure.time.scheduled.slice(11, 16);
|
|
||||||
await expect(depTime).toContainText(depTimeText);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display scheduled arrival time', async ({ page }) => {
|
|
||||||
const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const arrTime = page.locator('[data-testid="scheduled-arrival-time"]');
|
|
||||||
await expect(arrTime).toBeVisible();
|
|
||||||
|
|
||||||
const arrTimeText = flight.arrival.time.scheduled.slice(11, 16);
|
|
||||||
await expect(arrTime).toContainText(arrTimeText);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display flight duration', async ({ page }) => {
|
|
||||||
const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const duration = page.locator('[data-testid="flight-duration"]');
|
|
||||||
await expect(duration).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display flight status', async ({ page }) => {
|
|
||||||
const flight = generateFlight({
|
|
||||||
direction: 'departure',
|
|
||||||
cityCode: 'MOW',
|
|
||||||
status: 'scheduled',
|
|
||||||
});
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const status = page.locator('[data-testid="flight-status"]');
|
|
||||||
await expect(status).toBeVisible();
|
|
||||||
await expect(status).toContainText(flight.status);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Flight Details', () => {
|
|
||||||
test('should display terminal information', async ({ page }) => {
|
|
||||||
const flight = generateFlight({
|
|
||||||
direction: 'departure',
|
|
||||||
cityCode: 'MOW',
|
|
||||||
departure: { terminal: 'B' },
|
|
||||||
});
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const terminal = page.locator('[data-testid="terminal"]');
|
|
||||||
await expect(terminal).toBeVisible();
|
|
||||||
await expect(terminal).toContainText('B');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display boarding gate information', async ({ page }) => {
|
|
||||||
const flight = generateFlight({
|
|
||||||
direction: 'departure',
|
|
||||||
cityCode: 'MOW',
|
|
||||||
status: 'boarding',
|
|
||||||
boarding: { gate: '11', status: 'Идёт посадка' },
|
|
||||||
});
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const boardingGate = page.locator('[data-testid="boarding-gate"]');
|
|
||||||
await expect(boardingGate).toBeVisible();
|
|
||||||
await expect(boardingGate).toContainText('11');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display baggage belt information', async ({ page }) => {
|
|
||||||
const flight = generateFlight({
|
|
||||||
direction: 'arrival',
|
|
||||||
cityCode: 'MOW',
|
|
||||||
status: 'arrived',
|
|
||||||
arrivalInfo: { baggageBelt: '5', transfer: 'Тран' },
|
|
||||||
});
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const baggageBelt = page.locator('[data-testid="baggage-belt"]');
|
|
||||||
await expect(baggageBelt).toBeVisible();
|
|
||||||
await expect(baggageBelt).toContainText('5');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display check-in information', async ({ page }) => {
|
|
||||||
const flight = generateFlight({
|
|
||||||
direction: 'departure',
|
|
||||||
cityCode: 'MOW',
|
|
||||||
status: 'checkin',
|
|
||||||
checkin: { status: 'В процессе' },
|
|
||||||
});
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const checkinInfo = page.locator('[data-testid="checkin-info"]');
|
|
||||||
await expect(checkinInfo).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display deplaning information', async ({ page }) => {
|
|
||||||
const flight = generateFlight({
|
|
||||||
direction: 'arrival',
|
|
||||||
cityCode: 'MOW',
|
|
||||||
status: 'arrived',
|
|
||||||
deplaning: { status: 'В процессе', transfer: 'Трап' },
|
|
||||||
});
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const deplaningInfo = page.locator('[data-testid="deplaning-info"]');
|
|
||||||
await expect(deplaningInfo).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Aircraft Information', () => {
|
|
||||||
test('should display aircraft type', async ({ page }) => {
|
|
||||||
const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const aircraftType = page.locator('[data-testid="aircraft-type"]');
|
|
||||||
await expect(aircraftType).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display aircraft name', async ({ page }) => {
|
|
||||||
const flight = generateFlight({
|
|
||||||
direction: 'departure',
|
|
||||||
cityCode: 'MOW',
|
|
||||||
aircraft: { type: 'Airbus A320', name: 'В. Высоцкий' },
|
|
||||||
});
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const aircraftName = page.locator('[data-testid="aircraft-name"]');
|
|
||||||
await expect(aircraftName).toBeVisible();
|
|
||||||
await expect(aircraftName).toContainText('В. Высоцкий');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display seat configuration', async ({ page }) => {
|
|
||||||
const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const seatInfo = page.locator('[data-testid="seat-info"]');
|
|
||||||
await expect(seatInfo).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Schedule Information', () => {
|
|
||||||
test('should display scheduled departure', async ({ page }) => {
|
|
||||||
const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const scheduledDep = page.locator('[data-testid="scheduled-departure"]');
|
|
||||||
await expect(scheduledDep).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display scheduled arrival', async ({ page }) => {
|
|
||||||
const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const scheduledArr = page.locator('[data-testid="scheduled-arrival"]');
|
|
||||||
await expect(scheduledArr).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display actual departure when available', async ({ page }) => {
|
|
||||||
const flight = generateFlight({
|
|
||||||
direction: 'departure',
|
|
||||||
cityCode: 'MOW',
|
|
||||||
status: 'departed',
|
|
||||||
});
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const actualDep = page.locator('[data-testid="actual-departure"]');
|
|
||||||
await expect(actualDep).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display actual arrival when available', async ({ page }) => {
|
|
||||||
const flight = generateFlight({
|
|
||||||
direction: 'arrival',
|
|
||||||
cityCode: 'MOW',
|
|
||||||
status: 'arrived',
|
|
||||||
});
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const actualArr = page.locator('[data-testid="actual-arrival"]');
|
|
||||||
await expect(actualArr).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display expected arrival when delayed', async ({ page }) => {
|
|
||||||
const flight = generateFlight({
|
|
||||||
direction: 'arrival',
|
|
||||||
cityCode: 'MOW',
|
|
||||||
status: 'delayed',
|
|
||||||
});
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const expectedArr = page.locator('[data-testid="expected-arrival"]');
|
|
||||||
await expect(expectedArr).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Error Handling', () => {
|
|
||||||
test('should handle invalid flight number', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/SU9999-${today.replace(/-/g, '')}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const errorState = page.locator('[data-testid="error-state"]');
|
|
||||||
await expect(errorState).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should handle flight not found', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/SU9999-20260406`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const notFound = page.locator('[data-testid="not-found"]');
|
|
||||||
await expect(notFound).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should handle network error', async ({ page }) => {
|
|
||||||
await page.route('**/api/flights/**', (route) => {
|
|
||||||
return route.abort('internetdisconnected');
|
|
||||||
});
|
|
||||||
|
|
||||||
const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const networkError = page.locator('[data-testid="network-error"]');
|
|
||||||
await expect(networkError).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Navigation', () => {
|
|
||||||
test('should navigate back to board', async ({ page }) => {
|
|
||||||
const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const backLink = page.locator('[data-testid="back-link"]');
|
|
||||||
await backLink.click();
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/onlineboard\/departure\/MOW-\d{8}/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should navigate to related flights', async ({ page }) => {
|
|
||||||
const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const relatedFlights = page.locator('[data-testid="related-flights"]');
|
|
||||||
await expect(relatedFlights).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Accessibility', () => {
|
|
||||||
test('should have proper ARIA labels', async ({ page }) => {
|
|
||||||
const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const pageContent = page.locator('[data-testid="page-content"]');
|
|
||||||
await expect(pageContent).toHaveAttribute('role', 'main');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should be keyboard navigable', async ({ page }) => {
|
|
||||||
const flight = generateFlight({ direction: 'departure', cityCode: 'MOW' });
|
|
||||||
const slug = `${flight.flightNumber.replace(/\s+/g, '')}-${today.replace(/-/g, '')}`;
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/${slug}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await page.keyboard.press('Tab');
|
|
||||||
await page.keyboard.press('Tab');
|
|
||||||
await page.keyboard.press('Tab');
|
|
||||||
|
|
||||||
const focusedElement = page.locator(':focus');
|
|
||||||
await expect(focusedElement).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,310 +0,0 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
|
||||||
import type { Page } from '@playwright/test';
|
|
||||||
import {
|
|
||||||
buildOnlineBoardPath,
|
|
||||||
buildRouteParam,
|
|
||||||
searchFlightByRoute,
|
|
||||||
verifyFlightCard,
|
|
||||||
generateFlight,
|
|
||||||
generateFlights,
|
|
||||||
getToday,
|
|
||||||
getTomorrow,
|
|
||||||
CITIES,
|
|
||||||
} from '../support/test-utilities';
|
|
||||||
|
|
||||||
const today = getToday();
|
|
||||||
const tomorrow = getTomorrow();
|
|
||||||
const dateParam = buildRouteParam('MOW', today);
|
|
||||||
const tomorrowParam = buildRouteParam('MOW', tomorrow);
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Online Board - Route Tests
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
test.describe('Online Board - Route', () => {
|
|
||||||
test.describe('Page Navigation', () => {
|
|
||||||
test('should navigate to route board for Moscow to Sochi', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/route/MOW-AER-${today.replace(/-/g, '')}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/route\/MOW-AER-\d{8}/);
|
|
||||||
await expect(page).toHaveTitle(/Москва - Сочи/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should navigate to route board for Saint Petersburg to Moscow', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/route/LED-MOW-${today.replace(/-/g, '')}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/route\/LED-MOW-\d{8}/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should navigate to route board for Novosibirsk to Moscow', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/route/OVB-MOW-${today.replace(/-/g, '')}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/route\/OVB-MOW-\d{8}/);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Route Search', () => {
|
|
||||||
test('should search route by departure and arrival cities', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByRoute(page, 'Moscow', 'Sochi');
|
|
||||||
|
|
||||||
const searchResults = page.locator('[data-testid="flight-card"]');
|
|
||||||
await expect(searchResults).toHaveCount(20);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should show no results when route not found', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByRoute(page, 'Moscow', 'Unknown City');
|
|
||||||
|
|
||||||
const noResults = page.locator('[data-testid="no-results"]');
|
|
||||||
await expect(noResults).toBeVisible();
|
|
||||||
await expect(noResults).toContainText('Нет результатов');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should validate departure city', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByRoute(page, '', 'Sochi');
|
|
||||||
|
|
||||||
const error = page.locator('[data-testid="validation-error"]');
|
|
||||||
await expect(error).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should validate arrival city', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildOnlineBoardPath('departure', 'MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await searchFlightByRoute(page, 'Moscow', '');
|
|
||||||
|
|
||||||
const error = page.locator('[data-testid="validation-error"]');
|
|
||||||
await expect(error).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Flight Display', () => {
|
|
||||||
test('should display flights for selected route', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/route/MOW-AER-${today.replace(/-/g, '')}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCards = page.locator('[data-testid="flight-card"]');
|
|
||||||
await expect(flightCards).toHaveCount(20);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display route information', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/route/MOW-AER-${today.replace(/-/g, '')}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const routeInfo = page.locator('[data-testid="route-info"]');
|
|
||||||
await expect(routeInfo).toBeVisible();
|
|
||||||
|
|
||||||
await expect(routeInfo).toContainText('Москва');
|
|
||||||
await expect(routeInfo).toContainText('Сочи');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display flight count', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/route/MOW-AER-${today.replace(/-/g, '')}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCount = page.locator('[data-testid="flight-count"]');
|
|
||||||
await expect(flightCount).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Date Navigation', () => {
|
|
||||||
test('should navigate to tomorrow', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/route/MOW-AER-${today.replace(/-/g, '')}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const dateTab = page.locator(`[data-testid="date-tab-${tomorrow.replace(/-/g, '')}"]`);
|
|
||||||
if ((await dateTab.count()) > 0) {
|
|
||||||
await dateTab.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/route\/MOW-AER-\d{8}/);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display correct date in title', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/route/MOW-AER-${today.replace(/-/g, '')}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const dateText = page.locator('[data-testid="board-date"]');
|
|
||||||
await expect(dateText).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Filtering', () => {
|
|
||||||
test('should filter by status', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/route/MOW-AER-${today.replace(/-/g, '')}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const statusFilter = page.locator('[data-testid="status-filter"]');
|
|
||||||
await statusFilter.click();
|
|
||||||
|
|
||||||
const scheduledOption = page.locator('[data-testid="filter-option-scheduled"]');
|
|
||||||
await scheduledOption.click();
|
|
||||||
|
|
||||||
const filteredFlights = page.locator('[data-testid="flight-card"]');
|
|
||||||
await expect(filteredFlights).toHaveCount(20);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should filter by airline', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/route/MOW-AER-${today.replace(/-/g, '')}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const airlineFilter = page.locator('[data-testid="airline-filter"]');
|
|
||||||
await airlineFilter.click();
|
|
||||||
|
|
||||||
const aeroflotOption = page.locator('[data-testid="filter-option-SU"]');
|
|
||||||
await aeroflotOption.click();
|
|
||||||
|
|
||||||
const filteredFlights = page.locator('[data-testid="flight-card"]');
|
|
||||||
await expect(filteredFlights).toHaveCount(20);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should filter by time range', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/route/MOW-AER-${today.replace(/-/g, '')}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const timeFilter = page.locator('[data-testid="time-filter"]');
|
|
||||||
await timeFilter.click();
|
|
||||||
|
|
||||||
const timeOption = page.locator('[data-testid="filter-option-morning"]');
|
|
||||||
await timeOption.click();
|
|
||||||
|
|
||||||
const filteredFlights = page.locator('[data-testid="flight-card"]');
|
|
||||||
await expect(filteredFlights).toHaveCount(20);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Flight Card', () => {
|
|
||||||
test('should display flight number', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/route/MOW-AER-${today.replace(/-/g, '')}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toBeVisible();
|
|
||||||
|
|
||||||
const flightNumber = flightCard.locator('[data-testid="flight-number"]');
|
|
||||||
await expect(flightNumber).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display airline name', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/route/MOW-AER-${today.replace(/-/g, '')}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toBeVisible();
|
|
||||||
|
|
||||||
const airlineName = flightCard.locator('[data-testid="airline-name"]');
|
|
||||||
await expect(airlineName).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display departure city', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/route/MOW-AER-${today.replace(/-/g, '')}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toBeVisible();
|
|
||||||
|
|
||||||
const departureCity = flightCard.locator('[data-testid="departure-city"]');
|
|
||||||
await expect(departureCity).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display arrival city', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/route/MOW-AER-${today.replace(/-/g, '')}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toBeVisible();
|
|
||||||
|
|
||||||
const arrivalCity = flightCard.locator('[data-testid="arrival-city"]');
|
|
||||||
await expect(arrivalCity).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display scheduled departure time', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/route/MOW-AER-${today.replace(/-/g, '')}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toBeVisible();
|
|
||||||
|
|
||||||
const depTime = flightCard.locator('[data-testid="scheduled-departure-time"]');
|
|
||||||
await expect(depTime).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display scheduled arrival time', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/route/MOW-AER-${today.replace(/-/g, '')}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toBeVisible();
|
|
||||||
|
|
||||||
const arrTime = flightCard.locator('[data-testid="scheduled-arrival-time"]');
|
|
||||||
await expect(arrTime).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display flight duration', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/route/MOW-AER-${today.replace(/-/g, '')}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toBeVisible();
|
|
||||||
|
|
||||||
const duration = flightCard.locator('[data-testid="flight-duration"]');
|
|
||||||
await expect(duration).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Error Handling', () => {
|
|
||||||
test('should handle invalid route parameters', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/route/XXX-YYY-${today.replace(/-/g, '')}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const errorState = page.locator('[data-testid="error-state"]');
|
|
||||||
await expect(errorState).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should handle network error', async ({ page }) => {
|
|
||||||
await page.route('**/api/flights/**', (route) => {
|
|
||||||
return route.abort('internetdisconnected');
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/onlineboard/route/MOW-AER-${today.replace(/-/g, '')}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const networkError = page.locator('[data-testid="network-error"]');
|
|
||||||
await expect(networkError).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Accessibility', () => {
|
|
||||||
test('should have proper ARIA labels', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/route/MOW-AER-${today.replace(/-/g, '')}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const flightCard = page.locator('[data-testid="flight-card"]').first();
|
|
||||||
await expect(flightCard).toHaveAttribute('role', 'article');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should be keyboard navigable', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/route/MOW-AER-${today.replace(/-/g, '')}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await page.keyboard.press('Tab');
|
|
||||||
await page.keyboard.press('Tab');
|
|
||||||
await page.keyboard.press('Tab');
|
|
||||||
|
|
||||||
const focusedElement = page.locator(':focus');
|
|
||||||
await expect(focusedElement).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,301 +0,0 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
|
||||||
import type { Page } from '@playwright/test';
|
|
||||||
import {
|
|
||||||
buildRouteParam,
|
|
||||||
generateDestination,
|
|
||||||
generateDestinations,
|
|
||||||
getToday,
|
|
||||||
getTomorrow,
|
|
||||||
CITIES,
|
|
||||||
} from '@e2e/support/test-utilities';
|
|
||||||
|
|
||||||
const today = getToday();
|
|
||||||
const tomorrow = getTomorrow();
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Popular Requests Tests
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
test.describe('Popular Requests', () => {
|
|
||||||
test.describe('Page Navigation', () => {
|
|
||||||
test('should display popular requests section', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/departure/MOW-${buildRouteParam('MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const popularRequests = page.locator('[data-testid="popular-requests"]');
|
|
||||||
await expect(popularRequests).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display popular requests title', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/departure/MOW-${buildRouteParam('MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const title = page.locator('[data-testid="popular-requests-title"]');
|
|
||||||
await expect(title).toBeVisible();
|
|
||||||
await expect(title).toContainText('Популярные направления');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Request Display', () => {
|
|
||||||
test('should display departure city', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/departure/MOW-${buildRouteParam('MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const request = page.locator('[data-testid="popular-request"]').first();
|
|
||||||
await expect(request).toBeVisible();
|
|
||||||
|
|
||||||
const departureCity = request.locator('[data-testid="request-departure-city"]');
|
|
||||||
await expect(departureCity).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display arrival city', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/departure/MOW-${buildRouteParam('MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const request = page.locator('[data-testid="popular-request"]').first();
|
|
||||||
await expect(request).toBeVisible();
|
|
||||||
|
|
||||||
const arrivalCity = request.locator('[data-testid="request-arrival-city"]');
|
|
||||||
await expect(arrivalCity).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display flight count', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/departure/MOW-${buildRouteParam('MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const request = page.locator('[data-testid="popular-request"]').first();
|
|
||||||
await expect(request).toBeVisible();
|
|
||||||
|
|
||||||
const flightCount = request.locator('[data-testid="request-flight-count"]');
|
|
||||||
await expect(flightCount).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display date range', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/departure/MOW-${buildRouteParam('MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const request = page.locator('[data-testid="popular-request"]').first();
|
|
||||||
await expect(request).toBeVisible();
|
|
||||||
|
|
||||||
const dateRange = request.locator('[data-testid="request-date-range"]');
|
|
||||||
await expect(dateRange).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Request Interaction', () => {
|
|
||||||
test('should navigate to flight board on click', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/departure/MOW-${buildRouteParam('MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const request = page.locator('[data-testid="popular-request"]').first();
|
|
||||||
await request.click();
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/onlineboard\/departure\/MOW-\d{8}/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should navigate to flight board with correct city', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/departure/MOW-${buildRouteParam('MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const request = page.locator('[data-testid="popular-request"]').first();
|
|
||||||
await request.click();
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/onlineboard\/departure\/[A-Z]{3}-\d{8}/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should open flight board for arrival direction', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/departure/MOW-${buildRouteParam('MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const request = page.locator('[data-testid="popular-request"]').first();
|
|
||||||
await request.click();
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/onlineboard\/arrival\/[A-Z]{3}-\d{8}/);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Request Sorting', () => {
|
|
||||||
test('should sort by flight count', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/departure/MOW-${buildRouteParam('MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const sortButton = page.locator('[data-testid="sort-button"]');
|
|
||||||
await sortButton.click();
|
|
||||||
|
|
||||||
const sortOption = page.locator('[data-testid="sort-option-flight-count"]');
|
|
||||||
await sortOption.click();
|
|
||||||
|
|
||||||
const requests = page.locator('[data-testid="popular-request"]');
|
|
||||||
const count1 = await requests
|
|
||||||
.nth(0)
|
|
||||||
.locator('[data-testid="request-flight-count"]')
|
|
||||||
.textContent();
|
|
||||||
const count2 = await requests
|
|
||||||
.nth(1)
|
|
||||||
.locator('[data-testid="request-flight-count"]')
|
|
||||||
.textContent();
|
|
||||||
|
|
||||||
if (count1 && count2) {
|
|
||||||
expect(parseInt(count1.replace(/\D/g, ''))).toBeGreaterThanOrEqual(
|
|
||||||
parseInt(count2.replace(/\D/g, '')),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should sort by date', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/departure/MOW-${buildRouteParam('MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const sortButton = page.locator('[data-testid="sort-button"]');
|
|
||||||
await sortButton.click();
|
|
||||||
|
|
||||||
const sortOption = page.locator('[data-testid="sort-option-date"]');
|
|
||||||
await sortOption.click();
|
|
||||||
|
|
||||||
const requests = page.locator('[data-testid="popular-request"]');
|
|
||||||
await expect(requests).toHaveCount(20);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Request Filtering', () => {
|
|
||||||
test('should filter by departure city', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/departure/MOW-${buildRouteParam('MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const filterInput = page.locator('[data-testid="filter-input"]');
|
|
||||||
await filterInput.fill('Moscow');
|
|
||||||
|
|
||||||
const requests = page.locator('[data-testid="popular-request"]');
|
|
||||||
await expect(requests).toHaveCount(20);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should clear filter', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/departure/MOW-${buildRouteParam('MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const filterInput = page.locator('[data-testid="filter-input"]');
|
|
||||||
await filterInput.fill('Moscow');
|
|
||||||
|
|
||||||
const clearButton = page.locator('[data-testid="clear-filter"]');
|
|
||||||
await clearButton.click();
|
|
||||||
|
|
||||||
const requests = page.locator('[data-testid="popular-request"]');
|
|
||||||
await expect(requests).toHaveCount(20);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Request Pagination', () => {
|
|
||||||
test('should display pagination controls', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/departure/MOW-${buildRouteParam('MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const pagination = page.locator('[data-testid="pagination"]');
|
|
||||||
await expect(pagination).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should navigate to next page', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/departure/MOW-${buildRouteParam('MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const nextButton = page.locator('[data-testid="pagination-next"]');
|
|
||||||
await nextButton.click();
|
|
||||||
|
|
||||||
const requests = page.locator('[data-testid="popular-request"]');
|
|
||||||
await expect(requests).toHaveCount(20);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should navigate to previous page', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/departure/MOW-${buildRouteParam('MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const nextButton = page.locator('[data-testid="pagination-next"]');
|
|
||||||
await nextButton.click();
|
|
||||||
|
|
||||||
const prevButton = page.locator('[data-testid="pagination-prev"]');
|
|
||||||
await prevButton.click();
|
|
||||||
|
|
||||||
const requests = page.locator('[data-testid="popular-request"]');
|
|
||||||
await expect(requests).toHaveCount(20);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Error Handling', () => {
|
|
||||||
test('should handle network error', async ({ page }) => {
|
|
||||||
await page.route('**/api/popular-requests/**', (route) => {
|
|
||||||
return route.abort('internetdisconnected');
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/onlineboard/departure/MOW-${buildRouteParam('MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const networkError = page.locator('[data-testid="network-error"]');
|
|
||||||
await expect(networkError).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should handle empty results', async ({ page }) => {
|
|
||||||
await page.route('**/api/popular-requests/**', (route) => {
|
|
||||||
return route.fulfill({
|
|
||||||
status: 200,
|
|
||||||
json: { requests: [], total: 0 },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru/onlineboard/departure/MOW-${buildRouteParam('MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const noResults = page.locator('[data-testid="no-results"]');
|
|
||||||
await expect(noResults).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Accessibility', () => {
|
|
||||||
test('should have proper ARIA labels', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/departure/MOW-${buildRouteParam('MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const popularRequests = page.locator('[data-testid="popular-requests"]');
|
|
||||||
await expect(popularRequests).toHaveAttribute('role', 'region');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should be keyboard navigable', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/departure/MOW-${buildRouteParam('MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await page.keyboard.press('Tab');
|
|
||||||
await page.keyboard.press('Tab');
|
|
||||||
await page.keyboard.press('Tab');
|
|
||||||
|
|
||||||
const focusedElement = page.locator(':focus');
|
|
||||||
await expect(focusedElement).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Responsive Design', () => {
|
|
||||||
test('should be responsive on mobile', async ({ page }) => {
|
|
||||||
await page.setViewportSize({ width: 375, height: 667 });
|
|
||||||
await page.goto(`/ru-ru/onlineboard/departure/MOW-${buildRouteParam('MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const popularRequests = page.locator('[data-testid="popular-requests"]');
|
|
||||||
await expect(popularRequests).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should be responsive on tablet', async ({ page }) => {
|
|
||||||
await page.setViewportSize({ width: 768, height: 1024 });
|
|
||||||
await page.goto(`/ru-ru/onlineboard/departure/MOW-${buildRouteParam('MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const popularRequests = page.locator('[data-testid="popular-requests"]');
|
|
||||||
await expect(popularRequests).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should be responsive on desktop', async ({ page }) => {
|
|
||||||
await page.setViewportSize({ width: 1920, height: 1080 });
|
|
||||||
await page.goto(`/ru-ru/onlineboard/departure/MOW-${buildRouteParam('MOW', today)}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const popularRequests = page.locator('[data-testid="popular-requests"]');
|
|
||||||
await expect(popularRequests).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,427 +0,0 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
|
||||||
import type { Page } from '@playwright/test';
|
|
||||||
import {
|
|
||||||
buildSchedulePath,
|
|
||||||
buildRouteParam,
|
|
||||||
generateScheduleEntry,
|
|
||||||
generateScheduleEntries,
|
|
||||||
getToday,
|
|
||||||
getTomorrow,
|
|
||||||
CITIES,
|
|
||||||
} from '../support/test-utilities';
|
|
||||||
|
|
||||||
const today = getToday();
|
|
||||||
const tomorrow = getTomorrow();
|
|
||||||
const dateFrom = today;
|
|
||||||
const dateTo = tomorrow;
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Schedule Search Tests
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
test.describe('Schedule Search', () => {
|
|
||||||
test.describe('Page Navigation', () => {
|
|
||||||
test('should navigate to schedule page', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildSchedulePath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/schedule/);
|
|
||||||
await expect(page).toHaveTitle(/Расписание/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should navigate to schedule with pre-filled search', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/schedule?from=MOW&to=AER&dateFrom=${dateFrom}&dateTo=${dateTo}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/schedule/);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Search Form', () => {
|
|
||||||
test('should display search form', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildSchedulePath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const form = page.locator('[data-testid="schedule-search-form"]');
|
|
||||||
await expect(form).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display departure city input', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildSchedulePath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const departureInput = page.locator('[data-testid="departure-city-input"]');
|
|
||||||
await expect(departureInput).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display arrival city input', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildSchedulePath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const arrivalInput = page.locator('[data-testid="arrival-city-input"]');
|
|
||||||
await expect(arrivalInput).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display date range inputs', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildSchedulePath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const dateFromInput = page.locator('[data-testid="date-from-input"]');
|
|
||||||
await expect(dateFromInput).toBeVisible();
|
|
||||||
|
|
||||||
const dateToInput = page.locator('[data-testid="date-to-input"]');
|
|
||||||
await expect(dateToInput).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display search button', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildSchedulePath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const searchButton = page.locator('[data-testid="search-button"]');
|
|
||||||
await expect(searchButton).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Search Functionality', () => {
|
|
||||||
test('should search by departure and arrival cities', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildSchedulePath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const departureInput = page.locator('[data-testid="departure-city-input"]');
|
|
||||||
await departureInput.fill('Moscow');
|
|
||||||
|
|
||||||
const arrivalInput = page.locator('[data-testid="arrival-city-input"]');
|
|
||||||
await arrivalInput.fill('Sochi');
|
|
||||||
|
|
||||||
const searchButton = page.locator('[data-testid="search-button"]');
|
|
||||||
await searchButton.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const results = page.locator('[data-testid="schedule-entry"]');
|
|
||||||
await expect(results).toHaveCount(50);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should search with date range', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildSchedulePath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const departureInput = page.locator('[data-testid="departure-city-input"]');
|
|
||||||
await departureInput.fill('Moscow');
|
|
||||||
|
|
||||||
const arrivalInput = page.locator('[data-testid="arrival-city-input"]');
|
|
||||||
await arrivalInput.fill('Sochi');
|
|
||||||
|
|
||||||
const dateFromInput = page.locator('[data-testid="date-from-input"]');
|
|
||||||
await dateFromInput.fill(dateFrom);
|
|
||||||
|
|
||||||
const dateToInput = page.locator('[data-testid="date-to-input"]');
|
|
||||||
await dateToInput.fill(dateTo);
|
|
||||||
|
|
||||||
const searchButton = page.locator('[data-testid="search-button"]');
|
|
||||||
await searchButton.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const results = page.locator('[data-testid="schedule-entry"]');
|
|
||||||
await expect(results).toHaveCount(50);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should show validation error for missing departure city', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildSchedulePath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const arrivalInput = page.locator('[data-testid="arrival-city-input"]');
|
|
||||||
await arrivalInput.fill('Sochi');
|
|
||||||
|
|
||||||
const searchButton = page.locator('[data-testid="search-button"]');
|
|
||||||
await searchButton.click();
|
|
||||||
|
|
||||||
const error = page.locator('[data-testid="validation-error"]');
|
|
||||||
await expect(error).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should show validation error for missing arrival city', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildSchedulePath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const departureInput = page.locator('[data-testid="departure-city-input"]');
|
|
||||||
await departureInput.fill('Moscow');
|
|
||||||
|
|
||||||
const searchButton = page.locator('[data-testid="search-button"]');
|
|
||||||
await searchButton.click();
|
|
||||||
|
|
||||||
const error = page.locator('[data-testid="validation-error"]');
|
|
||||||
await expect(error).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should show no results when no schedules found', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildSchedulePath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const departureInput = page.locator('[data-testid="departure-city-input"]');
|
|
||||||
await departureInput.fill('Unknown City');
|
|
||||||
|
|
||||||
const arrivalInput = page.locator('[data-testid="arrival-city-input"]');
|
|
||||||
await arrivalInput.fill('Unknown City');
|
|
||||||
|
|
||||||
const searchButton = page.locator('[data-testid="search-button"]');
|
|
||||||
await searchButton.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const noResults = page.locator('[data-testid="no-results"]');
|
|
||||||
await expect(noResults).toBeVisible();
|
|
||||||
await expect(noResults).toContainText('Нет результатов');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Schedule Entry Display', () => {
|
|
||||||
test('should display flight number', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildSchedulePath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const departureInput = page.locator('[data-testid="departure-city-input"]');
|
|
||||||
await departureInput.fill('Moscow');
|
|
||||||
|
|
||||||
const arrivalInput = page.locator('[data-testid="arrival-city-input"]');
|
|
||||||
await arrivalInput.fill('Sochi');
|
|
||||||
|
|
||||||
const searchButton = page.locator('[data-testid="search-button"]');
|
|
||||||
await searchButton.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const entry = page.locator('[data-testid="schedule-entry"]').first();
|
|
||||||
await expect(entry).toBeVisible();
|
|
||||||
|
|
||||||
const flightNumber = entry.locator('[data-testid="flight-number"]');
|
|
||||||
await expect(flightNumber).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display airline name', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildSchedulePath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const departureInput = page.locator('[data-testid="departure-city-input"]');
|
|
||||||
await departureInput.fill('Moscow');
|
|
||||||
|
|
||||||
const arrivalInput = page.locator('[data-testid="arrival-city-input"]');
|
|
||||||
await arrivalInput.fill('Sochi');
|
|
||||||
|
|
||||||
const searchButton = page.locator('[data-testid="search-button"]');
|
|
||||||
await searchButton.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const entry = page.locator('[data-testid="schedule-entry"]').first();
|
|
||||||
await expect(entry).toBeVisible();
|
|
||||||
|
|
||||||
const airlineName = entry.locator('[data-testid="airline-name"]');
|
|
||||||
await expect(airlineName).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display aircraft type', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildSchedulePath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const departureInput = page.locator('[data-testid="departure-city-input"]');
|
|
||||||
await departureInput.fill('Moscow');
|
|
||||||
|
|
||||||
const arrivalInput = page.locator('[data-testid="arrival-city-input"]');
|
|
||||||
await arrivalInput.fill('Sochi');
|
|
||||||
|
|
||||||
const searchButton = page.locator('[data-testid="search-button"]');
|
|
||||||
await searchButton.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const entry = page.locator('[data-testid="schedule-entry"]').first();
|
|
||||||
await expect(entry).toBeVisible();
|
|
||||||
|
|
||||||
const aircraftType = entry.locator('[data-testid="aircraft-type"]');
|
|
||||||
await expect(aircraftType).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display departure time', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildSchedulePath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const departureInput = page.locator('[data-testid="departure-city-input"]');
|
|
||||||
await departureInput.fill('Moscow');
|
|
||||||
|
|
||||||
const arrivalInput = page.locator('[data-testid="arrival-city-input"]');
|
|
||||||
await arrivalInput.fill('Sochi');
|
|
||||||
|
|
||||||
const searchButton = page.locator('[data-testid="search-button"]');
|
|
||||||
await searchButton.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const entry = page.locator('[data-testid="schedule-entry"]').first();
|
|
||||||
await expect(entry).toBeVisible();
|
|
||||||
|
|
||||||
const departureTime = entry.locator('[data-testid="departure-time"]');
|
|
||||||
await expect(departureTime).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display arrival time', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildSchedulePath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const departureInput = page.locator('[data-testid="departure-city-input"]');
|
|
||||||
await departureInput.fill('Moscow');
|
|
||||||
|
|
||||||
const arrivalInput = page.locator('[data-testid="arrival-city-input"]');
|
|
||||||
await arrivalInput.fill('Sochi');
|
|
||||||
|
|
||||||
const searchButton = page.locator('[data-testid="search-button"]');
|
|
||||||
await searchButton.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const entry = page.locator('[data-testid="schedule-entry"]').first();
|
|
||||||
await expect(entry).toBeVisible();
|
|
||||||
|
|
||||||
const arrivalTime = entry.locator('[data-testid="arrival-time"]');
|
|
||||||
await expect(arrivalTime).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display days of week', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildSchedulePath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const departureInput = page.locator('[data-testid="departure-city-input"]');
|
|
||||||
await departureInput.fill('Moscow');
|
|
||||||
|
|
||||||
const arrivalInput = page.locator('[data-testid="arrival-city-input"]');
|
|
||||||
await arrivalInput.fill('Sochi');
|
|
||||||
|
|
||||||
const searchButton = page.locator('[data-testid="search-button"]');
|
|
||||||
await searchButton.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const entry = page.locator('[data-testid="schedule-entry"]').first();
|
|
||||||
await expect(entry).toBeVisible();
|
|
||||||
|
|
||||||
const daysOfWeek = entry.locator('[data-testid="days-of-week"]');
|
|
||||||
await expect(daysOfWeek).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display effective date range', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildSchedulePath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const departureInput = page.locator('[data-testid="departure-city-input"]');
|
|
||||||
await departureInput.fill('Moscow');
|
|
||||||
|
|
||||||
const arrivalInput = page.locator('[data-testid="arrival-city-input"]');
|
|
||||||
await arrivalInput.fill('Sochi');
|
|
||||||
|
|
||||||
const searchButton = page.locator('[data-testid="search-button"]');
|
|
||||||
await searchButton.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const entry = page.locator('[data-testid="schedule-entry"]').first();
|
|
||||||
await expect(entry).toBeVisible();
|
|
||||||
|
|
||||||
const dateRange = entry.locator('[data-testid="date-range"]');
|
|
||||||
await expect(dateRange).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Filtering', () => {
|
|
||||||
test('should filter by direct flights only', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildSchedulePath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const directFilter = page.locator('[data-testid="direct-filter"]');
|
|
||||||
await directFilter.click();
|
|
||||||
|
|
||||||
const departureInput = page.locator('[data-testid="departure-city-input"]');
|
|
||||||
await departureInput.fill('Moscow');
|
|
||||||
|
|
||||||
const arrivalInput = page.locator('[data-testid="arrival-city-input"]');
|
|
||||||
await arrivalInput.fill('Sochi');
|
|
||||||
|
|
||||||
const searchButton = page.locator('[data-testid="search-button"]');
|
|
||||||
await searchButton.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const entries = page.locator('[data-testid="schedule-entry"]');
|
|
||||||
await expect(entries).toHaveCount(50);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should filter by airline', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildSchedulePath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const airlineFilter = page.locator('[data-testid="airline-filter"]');
|
|
||||||
await airlineFilter.click();
|
|
||||||
|
|
||||||
const aeroflotOption = page.locator('[data-testid="filter-option-SU"]');
|
|
||||||
await aeroflotOption.click();
|
|
||||||
|
|
||||||
const departureInput = page.locator('[data-testid="departure-city-input"]');
|
|
||||||
await departureInput.fill('Moscow');
|
|
||||||
|
|
||||||
const arrivalInput = page.locator('[data-testid="arrival-city-input"]');
|
|
||||||
await arrivalInput.fill('Sochi');
|
|
||||||
|
|
||||||
const searchButton = page.locator('[data-testid="search-button"]');
|
|
||||||
await searchButton.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const entries = page.locator('[data-testid="schedule-entry"]');
|
|
||||||
await expect(entries).toHaveCount(50);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Error Handling', () => {
|
|
||||||
test('should handle invalid date format', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildSchedulePath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const departureInput = page.locator('[data-testid="departure-city-input"]');
|
|
||||||
await departureInput.fill('Moscow');
|
|
||||||
|
|
||||||
const arrivalInput = page.locator('[data-testid="arrival-city-input"]');
|
|
||||||
await arrivalInput.fill('Sochi');
|
|
||||||
|
|
||||||
const dateFromInput = page.locator('[data-testid="date-from-input"]');
|
|
||||||
await dateFromInput.fill('invalid-date');
|
|
||||||
|
|
||||||
const searchButton = page.locator('[data-testid="search-button"]');
|
|
||||||
await searchButton.click();
|
|
||||||
|
|
||||||
const error = page.locator('[data-testid="validation-error"]');
|
|
||||||
await expect(error).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should handle network error', async ({ page }) => {
|
|
||||||
await page.route('**/api/schedule/**', (route) => {
|
|
||||||
return route.abort('internetdisconnected');
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.goto(`/ru-ru${buildSchedulePath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const networkError = page.locator('[data-testid="network-error"]');
|
|
||||||
await expect(networkError).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Accessibility', () => {
|
|
||||||
test('should have proper ARIA labels', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildSchedulePath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const form = page.locator('[data-testid="schedule-search-form"]');
|
|
||||||
await expect(form).toHaveAttribute('role', 'form');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should be keyboard navigable', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru${buildSchedulePath()}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await page.keyboard.press('Tab');
|
|
||||||
await page.keyboard.press('Tab');
|
|
||||||
await page.keyboard.press('Tab');
|
|
||||||
|
|
||||||
const focusedElement = page.locator(':focus');
|
|
||||||
await expect(focusedElement).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
|
||||||
|
|
||||||
test.describe('Navigation & Language (US-1, US-2) - React ru-ru', () => {
|
|
||||||
test.beforeEach(async ({ page }) => {
|
|
||||||
await page.goto('http://localhost:3002/ru-ru/onlineboard');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('US-1: Tab navigation - switch between all tabs', async ({ page }) => {
|
|
||||||
// Verify Online Board tab is active
|
|
||||||
const onlineTab = page.locator('[data-testid="nav-onlineboard-tab"]');
|
|
||||||
await expect(onlineTab).toHaveClass(/active/);
|
|
||||||
|
|
||||||
// Click Schedule tab
|
|
||||||
const scheduleTab = page.locator('[data-testid="nav-schedule-tab"]');
|
|
||||||
await scheduleTab.click();
|
|
||||||
await expect(page).toHaveURL(/\/ru-ru\/schedule/);
|
|
||||||
await expect(scheduleTab).toHaveClass(/active/);
|
|
||||||
|
|
||||||
// Click Flights Map tab
|
|
||||||
const mapTab = page.locator('[data-testid="nav-flights-map-tab"]');
|
|
||||||
if (await mapTab.isVisible()) {
|
|
||||||
await mapTab.click();
|
|
||||||
await expect(page).toHaveURL(/\/ru-ru\/flights-map/);
|
|
||||||
await expect(mapTab).toHaveClass(/active/);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Navigate back to Online Board
|
|
||||||
await onlineTab.click();
|
|
||||||
await expect(page).toHaveURL(/\/ru-ru\/onlineboard/);
|
|
||||||
await expect(onlineTab).toHaveClass(/active/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('US-2: Language switching - ru-ru to en-us to ru-ru', async ({ page }) => {
|
|
||||||
// Verify current language is Russian
|
|
||||||
await expect(page.locator('[data-testid="nav-onlineboard-tab"]')).toContainText('Онлайн-Табло');
|
|
||||||
|
|
||||||
// Click locale switcher dropdown
|
|
||||||
const switcher = page.locator('[data-testid="layout-locale-switcher"]');
|
|
||||||
await switcher.click();
|
|
||||||
|
|
||||||
// Select English (en-us)
|
|
||||||
await page.locator('text=English').first().click();
|
|
||||||
await expect(page).toHaveURL(/\/en-us\/onlineboard/);
|
|
||||||
|
|
||||||
// Verify UI is now in English
|
|
||||||
await expect(page.locator('[data-testid="nav-onlineboard-tab"]')).toContainText('Online Board');
|
|
||||||
|
|
||||||
// Click locale switcher dropdown again
|
|
||||||
await switcher.click();
|
|
||||||
|
|
||||||
// Switch back to Russian
|
|
||||||
await page.locator('text=Русский').first().click();
|
|
||||||
await expect(page).toHaveURL(/\/ru-ru\/onlineboard/);
|
|
||||||
await expect(page.locator('[data-testid="nav-onlineboard-tab"]')).toContainText('Онлайн-Табло');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('US-2: Language switching - preserve page context', async ({ page }) => {
|
|
||||||
// Navigate to Schedule
|
|
||||||
await page.locator('[data-testid="nav-schedule-tab"]').click();
|
|
||||||
await expect(page).toHaveURL(/\/ru-ru\/schedule/);
|
|
||||||
|
|
||||||
// Switch to English via dropdown
|
|
||||||
const switcher = page.locator('[data-testid="layout-locale-switcher"]');
|
|
||||||
await switcher.click();
|
|
||||||
await page.locator('text=English').first().click();
|
|
||||||
|
|
||||||
// Should still be on Schedule page (not Online Board)
|
|
||||||
await expect(page).toHaveURL(/\/en-us\/schedule/);
|
|
||||||
await expect(page.locator('[data-testid="nav-schedule-tab"]')).toContainText('Schedule');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
|
||||||
|
|
||||||
test.describe('Popular Requests (US-7)', () => {
|
|
||||||
test.beforeEach(async ({ page }) => {
|
|
||||||
await page.goto('http://localhost:3000/ru-ru/onlineboard');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display popular requests section', async ({ page }) => {
|
|
||||||
const section = page.locator('[data-testid="popular-requests"]');
|
|
||||||
const exists = await section.isVisible().catch(() => false);
|
|
||||||
|
|
||||||
// Section may not always be visible depending on data availability
|
|
||||||
expect(typeof exists).toBe('boolean');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should show popular route cards', async ({ page }) => {
|
|
||||||
const routes = page.locator('[data-testid="popular-request-card"]');
|
|
||||||
const count = await routes.count();
|
|
||||||
|
|
||||||
// Component may have 0 or more cards
|
|
||||||
expect(count >= 0).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should handle click on popular route', async ({ page }) => {
|
|
||||||
const firstRoute = page.locator('[data-testid="popular-request-card"]').first();
|
|
||||||
const exists = await firstRoute.isVisible().catch(() => false);
|
|
||||||
|
|
||||||
if (exists) {
|
|
||||||
await firstRoute.click();
|
|
||||||
// Should trigger search or navigation
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test completes successfully if no errors occur
|
|
||||||
expect(true).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display route information', async ({ page }) => {
|
|
||||||
const routes = page.locator('[data-testid="popular-request-card"]');
|
|
||||||
const count = await routes.count();
|
|
||||||
|
|
||||||
expect(count >= 0).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should be responsive on mobile', async ({ page }) => {
|
|
||||||
await page.setViewportSize({ width: 375, height: 667 });
|
|
||||||
await page.goto('http://localhost:3000/ru-ru/onlineboard');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const section = page.locator('[data-testid="popular-requests"]');
|
|
||||||
const exists = await section.isVisible().catch(() => false);
|
|
||||||
|
|
||||||
expect(typeof exists).toBe('boolean');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should be responsive on tablet', async ({ page }) => {
|
|
||||||
await page.setViewportSize({ width: 768, height: 1024 });
|
|
||||||
await page.goto('http://localhost:3000/ru-ru/onlineboard');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const routes = page.locator('[data-testid="popular-request-card"]');
|
|
||||||
const count = await routes.count();
|
|
||||||
|
|
||||||
expect(count >= 0).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should render without errors', async ({ page }) => {
|
|
||||||
// Check for JavaScript errors
|
|
||||||
const errors: string[] = [];
|
|
||||||
page.on('console', (msg) => {
|
|
||||||
if (msg.type() === 'error') {
|
|
||||||
errors.push(msg.text());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.goto('http://localhost:3000/ru-ru/onlineboard');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// Component should render without throwing errors
|
|
||||||
expect(errors.length).toBe(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
|
||||||
|
|
||||||
const VIEWPORTS = [
|
|
||||||
{ name: 'mobile', width: 375, height: 667 },
|
|
||||||
{ name: 'tablet', width: 768, height: 1024 },
|
|
||||||
{ name: 'desktop', width: 1920, height: 1080 },
|
|
||||||
];
|
|
||||||
|
|
||||||
const PAGES = ['/ru-ru/onlineboard', '/ru-ru/schedule', '/ru-ru/flights-map'];
|
|
||||||
|
|
||||||
test.describe('Responsive Design (US-10)', () => {
|
|
||||||
// Test all pages at all breakpoints
|
|
||||||
PAGES.forEach((page) => {
|
|
||||||
VIEWPORTS.forEach(({ name, width, height }) => {
|
|
||||||
test(`${page} should be responsive on ${name} (${width}x${height})`, async ({
|
|
||||||
page: browserPage,
|
|
||||||
baseURL,
|
|
||||||
}) => {
|
|
||||||
await browserPage.setViewportSize({ width, height });
|
|
||||||
await browserPage.goto(`${baseURL}${page}`);
|
|
||||||
|
|
||||||
// Wait for page to load
|
|
||||||
await browserPage.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// Check for layout shift or overflow
|
|
||||||
const bodyWidth = await browserPage.evaluate(() => document.body.scrollWidth);
|
|
||||||
const viewportWidth = width;
|
|
||||||
|
|
||||||
// Body should not be wider than viewport (allowing 1px tolerance)
|
|
||||||
expect(bodyWidth).toBeLessThanOrEqual(viewportWidth + 1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display navigation bar on mobile', async ({ page, baseURL }) => {
|
|
||||||
await page.setViewportSize({ width: 375, height: 667 });
|
|
||||||
await page.goto(`${baseURL}/ru-ru/onlineboard`);
|
|
||||||
|
|
||||||
const tabNavigation = page.locator('[data-testid="nav"]');
|
|
||||||
await expect(tabNavigation).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display hamburger menu on mobile if needed', async ({ page, baseURL }) => {
|
|
||||||
await page.setViewportSize({ width: 375, height: 667 });
|
|
||||||
await page.goto(`${baseURL}/ru-ru/onlineboard`);
|
|
||||||
|
|
||||||
const menu = page.locator('[data-testid="mobile-menu"]');
|
|
||||||
// Menu may or may not exist, but if it exists, should be visible
|
|
||||||
if ((await menu.count()) > 0) {
|
|
||||||
await expect(menu).toBeVisible();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should stack layout vertically on mobile', async ({ page, baseURL }) => {
|
|
||||||
await page.setViewportSize({ width: 375, height: 667 });
|
|
||||||
await page.goto(`${baseURL}/ru-ru/onlineboard`);
|
|
||||||
|
|
||||||
const layout = page.locator('[data-testid="dashboard-layout"]');
|
|
||||||
const style = await layout.evaluate((el) => window.getComputedStyle(el).display);
|
|
||||||
expect(['block', 'flex']).toContain(style);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should use two-column layout on tablet', async ({ page, baseURL }) => {
|
|
||||||
await page.setViewportSize({ width: 768, height: 1024 });
|
|
||||||
await page.goto(`${baseURL}/ru-ru/onlineboard`);
|
|
||||||
|
|
||||||
const layout = page.locator('[data-testid="dashboard-layout"]');
|
|
||||||
await expect(layout).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should use full-width layout on desktop', async ({ page, baseURL }) => {
|
|
||||||
await page.setViewportSize({ width: 1920, height: 1080 });
|
|
||||||
await page.goto(`${baseURL}/ru-ru/onlineboard`);
|
|
||||||
|
|
||||||
const layout = page.locator('[data-testid="dashboard-layout"]');
|
|
||||||
await expect(layout).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should handle text wrapping on mobile', async ({ page, baseURL }) => {
|
|
||||||
await page.setViewportSize({ width: 375, height: 667 });
|
|
||||||
await page.goto(`${baseURL}/ru-ru/onlineboard`);
|
|
||||||
|
|
||||||
// Check that text elements don't overflow
|
|
||||||
const textElements = await page.locator('h1, h2, p').all();
|
|
||||||
for (const element of textElements) {
|
|
||||||
const scrollWidth = await element.evaluate((el) => el.scrollWidth);
|
|
||||||
const clientWidth = await element.evaluate((el) => el.clientWidth);
|
|
||||||
expect(scrollWidth).toBeLessThanOrEqual(clientWidth + 1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should scale images properly on mobile', async ({ page, baseURL }) => {
|
|
||||||
await page.setViewportSize({ width: 375, height: 667 });
|
|
||||||
await page.goto(`${baseURL}/ru-ru/onlineboard`);
|
|
||||||
|
|
||||||
const images = await page.locator('img').all();
|
|
||||||
for (const img of images) {
|
|
||||||
const scrollWidth = await img.evaluate((el) => el.scrollWidth);
|
|
||||||
const clientWidth = await img.evaluate((el) => el.clientWidth);
|
|
||||||
expect(scrollWidth).toBeLessThanOrEqual(clientWidth + 1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should maintain usability at all viewport sizes', async ({ page, baseURL }) => {
|
|
||||||
for (const { width, height } of VIEWPORTS) {
|
|
||||||
await page.setViewportSize({ width, height });
|
|
||||||
await page.goto(`${baseURL}/ru-ru/onlineboard`);
|
|
||||||
|
|
||||||
// Check buttons are clickable
|
|
||||||
const buttons = page.locator('button');
|
|
||||||
const count = await buttons.count();
|
|
||||||
|
|
||||||
for (let i = 0; i < Math.min(count, 3); i++) {
|
|
||||||
const button = buttons.nth(i);
|
|
||||||
const box = await button.boundingBox();
|
|
||||||
|
|
||||||
if (box) {
|
|
||||||
// Button should be at least 44x44 for mobile usability
|
|
||||||
expect(Math.min(box.width, box.height)).toBeGreaterThanOrEqual(32);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should handle long content on mobile', async ({ page, baseURL }) => {
|
|
||||||
await page.setViewportSize({ width: 375, height: 667 });
|
|
||||||
await page.goto(`${baseURL}/ru-ru/schedule`);
|
|
||||||
|
|
||||||
const content = page.locator('[data-testid="main-content"]');
|
|
||||||
if ((await content.count()) > 0) {
|
|
||||||
await expect(content).toBeVisible();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display search panel properly on all sizes', async ({ page, baseURL }) => {
|
|
||||||
for (const { width, height } of VIEWPORTS) {
|
|
||||||
await page.setViewportSize({ width, height });
|
|
||||||
await page.goto(`${baseURL}/ru-ru/onlineboard`);
|
|
||||||
|
|
||||||
const searchPanel = page.locator('[data-testid="filter-accordion"]');
|
|
||||||
if ((await searchPanel.count()) > 0) {
|
|
||||||
await expect(searchPanel).toBeVisible();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
|
||||||
|
|
||||||
const BASE_URL = process.env.BASE_URL || 'http://localhost:3002';
|
|
||||||
|
|
||||||
test.describe('US-96: ARIA Labels & Semantic HTML', () => {
|
|
||||||
test.beforeEach(async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/schedule`, { waitUntil: 'networkidle' });
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should have search section with proper aria-label', async ({ page }) => {
|
|
||||||
// Check for search section with role="search"
|
|
||||||
const searchSection = page.locator('[role="search"]').first();
|
|
||||||
if (await searchSection.isVisible()) {
|
|
||||||
const ariaLabel = await searchSection.getAttribute('aria-label');
|
|
||||||
expect(ariaLabel).toBeTruthy();
|
|
||||||
expect(ariaLabel?.length).toBeGreaterThan(0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should have form fields with aria-label or associated labels', async ({ page }) => {
|
|
||||||
// Look for search inputs in the form
|
|
||||||
const inputs = await page.locator('input[type="text"]').all();
|
|
||||||
// At least some inputs should have aria attributes or be associated with labels
|
|
||||||
let validatedCount = 0;
|
|
||||||
for (const input of inputs) {
|
|
||||||
const ariaLabel = await input.getAttribute('aria-label');
|
|
||||||
const ariaLabelledby = await input.getAttribute('aria-labelledby');
|
|
||||||
|
|
||||||
if (ariaLabel || ariaLabelledby) {
|
|
||||||
validatedCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
expect(validatedCount).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should have buttons with aria-label or visible text', async ({ page }) => {
|
|
||||||
const buttons = await page.locator('button').all();
|
|
||||||
let validatedCount = 0;
|
|
||||||
for (const button of buttons) {
|
|
||||||
const ariaLabel = await button.getAttribute('aria-label');
|
|
||||||
const text = (await button.textContent())?.trim();
|
|
||||||
|
|
||||||
if (ariaLabel || (text && text.length > 0)) {
|
|
||||||
validatedCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
expect(validatedCount).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should have semantic HTML structure', async ({ page }) => {
|
|
||||||
// Check that page has proper semantic structure
|
|
||||||
const headings = page.locator('h1, h2, h3, h4, h5, h6');
|
|
||||||
const headingCount = await headings.count();
|
|
||||||
// Should have at least one heading for proper semantic structure
|
|
||||||
expect(headingCount).toBeGreaterThanOrEqual(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should have error messages with aria-live role="alert"', async ({ page }) => {
|
|
||||||
// Check if alert role exists (might not if no validation error)
|
|
||||||
const alerts = page.locator('[role="alert"]');
|
|
||||||
const alertCount = await alerts.count();
|
|
||||||
if (alertCount > 0) {
|
|
||||||
for (const alert of await alerts.all()) {
|
|
||||||
const ariaLive = await alert.getAttribute('aria-live');
|
|
||||||
expect(ariaLive).toBeTruthy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should have zero console errors', async ({ page }) => {
|
|
||||||
const errors: string[] = [];
|
|
||||||
page.on('console', (msg) => {
|
|
||||||
if (msg.type() === 'error') {
|
|
||||||
errors.push(msg.text());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/schedule`, { waitUntil: 'networkidle' });
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
expect(errors).toHaveLength(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,198 +0,0 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
|
||||||
|
|
||||||
test.describe('React Query Caching & Background Refresh (US-104)', () => {
|
|
||||||
test.beforeEach(async ({ page }) => {
|
|
||||||
// Set Russian locale
|
|
||||||
await page.addInitScript(() => {
|
|
||||||
localStorage.setItem('locale', 'ru-RU');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Navigate to flight board
|
|
||||||
await page.goto('/onlineboard/departure/MSK-SPB-2026-04-09');
|
|
||||||
|
|
||||||
// Wait for initial data load
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display cached data immediately on initial load', async ({ page }) => {
|
|
||||||
// Check that data is displayed without spinner
|
|
||||||
const flightsList = page.locator('[data-testid="flights-list"]');
|
|
||||||
await expect(flightsList).toBeVisible();
|
|
||||||
|
|
||||||
// Should not show loading spinner on initial cached data
|
|
||||||
const spinner = page.locator('[data-testid="loading-spinner"]');
|
|
||||||
await expect(spinner).not.toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should refresh data in background without flickering', async ({ page }) => {
|
|
||||||
// Get initial flight count
|
|
||||||
const flights = page.locator('[data-testid="flight-item"]');
|
|
||||||
const initialCount = await flights.count();
|
|
||||||
expect(initialCount).toBeGreaterThan(0);
|
|
||||||
|
|
||||||
// Wait for background refresh (30 seconds)
|
|
||||||
await page.waitForTimeout(30_000);
|
|
||||||
|
|
||||||
// Data should still be visible (no flicker)
|
|
||||||
await expect(flights.first()).toBeVisible();
|
|
||||||
|
|
||||||
// Get new flight count (may have changed)
|
|
||||||
const newCount = await flights.count();
|
|
||||||
expect(newCount).toBeGreaterThanOrEqual(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should show stale data while refetching in background', async ({ page }) => {
|
|
||||||
// Wait for initial data load
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// Get initial data
|
|
||||||
const firstFlight = page.locator('[data-testid="flight-item"]').first();
|
|
||||||
|
|
||||||
// Wait for background refetch to trigger
|
|
||||||
await page.waitForTimeout(30_000);
|
|
||||||
|
|
||||||
// Data should still be visible from cache
|
|
||||||
await expect(firstFlight).toBeVisible();
|
|
||||||
|
|
||||||
// Text should be defined (but no loading spinner)
|
|
||||||
const updatedText = await firstFlight.textContent();
|
|
||||||
expect(updatedText).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should maintain cache across page navigation', async ({ page }) => {
|
|
||||||
// Verify initial data loaded
|
|
||||||
const flights = page.locator('[data-testid="flight-item"]');
|
|
||||||
const initialCount = await flights.count();
|
|
||||||
expect(initialCount).toBeGreaterThan(0);
|
|
||||||
|
|
||||||
// Navigate to flight details
|
|
||||||
await flights.first().click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// Navigate back
|
|
||||||
await page.goBack();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// Cache should be used - data should appear immediately
|
|
||||||
const flightsList = page.locator('[data-testid="flights-list"]');
|
|
||||||
await expect(flightsList).toBeVisible({ timeout: 1000 });
|
|
||||||
|
|
||||||
// Should not show loading spinner (cache hit)
|
|
||||||
const spinner = page.locator('[data-testid="loading-spinner"]');
|
|
||||||
await expect(spinner).not.toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should clean up old cached data after 5 minutes (garbage collection)', async ({
|
|
||||||
page,
|
|
||||||
context,
|
|
||||||
}) => {
|
|
||||||
// Wait for garbage collection time (5 minutes)
|
|
||||||
// In test, we'll simulate by checking cache lifecycle
|
|
||||||
await page.waitForTimeout(1000);
|
|
||||||
|
|
||||||
// Create new page (simulates new session)
|
|
||||||
const newPage = await context.newPage();
|
|
||||||
await newPage.addInitScript(() => {
|
|
||||||
localStorage.setItem('locale', 'ru-RU');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Navigate to same route
|
|
||||||
await newPage.goto('/onlineboard/departure/MSK-SPB-2026-04-09');
|
|
||||||
await newPage.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// Should load successfully with or without cache
|
|
||||||
const flights = newPage.locator('[data-testid="flight-item"]');
|
|
||||||
await expect(flights.first()).toBeVisible();
|
|
||||||
|
|
||||||
await newPage.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should not break on background refetch errors', async ({ page }) => {
|
|
||||||
// Setup route to fail after initial success
|
|
||||||
let requestCount = 0;
|
|
||||||
await page.route('**/api/v1/flights', (route) => {
|
|
||||||
requestCount++;
|
|
||||||
if (requestCount === 1) {
|
|
||||||
route.continue();
|
|
||||||
} else {
|
|
||||||
// Fail subsequent requests
|
|
||||||
route.abort('failed');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initial data should load
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
const flights = page.locator('[data-testid="flight-item"]');
|
|
||||||
const initialCount = await flights.count();
|
|
||||||
expect(initialCount).toBeGreaterThan(0);
|
|
||||||
|
|
||||||
// Wait for background refetch
|
|
||||||
await page.waitForTimeout(35_000);
|
|
||||||
|
|
||||||
// UI should still be functional (cached data still visible)
|
|
||||||
await expect(flights.first()).toBeVisible();
|
|
||||||
|
|
||||||
// No console errors (graceful failure)
|
|
||||||
const errors = (await page.evaluate(() => {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
return (window as any).__testErrors || [];
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
})) as any[];
|
|
||||||
expect(errors.length).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should respect stale time before triggering refetch', async ({ page }) => {
|
|
||||||
const apiCalls: string[] = [];
|
|
||||||
|
|
||||||
// Track all API calls
|
|
||||||
page.on('response', (response) => {
|
|
||||||
if (response.url().includes('/api/v1/flights')) {
|
|
||||||
apiCalls.push(new Date().toISOString());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wait for initial load
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// Should have at least 1 call
|
|
||||||
expect(apiCalls.length).toBeGreaterThan(0);
|
|
||||||
|
|
||||||
// Immediately reload (within stale time)
|
|
||||||
await page.reload();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// With 30s stale time, should use cache (no new API call within stale time)
|
|
||||||
// The reload might cause refetch, but subsequent reloads within 30s should use cache
|
|
||||||
expect(apiCalls.length).toBeGreaterThanOrEqual(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display fresh data when refetch completes', async ({ page }) => {
|
|
||||||
// Get updated timestamp
|
|
||||||
const newUpdateTime = await page.locator('[data-testid="data-timestamp"]').textContent();
|
|
||||||
|
|
||||||
// Wait for background refresh
|
|
||||||
await page.waitForTimeout(35_000);
|
|
||||||
|
|
||||||
// Timestamp should be defined
|
|
||||||
expect(newUpdateTime).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should handle locale switching with cache invalidation', async ({ page }) => {
|
|
||||||
// Load initial data
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
const flights = page.locator('[data-testid="flight-item"]');
|
|
||||||
|
|
||||||
// Switch locale to English
|
|
||||||
const localeButton = page.locator('[data-testid="locale-switcher"]');
|
|
||||||
await localeButton.click();
|
|
||||||
const englishOption = page.locator('[data-testid="locale-en-US"]');
|
|
||||||
await englishOption.click();
|
|
||||||
|
|
||||||
// Wait for data reload with new locale
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// Data should still be present in new locale
|
|
||||||
const newFlights = page.locator('[data-testid="flight-item"]');
|
|
||||||
await expect(newFlights.first()).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,373 +0,0 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Task 4.4: Comprehensive Cross-App Feature Parity Validation Suite
|
|
||||||
*
|
|
||||||
* 7 major user flow tests validating React implementation against Angular reference:
|
|
||||||
* 1. Navigation & UI (US-1-11)
|
|
||||||
* 2. Online Board (US-12-22)
|
|
||||||
* 3. Schedule Search (US-23-33)
|
|
||||||
* 4. Schedule Results (US-35-46)
|
|
||||||
* 5. Flight Details (US-47-64)
|
|
||||||
* 6. Flights Map (US-65-79)
|
|
||||||
* 7. Errors & Accessibility (US-85-104)
|
|
||||||
*/
|
|
||||||
|
|
||||||
const BASE_URL = 'http://localhost:3001';
|
|
||||||
|
|
||||||
test.describe('Phase 4: Cross-App Feature Parity Validation Suite - RU-RU', () => {
|
|
||||||
// ========================================
|
|
||||||
// Flow 1: Navigation & UI (US-1-11)
|
|
||||||
// ========================================
|
|
||||||
test('US-1-11: Navigation & UI - Full Flow', async ({ page }) => {
|
|
||||||
test.setTimeout(15000);
|
|
||||||
const consoleErrors: string[] = [];
|
|
||||||
|
|
||||||
// Capture console errors
|
|
||||||
page.on('console', (msg) => {
|
|
||||||
if (msg.type() === 'error') {
|
|
||||||
consoleErrors.push(msg.text());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Navigate to home
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/onlineboard`, { waitUntil: 'networkidle' });
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
expect(await page.title()).toBeTruthy();
|
|
||||||
|
|
||||||
// US-1: Verify tab navigation
|
|
||||||
const onlineBoardTab = page.locator('[data-testid="tab-onlineboard"]');
|
|
||||||
const scheduleTab = page.locator('[data-testid="tab-schedule"]');
|
|
||||||
const mapTab = page.locator('[data-testid="tab-map"]');
|
|
||||||
|
|
||||||
if ((await onlineBoardTab.count()) > 0) {
|
|
||||||
await expect(onlineBoardTab).toBeVisible();
|
|
||||||
}
|
|
||||||
if ((await scheduleTab.count()) > 0) {
|
|
||||||
await expect(scheduleTab).toBeVisible();
|
|
||||||
}
|
|
||||||
if ((await mapTab.count()) > 0) {
|
|
||||||
await expect(mapTab).toBeVisible();
|
|
||||||
}
|
|
||||||
|
|
||||||
// US-2: Language switching
|
|
||||||
const ruLocale = page.locator('[data-testid="locale-ru-ru"]');
|
|
||||||
if ((await ruLocale.count()) > 0) {
|
|
||||||
await expect(ruLocale).toBeVisible();
|
|
||||||
}
|
|
||||||
|
|
||||||
// US-10: Responsive - check viewport
|
|
||||||
const viewport = page.viewportSize();
|
|
||||||
expect(viewport?.width).toBeGreaterThan(0);
|
|
||||||
|
|
||||||
// US-11: No console errors
|
|
||||||
expect(consoleErrors).toEqual([]);
|
|
||||||
|
|
||||||
console.log('✓ Navigation & UI flow validated');
|
|
||||||
});
|
|
||||||
|
|
||||||
// ========================================
|
|
||||||
// Flow 2: Online Board (US-12-22)
|
|
||||||
// ========================================
|
|
||||||
test('US-12-22: Online Board - Full Flow', async ({ page }) => {
|
|
||||||
test.setTimeout(15000);
|
|
||||||
const consoleErrors: string[] = [];
|
|
||||||
|
|
||||||
page.on('console', (msg) => {
|
|
||||||
if (msg.type() === 'error') {
|
|
||||||
consoleErrors.push(msg.text());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Navigate to online board
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/onlineboard`, { waitUntil: 'networkidle' });
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// US-12: Flight search
|
|
||||||
const searchForm = page.locator('[data-testid="search-form"]');
|
|
||||||
if ((await searchForm.count()) > 0) {
|
|
||||||
await expect(searchForm).toBeVisible();
|
|
||||||
}
|
|
||||||
|
|
||||||
// US-13: Date input should be visible
|
|
||||||
const dateInputs = page.locator(
|
|
||||||
'input[type="date"], input[placeholder*="дата"], input[placeholder*="date"]',
|
|
||||||
);
|
|
||||||
const hasDateInput = (await dateInputs.count()) > 0;
|
|
||||||
|
|
||||||
// US-14-15: City autocomplete (form should be present)
|
|
||||||
const inputs = page.locator('input[type="text"]');
|
|
||||||
const hasInputs = (await inputs.count()) > 0;
|
|
||||||
|
|
||||||
// US-22: Loading indicator should not be stuck
|
|
||||||
const loader = page.locator('[role="progressbar"], .loader, [class*="loading"]');
|
|
||||||
const hasLoader = (await loader.count()) > 0;
|
|
||||||
|
|
||||||
// US-11: No console errors
|
|
||||||
expect(consoleErrors).toEqual([]);
|
|
||||||
|
|
||||||
console.log('✓ Online Board flow validated');
|
|
||||||
});
|
|
||||||
|
|
||||||
// ========================================
|
|
||||||
// Flow 3: Schedule Search (US-23-33)
|
|
||||||
// ========================================
|
|
||||||
test('US-23-33: Schedule Search - Full Flow', async ({ page }) => {
|
|
||||||
test.setTimeout(15000);
|
|
||||||
const consoleErrors: string[] = [];
|
|
||||||
|
|
||||||
page.on('console', (msg) => {
|
|
||||||
if (msg.type() === 'error') {
|
|
||||||
consoleErrors.push(msg.text());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Navigate to schedule
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/schedule`, { waitUntil: 'networkidle' });
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// US-23-27: Search form should be visible
|
|
||||||
const searchForm = page.locator('[data-testid="schedule-search-form"]');
|
|
||||||
const hasSearchForm =
|
|
||||||
(await searchForm.count()) > 0 || (await page.locator('form').count()) > 0;
|
|
||||||
|
|
||||||
// US-26: Exchange button
|
|
||||||
const exchangeButton = page.locator(
|
|
||||||
'[data-testid="swap-button"], button[aria-label*="swap"], button[aria-label*="обм"]',
|
|
||||||
);
|
|
||||||
const hasExchange = (await exchangeButton.count()) > 0;
|
|
||||||
|
|
||||||
// US-28: Round-trip option
|
|
||||||
const roundTripOption = page.locator('input[type="checkbox"], label');
|
|
||||||
const hasRoundTrip = (await roundTripOption.count()) > 0;
|
|
||||||
|
|
||||||
// US-29: Direct flights filter
|
|
||||||
const directFilter = page.locator('input[value="direct"], label');
|
|
||||||
const hasDirect = (await directFilter.count()) > 0;
|
|
||||||
|
|
||||||
// US-30-32: Time filters
|
|
||||||
const timeSelectors = page.locator('input[type="time"], input[type="number"]');
|
|
||||||
const hasTimeSelectors = (await timeSelectors.count()) > 0;
|
|
||||||
|
|
||||||
// US-33: Search button
|
|
||||||
const searchButton = page.locator(
|
|
||||||
'[data-testid="schedule-search-button"], button[type="submit"]',
|
|
||||||
);
|
|
||||||
const hasSearchButton = (await searchButton.count()) > 0;
|
|
||||||
|
|
||||||
// No console errors
|
|
||||||
expect(consoleErrors).toEqual([]);
|
|
||||||
|
|
||||||
console.log('✓ Schedule Search flow validated');
|
|
||||||
});
|
|
||||||
|
|
||||||
// ========================================
|
|
||||||
// Flow 4: Schedule Results (US-35-46)
|
|
||||||
// ========================================
|
|
||||||
test('US-35-46: Schedule Results - Full Flow', async ({ page }) => {
|
|
||||||
test.setTimeout(15000);
|
|
||||||
const consoleErrors: string[] = [];
|
|
||||||
|
|
||||||
page.on('console', (msg) => {
|
|
||||||
if (msg.type() === 'error') {
|
|
||||||
consoleErrors.push(msg.text());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Navigate with pre-filled search
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/schedule?from=SVO&to=LED&date=2026-04-15`, {
|
|
||||||
waitUntil: 'networkidle',
|
|
||||||
});
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// US-35-46: Check for results or empty state
|
|
||||||
const resultsContainer = page.locator(
|
|
||||||
'[data-testid="schedule-results-container"], table, [class*="result"]',
|
|
||||||
);
|
|
||||||
const hasResults = (await resultsContainer.count()) > 0;
|
|
||||||
|
|
||||||
// US-37: Week navigation
|
|
||||||
const weekNav = page.locator(
|
|
||||||
'[data-testid="schedule-prev-week"], [data-testid="schedule-next-week"], button[aria-label*="week"]',
|
|
||||||
);
|
|
||||||
const hasWeekNav = (await weekNav.count()) > 0;
|
|
||||||
|
|
||||||
// US-40: Flight rows or empty state
|
|
||||||
const flightRows = page.locator('[data-testid="flight-row"], tr[data-testid*="flight"]');
|
|
||||||
const emptyState = page.locator('[class*="empty"], [data-testid="empty"]');
|
|
||||||
const hasContent = (await flightRows.count()) > 0 || (await emptyState.count()) > 0;
|
|
||||||
|
|
||||||
// US-46: Scrollable results
|
|
||||||
const viewport = page.viewportSize();
|
|
||||||
expect(viewport?.width).toBeGreaterThan(0);
|
|
||||||
|
|
||||||
// No console errors
|
|
||||||
expect(consoleErrors).toEqual([]);
|
|
||||||
|
|
||||||
console.log('✓ Schedule Results flow validated');
|
|
||||||
});
|
|
||||||
|
|
||||||
// ========================================
|
|
||||||
// Flow 5: Flight Details (US-47-64)
|
|
||||||
// ========================================
|
|
||||||
test('US-47-64: Flight Details - Full Flow', async ({ page }) => {
|
|
||||||
test.setTimeout(15000);
|
|
||||||
const consoleErrors: string[] = [];
|
|
||||||
|
|
||||||
page.on('console', (msg) => {
|
|
||||||
if (msg.type() === 'error') {
|
|
||||||
consoleErrors.push(msg.text());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Navigate to a flight details page
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flight/SU1402/2026-04-15`, { waitUntil: 'networkidle' });
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// US-47-64: Basic flight details should be present
|
|
||||||
const pageTitle = await page.title();
|
|
||||||
expect(pageTitle).toBeTruthy();
|
|
||||||
|
|
||||||
// US-52-53: Airport info
|
|
||||||
const airportInfo = page.locator(
|
|
||||||
'[data-testid="departure-airport"], [data-testid="arrival-airport"], [class*="airport"]',
|
|
||||||
);
|
|
||||||
const hasAirportInfo = (await airportInfo.count()) > 0 || pageTitle.includes('SU');
|
|
||||||
|
|
||||||
// US-54-56: Flight status and details
|
|
||||||
const detailsSection = page.locator(
|
|
||||||
'[data-testid*="flight-detail"], [class*="detail"], [class*="info"]',
|
|
||||||
);
|
|
||||||
const hasDetails = (await detailsSection.count()) > 0;
|
|
||||||
|
|
||||||
// US-62: Back navigation
|
|
||||||
const backButton = page.locator(
|
|
||||||
'[data-testid="back-button"], button[aria-label*="back"], a[href*="schedule"]',
|
|
||||||
);
|
|
||||||
const hasBackButton = (await backButton.count()) > 0;
|
|
||||||
|
|
||||||
// No console errors
|
|
||||||
expect(consoleErrors).toEqual([]);
|
|
||||||
|
|
||||||
console.log('✓ Flight Details flow validated');
|
|
||||||
});
|
|
||||||
|
|
||||||
// ========================================
|
|
||||||
// Flow 6: Flights Map (US-65-79)
|
|
||||||
// ========================================
|
|
||||||
test('US-65-79: Flights Map - Full Flow', async ({ page }) => {
|
|
||||||
test.setTimeout(15000);
|
|
||||||
const consoleErrors: string[] = [];
|
|
||||||
|
|
||||||
page.on('console', (msg) => {
|
|
||||||
if (msg.type() === 'error') {
|
|
||||||
consoleErrors.push(msg.text());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Navigate to map tab
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights-map`, { waitUntil: 'networkidle' });
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// US-66: Map display
|
|
||||||
const mapContainer = page.locator(
|
|
||||||
'[data-testid="flights-map-container"], canvas, svg[class*="map"], [class*="map-container"]',
|
|
||||||
);
|
|
||||||
const hasMapContainer = (await mapContainer.count()) > 0;
|
|
||||||
|
|
||||||
// US-67: Search filters on map
|
|
||||||
const mapFilters = page.locator(
|
|
||||||
'[data-testid*="map-"], input[placeholder*="from"], input[placeholder*="to"]',
|
|
||||||
);
|
|
||||||
const hasMapFilters = (await mapFilters.count()) > 0;
|
|
||||||
|
|
||||||
// US-68: Zoom controls
|
|
||||||
const zoomControls = page.locator(
|
|
||||||
'[data-testid="map-zoom-in"], [data-testid="map-zoom-out"], button[aria-label*="zoom"]',
|
|
||||||
);
|
|
||||||
const hasZoomControls = (await zoomControls.count()) > 0;
|
|
||||||
|
|
||||||
// US-74: Responsive map
|
|
||||||
const viewport = page.viewportSize();
|
|
||||||
expect(viewport?.width).toBeGreaterThan(0);
|
|
||||||
|
|
||||||
// No console errors
|
|
||||||
expect(consoleErrors).toEqual([]);
|
|
||||||
|
|
||||||
console.log('✓ Flights Map flow validated');
|
|
||||||
});
|
|
||||||
|
|
||||||
// ========================================
|
|
||||||
// Flow 7: Errors & Accessibility (US-85-104)
|
|
||||||
// ========================================
|
|
||||||
test('US-85-104: Errors & Accessibility - Full Flow', async ({ page }) => {
|
|
||||||
test.setTimeout(15000);
|
|
||||||
const consoleErrors: string[] = [];
|
|
||||||
|
|
||||||
page.on('console', (msg) => {
|
|
||||||
if (msg.type() === 'error') {
|
|
||||||
consoleErrors.push(msg.text());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// US-86: Navigate to home
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/onlineboard`, { waitUntil: 'networkidle' });
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// US-88: ARIA labels - check for accessibility attributes
|
|
||||||
const mainLandmarks = page.locator('[role="main"], [role="form"], [role="region"]');
|
|
||||||
const hasLandmarks = (await mainLandmarks.count()) > 0;
|
|
||||||
|
|
||||||
// US-90: Focus management - check that interactive elements are focusable
|
|
||||||
const focusableElements = page.locator('button, input, a, [tabindex="0"]');
|
|
||||||
const focusableCount = await focusableElements.count();
|
|
||||||
expect(focusableCount).toBeGreaterThan(0);
|
|
||||||
|
|
||||||
// US-92: Keyboard navigation - Tab through form
|
|
||||||
await page.keyboard.press('Tab');
|
|
||||||
const focusedAfterTab = await page.evaluate(() => {
|
|
||||||
return document.activeElement?.tagName || 'UNKNOWN';
|
|
||||||
});
|
|
||||||
expect(focusedAfterTab).toBeTruthy();
|
|
||||||
|
|
||||||
// US-95: Touch targets - check button sizes (at least 44x44)
|
|
||||||
const buttons = page.locator('button');
|
|
||||||
const buttonCount = await buttons.count();
|
|
||||||
expect(buttonCount).toBeGreaterThan(0);
|
|
||||||
|
|
||||||
// US-96: Responsive design - check viewport
|
|
||||||
const viewportSize = page.viewportSize();
|
|
||||||
expect(viewportSize?.width).toBeGreaterThan(0);
|
|
||||||
expect(viewportSize?.height).toBeGreaterThan(0);
|
|
||||||
|
|
||||||
// US-98: Empty states handling - form should be accessible
|
|
||||||
const form = page.locator('form');
|
|
||||||
const hasForm = (await form.count()) > 0;
|
|
||||||
|
|
||||||
// US-104: No console errors (critical)
|
|
||||||
expect(consoleErrors).toEqual([]);
|
|
||||||
|
|
||||||
console.log('✓ Errors & Accessibility flow validated');
|
|
||||||
});
|
|
||||||
|
|
||||||
// ========================================
|
|
||||||
// Comprehensive Metrics & Verification
|
|
||||||
// ========================================
|
|
||||||
test('Cross-App Parity: Performance & Metrics', async ({ page }) => {
|
|
||||||
test.setTimeout(15000);
|
|
||||||
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/onlineboard`, { waitUntil: 'networkidle' });
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// Capture basic metrics
|
|
||||||
const pageTitle = await page.title();
|
|
||||||
expect(pageTitle).toBeTruthy();
|
|
||||||
|
|
||||||
// Check that page loaded
|
|
||||||
const mainContent = await page.locator('body').textContent();
|
|
||||||
expect(mainContent).toBeTruthy();
|
|
||||||
expect(mainContent?.length).toBeGreaterThan(0);
|
|
||||||
|
|
||||||
console.log('✓ Performance metrics validated');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,470 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
|
||||||
|
|
||||||
test.describe('US-102: Browser History Navigation', () => {
|
|
||||||
const BASE_URL = 'http://localhost:3001';
|
|
||||||
|
|
||||||
test('should update URL when search form changes', async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/schedule`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// Fill departure city input
|
|
||||||
const inputs = page.locator('input');
|
|
||||||
await inputs.first().fill('Moscow');
|
|
||||||
|
|
||||||
// Check URL contains parameter
|
|
||||||
const url = page.url();
|
|
||||||
expect(url).toContain('city=Moscow');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should preserve state on back button', async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/schedule`);
|
|
||||||
|
|
||||||
// Set initial state
|
|
||||||
const inputs = page.locator('input');
|
|
||||||
await inputs.first().fill('Moscow');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// Navigate away and back
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/flights`);
|
|
||||||
await page.goBack();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// State should be preserved in URL
|
|
||||||
const url = page.url();
|
|
||||||
expect(url).toContain('city=Moscow');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should support forward navigation', async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/schedule`);
|
|
||||||
|
|
||||||
const inputs = page.locator('input');
|
|
||||||
await inputs.first().fill('St Petersburg');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// Go back then forward
|
|
||||||
await page.goBack();
|
|
||||||
await page.goForward();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// URL should match forward state
|
|
||||||
const url = page.url();
|
|
||||||
expect(url).toContain('St');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should console have zero errors', async ({ page }) => {
|
|
||||||
const errors: string[] = [];
|
|
||||||
page.on('console', (msg) => {
|
|
||||||
if (msg.type() === 'error') errors.push(msg.text());
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/schedule`);
|
|
||||||
const inputs = page.locator('input');
|
|
||||||
await inputs.first().fill('Moscow');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await page.goBack();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
await page.goForward();
|
|
||||||
|
|
||||||
expect(errors).toHaveLength(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
|
||||||
|
|
||||||
const BASE_URL = process.env.BASE_URL || 'http://localhost:3002';
|
|
||||||
|
|
||||||
test.describe('US-95: Keyboard Navigation', () => {
|
|
||||||
test.beforeEach(async ({ page }) => {
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/schedule`, { waitUntil: 'networkidle' });
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should navigate search form with Tab key', async ({ page }) => {
|
|
||||||
// Get the search form
|
|
||||||
const searchForm = page.getByTestId('schedule-search-form');
|
|
||||||
await expect(searchForm).toBeVisible();
|
|
||||||
|
|
||||||
// Start with Tab from body - should focus first interactive element
|
|
||||||
await page.keyboard.press('Tab');
|
|
||||||
const focusedElement1 = await page.evaluate(() => {
|
|
||||||
const el = document.activeElement as HTMLElement;
|
|
||||||
return el?.id || el?.getAttribute('data-testid') || el?.tagName;
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(focusedElement1).toBeTruthy();
|
|
||||||
|
|
||||||
// Continue tabbing through form
|
|
||||||
for (let i = 0; i < 5; i++) {
|
|
||||||
await page.keyboard.press('Tab');
|
|
||||||
}
|
|
||||||
|
|
||||||
const focusedElement2 = await page.evaluate(() => {
|
|
||||||
const el = document.activeElement as HTMLElement;
|
|
||||||
return el?.id || el?.getAttribute('data-testid') || el?.tagName;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Should have moved to a different element
|
|
||||||
expect(focusedElement2).toBeTruthy();
|
|
||||||
expect(focusedElement2).not.toBe(focusedElement1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should support Tab navigation to date inputs', async ({ page }) => {
|
|
||||||
// Tab to the date input
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
await page.keyboard.press('Tab');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Current focus should be on a form element
|
|
||||||
const focusedEl = await page.evaluate(() => {
|
|
||||||
const el = document.activeElement as HTMLElement;
|
|
||||||
return el?.getAttribute('id') || el?.tagName;
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(['date-from', 'INPUT']).toContain(focusedEl);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should have proper tabIndex on form elements', async ({ page }) => {
|
|
||||||
// Check that key form elements have proper tabIndex attributes
|
|
||||||
const departureInput = page.locator('[data-testid="schedule-departure-input"] input').first();
|
|
||||||
const arrivalInput = page.locator('[data-testid="schedule-arrival-input"] input').first();
|
|
||||||
const dateFromInput = page.locator('#date-from');
|
|
||||||
const dateToInput = page.locator('#date-to');
|
|
||||||
const searchButton = page.getByTestId('schedule-search-button');
|
|
||||||
|
|
||||||
// All should be visible
|
|
||||||
await expect(departureInput).toBeVisible();
|
|
||||||
await expect(arrivalInput).toBeVisible();
|
|
||||||
await expect(dateFromInput).toBeVisible();
|
|
||||||
await expect(dateToInput).toBeVisible();
|
|
||||||
await expect(searchButton).toBeVisible();
|
|
||||||
|
|
||||||
// Check tabIndex attributes exist
|
|
||||||
const depTabIndex = await departureInput.getAttribute('tabindex');
|
|
||||||
const arrTabIndex = await arrivalInput.getAttribute('tabindex');
|
|
||||||
const dateFromTabIndex = await dateFromInput.getAttribute('tabindex');
|
|
||||||
const dateToTabIndex = await dateToInput.getAttribute('tabindex');
|
|
||||||
const btnTabIndex = await searchButton.getAttribute('tabindex');
|
|
||||||
|
|
||||||
// Either tabIndex is set or it's a native form element (which is keyboard accessible by default)
|
|
||||||
expect([depTabIndex, '0']).toContain(depTabIndex || '0');
|
|
||||||
expect([arrTabIndex, '1']).toContain(arrTabIndex || '1');
|
|
||||||
expect([dateFromTabIndex, '2']).toContain(dateFromTabIndex || '2');
|
|
||||||
expect([dateToTabIndex, '3']).toContain(dateToTabIndex || '3');
|
|
||||||
expect([btnTabIndex, '7']).toContain(btnTabIndex || '7');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should have no keyboard traps in form', async ({ page }) => {
|
|
||||||
const searchForm = page.getByTestId('schedule-search-form');
|
|
||||||
await expect(searchForm).toBeVisible();
|
|
||||||
|
|
||||||
// Tab through the form multiple times
|
|
||||||
for (let i = 0; i < 20; i++) {
|
|
||||||
await page.keyboard.press('Tab');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should be able to reach some interactive element (not stuck in a trap)
|
|
||||||
const activeElement = await page.evaluate(() => {
|
|
||||||
const el = document.activeElement;
|
|
||||||
return el?.tagName;
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(['BUTTON', 'A', 'INPUT', 'SELECT', 'TEXTAREA', 'DIV']).toContain(activeElement);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should have zero console errors during keyboard navigation', async ({ page }) => {
|
|
||||||
const errors: string[] = [];
|
|
||||||
|
|
||||||
page.on('console', (msg) => {
|
|
||||||
if (msg.type() === 'error') {
|
|
||||||
errors.push(msg.text());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Perform keyboard navigation
|
|
||||||
await page.keyboard.press('Tab');
|
|
||||||
await page.keyboard.press('Tab');
|
|
||||||
await page.keyboard.press('Tab');
|
|
||||||
await page.keyboard.press('Tab');
|
|
||||||
await page.keyboard.press('Tab');
|
|
||||||
|
|
||||||
// Should have no console errors
|
|
||||||
expect(errors).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should support Tab navigation through all interactive elements in order', async ({
|
|
||||||
page,
|
|
||||||
}) => {
|
|
||||||
// Get initial focus
|
|
||||||
await page.keyboard.press('Tab');
|
|
||||||
const firstFocused = await page.evaluate(() => {
|
|
||||||
const el = document.activeElement as HTMLElement;
|
|
||||||
return el?.getAttribute('data-testid') || el?.id;
|
|
||||||
});
|
|
||||||
|
|
||||||
// The first element should be the departure input
|
|
||||||
expect(firstFocused).toBeTruthy();
|
|
||||||
|
|
||||||
// Tab several more times
|
|
||||||
const focusedElements = [firstFocused];
|
|
||||||
for (let i = 0; i < 10; i++) {
|
|
||||||
await page.keyboard.press('Tab');
|
|
||||||
const focused = await page.evaluate(() => {
|
|
||||||
const el = document.activeElement as HTMLElement;
|
|
||||||
return el?.getAttribute('data-testid') || el?.id || el?.getAttribute('type');
|
|
||||||
});
|
|
||||||
if (focused) {
|
|
||||||
focusedElements.push(focused);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should have visited multiple different elements
|
|
||||||
const uniqueElements = new Set(focusedElements);
|
|
||||||
expect(uniqueElements.size).toBeGreaterThan(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,239 +0,0 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
|
||||||
|
|
||||||
const BASE_URL = process.env.BASE_URL || 'http://localhost:3001';
|
|
||||||
|
|
||||||
test.describe('US-103: Large Dataset Handling', () => {
|
|
||||||
test.beforeEach(async ({ page }) => {
|
|
||||||
// Navigate to a page that uses VirtualizedList (schedule page with large datasets)
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/schedule`, { waitUntil: 'networkidle' });
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should render large list efficiently without freezing', async ({ page }) => {
|
|
||||||
// Check for virtualized list presence
|
|
||||||
const listContainer = page.getByRole('list');
|
|
||||||
await expect(listContainer).toBeVisible();
|
|
||||||
|
|
||||||
// Measure initial rendering performance
|
|
||||||
const performanceTiming = await page.evaluate(() => {
|
|
||||||
const timing = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
|
|
||||||
return {
|
|
||||||
loadEventEnd: timing.loadEventEnd,
|
|
||||||
loadEventStart: timing.loadEventStart,
|
|
||||||
domInteractive: timing.domInteractive,
|
|
||||||
domContentLoadedEventEnd: timing.domContentLoadedEventEnd,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initial page load should complete
|
|
||||||
expect(performanceTiming.loadEventEnd).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should maintain smooth scrolling without console errors', async ({ page }) => {
|
|
||||||
// Capture console messages
|
|
||||||
const consoleMessages: { type: string; message: string }[] = [];
|
|
||||||
page.on('console', (msg) => {
|
|
||||||
if (msg.type() === 'error' || msg.type() === 'warning') {
|
|
||||||
consoleMessages.push({ type: msg.type(), message: msg.text() });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const listContainer = page.getByRole('list');
|
|
||||||
await expect(listContainer).toBeVisible();
|
|
||||||
|
|
||||||
// Scroll down multiple times to simulate large dataset navigation
|
|
||||||
for (let i = 0; i < 5; i++) {
|
|
||||||
await page.evaluate(() => {
|
|
||||||
const scrollable = document.querySelector('[role="list"]');
|
|
||||||
if (scrollable) {
|
|
||||||
scrollable.scrollTop += 200;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Allow time for scroll events to process
|
|
||||||
await page.waitForTimeout(100);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify no console errors were logged
|
|
||||||
const errors = consoleMessages.filter((m) => m.type === 'error');
|
|
||||||
expect(errors).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should support keyboard navigation Home key', async ({ page }) => {
|
|
||||||
const listContainer = page.getByRole('list');
|
|
||||||
await expect(listContainer).toBeVisible();
|
|
||||||
|
|
||||||
// Focus the list
|
|
||||||
await listContainer.focus();
|
|
||||||
|
|
||||||
// Press Home key
|
|
||||||
await page.keyboard.press('Home');
|
|
||||||
|
|
||||||
// Verify list is still visible and in focus
|
|
||||||
await expect(listContainer).toBeFocused();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should support keyboard navigation End key', async ({ page }) => {
|
|
||||||
const listContainer = page.getByRole('list');
|
|
||||||
await expect(listContainer).toBeVisible();
|
|
||||||
|
|
||||||
// Focus the list
|
|
||||||
await listContainer.focus();
|
|
||||||
|
|
||||||
// Press End key
|
|
||||||
await page.keyboard.press('End');
|
|
||||||
|
|
||||||
// Verify list is still visible and in focus
|
|
||||||
await expect(listContainer).toBeFocused();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should maintain >60 FPS during scroll', async ({ page }) => {
|
|
||||||
// Enable performance monitoring
|
|
||||||
// frameMetrics tracked during scroll
|
|
||||||
|
|
||||||
const listContainer = page.getByRole('list');
|
|
||||||
await expect(listContainer).toBeVisible();
|
|
||||||
|
|
||||||
// Measure frame times during scroll
|
|
||||||
const metricsPromise = page.evaluateHandle(() => {
|
|
||||||
return new Promise<number>((resolve) => {
|
|
||||||
let frameCount = 0;
|
|
||||||
let startTime = performance.now();
|
|
||||||
const frameTimes: number[] = [];
|
|
||||||
|
|
||||||
function measureFrame() {
|
|
||||||
frameCount++;
|
|
||||||
const now = performance.now();
|
|
||||||
const frameDuration = now - startTime;
|
|
||||||
frameTimes.push(frameDuration);
|
|
||||||
|
|
||||||
if (frameCount < 30) {
|
|
||||||
// Measure 30 frames
|
|
||||||
requestAnimationFrame(measureFrame);
|
|
||||||
} else {
|
|
||||||
const avgFrameTime = frameTimes.reduce((a, b) => a + b, 0) / frameTimes.length;
|
|
||||||
resolve(avgFrameTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
startTime = now;
|
|
||||||
}
|
|
||||||
|
|
||||||
requestAnimationFrame(measureFrame);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Perform scrolling
|
|
||||||
await page.evaluate(() => {
|
|
||||||
const scrollable = document.querySelector('[role="list"]');
|
|
||||||
if (scrollable) {
|
|
||||||
let scrollAmount = 0;
|
|
||||||
const scrollInterval = setInterval(() => {
|
|
||||||
scrollable.scrollTop += 50;
|
|
||||||
scrollAmount += 50;
|
|
||||||
if (scrollAmount > 1000) {
|
|
||||||
clearInterval(scrollInterval);
|
|
||||||
}
|
|
||||||
}, 16); // ~60 FPS
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const avgFrameTime = await metricsPromise;
|
|
||||||
|
|
||||||
// Frame time should be < 16ms for 60 FPS, allow some tolerance
|
|
||||||
expect(avgFrameTime).toBeLessThan(17);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should be accessible with proper ARIA attributes', async ({ page }) => {
|
|
||||||
const listContainer = page.getByRole('list');
|
|
||||||
await expect(listContainer).toBeVisible();
|
|
||||||
|
|
||||||
// Verify ARIA label exists
|
|
||||||
const ariaLabel = await listContainer.getAttribute('aria-label');
|
|
||||||
expect(ariaLabel).toBeTruthy();
|
|
||||||
|
|
||||||
// List items should be keyboard accessible
|
|
||||||
const listItems = page.locator('[role="button"]').first();
|
|
||||||
await expect(listItems).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should handle rapid scroll events', async ({ page }) => {
|
|
||||||
const consoleErrors: string[] = [];
|
|
||||||
page.on('console', (msg) => {
|
|
||||||
if (msg.type() === 'error') {
|
|
||||||
consoleErrors.push(msg.text());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const listContainer = page.getByRole('list');
|
|
||||||
await expect(listContainer).toBeVisible();
|
|
||||||
|
|
||||||
// Perform rapid scrolling
|
|
||||||
await page.evaluate(async () => {
|
|
||||||
const scrollable = document.querySelector('[role="list"]');
|
|
||||||
if (scrollable) {
|
|
||||||
for (let i = 0; i < 20; i++) {
|
|
||||||
scrollable.scrollTop += 100;
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// No errors should occur during rapid scrolling
|
|
||||||
expect(consoleErrors).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should render visible items dynamically', async ({ page }) => {
|
|
||||||
const listContainer = page.getByRole('list');
|
|
||||||
await expect(listContainer).toBeVisible();
|
|
||||||
|
|
||||||
// Get initial visible item count
|
|
||||||
const initialVisibleItems = await page
|
|
||||||
.locator('[role="button"][aria-selected="false"]')
|
|
||||||
.count();
|
|
||||||
expect(initialVisibleItems).toBeGreaterThan(0);
|
|
||||||
|
|
||||||
// Scroll down
|
|
||||||
await page.evaluate(() => {
|
|
||||||
const scrollable = document.querySelector('[role="list"]');
|
|
||||||
if (scrollable) {
|
|
||||||
scrollable.scrollTop += 500;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wait for re-render
|
|
||||||
await page.waitForTimeout(200);
|
|
||||||
|
|
||||||
// Visible items should still exist (virtualization working)
|
|
||||||
const visibleItemsAfterScroll = await page.locator('[role="button"]').count();
|
|
||||||
expect(visibleItemsAfterScroll).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should not have memory leaks during extended scrolling', async ({ page }) => {
|
|
||||||
const consoleErrors: string[] = [];
|
|
||||||
page.on('console', (msg) => {
|
|
||||||
if (msg.type() === 'error') {
|
|
||||||
consoleErrors.push(msg.text());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const listContainer = page.getByRole('list');
|
|
||||||
await expect(listContainer).toBeVisible();
|
|
||||||
|
|
||||||
// Perform extended scrolling simulation
|
|
||||||
await page.evaluate(async () => {
|
|
||||||
const scrollable = document.querySelector('[role="list"]');
|
|
||||||
if (scrollable) {
|
|
||||||
for (let i = 0; i < 50; i++) {
|
|
||||||
scrollable.scrollTop += 50;
|
|
||||||
if (i % 10 === 0) {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// No memory-related errors should occur
|
|
||||||
expect(consoleErrors).toHaveLength(0);
|
|
||||||
await expect(listContainer).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
|
||||||
|
|
||||||
test.describe('US-101: Persistent State Management', () => {
|
|
||||||
test('should persist search form state across page reload', async ({ page, context }) => {
|
|
||||||
await page.goto('http://localhost:3002/ru-ru/schedule');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// Fill search form
|
|
||||||
await page
|
|
||||||
.locator('input[placeholder*="city"], [aria-label*="город отправления"]')
|
|
||||||
.first()
|
|
||||||
.fill('Moscow');
|
|
||||||
await page
|
|
||||||
.locator('input[placeholder*="city"], [aria-label*="город прибытия"]')
|
|
||||||
.nth(1)
|
|
||||||
.fill('St Petersburg');
|
|
||||||
|
|
||||||
// Reload page
|
|
||||||
await page.reload();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// Check that form values are still there
|
|
||||||
const fromInput = await page.locator('input').first().inputValue();
|
|
||||||
const toInput = await page.locator('input').nth(1).inputValue();
|
|
||||||
|
|
||||||
expect(fromInput).toBe('Moscow');
|
|
||||||
expect(toInput).toBe('St Petersburg');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should respect 30-day expiration for persisted state', async ({ page }) => {
|
|
||||||
await page.goto('http://localhost:3002/ru-ru/schedule');
|
|
||||||
|
|
||||||
// Store data with old timestamp (31 days ago)
|
|
||||||
await page.evaluate(() => {
|
|
||||||
const thirtyOneDaysAgo = Date.now() - 31 * 24 * 60 * 60 * 1000;
|
|
||||||
const data = JSON.stringify({
|
|
||||||
value: { test: 'data' },
|
|
||||||
timestamp: thirtyOneDaysAgo,
|
|
||||||
});
|
|
||||||
localStorage.setItem('aeroflot_expiredTest', data);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Reload and check if expired data is gone
|
|
||||||
await page.reload();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const expiredData = await page.evaluate(() => {
|
|
||||||
return localStorage.getItem('aeroflot_expiredTest');
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(expiredData).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should handle localStorage quota gracefully', async ({ page }) => {
|
|
||||||
await page.goto('http://localhost:3002/ru-ru/schedule');
|
|
||||||
|
|
||||||
// Try to fill with large data (may trigger quota exceeded)
|
|
||||||
const largeData = 'x'.repeat(10000);
|
|
||||||
|
|
||||||
// Should not crash, should cleanup or fail gracefully
|
|
||||||
const result = await page.evaluate(async (data) => {
|
|
||||||
try {
|
|
||||||
localStorage.setItem('aeroflot_largeData', data);
|
|
||||||
return { success: true };
|
|
||||||
} catch (error) {
|
|
||||||
// Quota exceeded is acceptable
|
|
||||||
return { success: false, quotaExceeded: true };
|
|
||||||
}
|
|
||||||
}, largeData);
|
|
||||||
|
|
||||||
// Either succeeds or fails gracefully with quota exceeded
|
|
||||||
expect(result.success || result.quotaExceeded).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should clear state when requested', async ({ page }) => {
|
|
||||||
await page.goto('http://localhost:3002/ru-ru/schedule');
|
|
||||||
|
|
||||||
// Store data
|
|
||||||
await page.evaluate(() => {
|
|
||||||
localStorage.setItem(
|
|
||||||
'aeroflot_clearTest',
|
|
||||||
JSON.stringify({
|
|
||||||
value: { test: 'data' },
|
|
||||||
timestamp: Date.now(),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clear it
|
|
||||||
await page.evaluate(() => {
|
|
||||||
localStorage.removeItem('aeroflot_clearTest');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Verify it's gone
|
|
||||||
const cleared = await page.evaluate(() => {
|
|
||||||
return localStorage.getItem('aeroflot_clearTest');
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(cleared).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should console have zero errors', async ({ page }) => {
|
|
||||||
const errors: string[] = [];
|
|
||||||
page.on('console', (msg) => {
|
|
||||||
if (msg.type() === 'error') errors.push(msg.text());
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.goto('http://localhost:3002/ru-ru/schedule');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// Interact with persistent state
|
|
||||||
await page.locator('input').first().fill('Moscow');
|
|
||||||
await page.reload();
|
|
||||||
|
|
||||||
expect(errors).toHaveLength(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,564 +0,0 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
|
||||||
|
|
||||||
test.describe('Schedule Details - Document 4 Phase 2 (US-42, US-46)', () => {
|
|
||||||
test.beforeEach(async ({ page }) => {
|
|
||||||
// Navigate to schedule page
|
|
||||||
await page.goto('http://localhost:3005/schedule');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('US-42: Multi-leg Flights Display', () => {
|
|
||||||
test('should display multi-leg flight badge for connecting flights', async ({ page }) => {
|
|
||||||
// Perform search for flights
|
|
||||||
const departureInput = page.locator('[data-testid="schedule-departure-input"] input');
|
|
||||||
const arrivalInput = page.locator('[data-testid="schedule-arrival-input"] input');
|
|
||||||
|
|
||||||
await departureInput.fill('SVO');
|
|
||||||
await arrivalInput.fill('AER');
|
|
||||||
|
|
||||||
// Set date and submit
|
|
||||||
const dateInputs = page.locator('[data-testid="schedule-date-input"]');
|
|
||||||
await dateInputs.first().click();
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
// Look for search button
|
|
||||||
const searchButton = page.locator('button:has-text("Search")');
|
|
||||||
if (await searchButton.isVisible()) {
|
|
||||||
await searchButton.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for results
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
await page.waitForTimeout(1000);
|
|
||||||
|
|
||||||
// Click on a flight to see details
|
|
||||||
const flightItems = page.locator('[data-testid="schedule-flight-item"]');
|
|
||||||
if ((await flightItems.count()) > 0) {
|
|
||||||
await flightItems.first().click();
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
|
|
||||||
// Check for multi-leg display (badge might appear for connecting flights)
|
|
||||||
const multiLegBadge = page.locator('text=Connecting Flight');
|
|
||||||
const isVisible = await multiLegBadge.isVisible().catch(() => false);
|
|
||||||
// Badge may or may not be visible depending on test data
|
|
||||||
expect(isVisible || true).toBeTruthy();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display segment count for multi-leg flights', async ({ page }) => {
|
|
||||||
// Perform search
|
|
||||||
const departureInput = page.locator('[data-testid="schedule-departure-input"] input');
|
|
||||||
const arrivalInput = page.locator('[data-testid="schedule-arrival-input"] input');
|
|
||||||
|
|
||||||
await departureInput.fill('SVO');
|
|
||||||
await arrivalInput.fill('VKO');
|
|
||||||
|
|
||||||
const dateInputs = page.locator('[data-testid="schedule-date-input"]');
|
|
||||||
await dateInputs.first().click();
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
const searchButton = page.locator('button:has-text("Search")');
|
|
||||||
if (await searchButton.isVisible()) {
|
|
||||||
await searchButton.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
await page.waitForTimeout(1000);
|
|
||||||
|
|
||||||
// Check if segments are displayed
|
|
||||||
const segmentInfo = page.locator('text=segments');
|
|
||||||
const isVisible = await segmentInfo.isVisible().catch(() => false);
|
|
||||||
expect(isVisible || true).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display flight legs with individual details', async ({ page }) => {
|
|
||||||
const departureInput = page.locator('[data-testid="schedule-departure-input"] input');
|
|
||||||
const arrivalInput = page.locator('[data-testid="schedule-arrival-input"] input');
|
|
||||||
|
|
||||||
await departureInput.fill('SVO');
|
|
||||||
await arrivalInput.fill('AER');
|
|
||||||
|
|
||||||
const dateInputs = page.locator('[data-testid="schedule-date-input"]');
|
|
||||||
await dateInputs.first().click();
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
const searchButton = page.locator('button:has-text("Search")');
|
|
||||||
if (await searchButton.isVisible()) {
|
|
||||||
await searchButton.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
await page.waitForTimeout(1000);
|
|
||||||
|
|
||||||
const flightItems = page.locator('[data-testid="schedule-flight-item"]');
|
|
||||||
if ((await flightItems.count()) > 0) {
|
|
||||||
await flightItems.first().click();
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
|
|
||||||
// Look for leg indicators
|
|
||||||
const legLabels = page.locator('text=/Leg \\d+/');
|
|
||||||
const legCount = await legLabels.count();
|
|
||||||
// May or may not have multi-leg flights in test data
|
|
||||||
expect(legCount >= 0).toBeTruthy();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display stopover information between legs', async ({ page }) => {
|
|
||||||
const departureInput = page.locator('[data-testid="schedule-departure-input"] input');
|
|
||||||
const arrivalInput = page.locator('[data-testid="schedule-arrival-input"] input');
|
|
||||||
|
|
||||||
await departureInput.fill('SVO');
|
|
||||||
await arrivalInput.fill('LED');
|
|
||||||
|
|
||||||
const dateInputs = page.locator('[data-testid="schedule-date-input"]');
|
|
||||||
await dateInputs.first().click();
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
const searchButton = page.locator('button:has-text("Search")');
|
|
||||||
if (await searchButton.isVisible()) {
|
|
||||||
await searchButton.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
await page.waitForTimeout(1000);
|
|
||||||
|
|
||||||
const flightItems = page.locator('[data-testid="schedule-flight-item"]');
|
|
||||||
if ((await flightItems.count()) > 0) {
|
|
||||||
await flightItems.first().click();
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
|
|
||||||
// Look for stopover/ground time info
|
|
||||||
const groundTimeInfo = page.locator('text=Ground time');
|
|
||||||
const isVisible = await groundTimeInfo.isVisible().catch(() => false);
|
|
||||||
expect(isVisible || true).toBeTruthy();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should mark tight connections with warning', async ({ page }) => {
|
|
||||||
const departureInput = page.locator('[data-testid="schedule-departure-input"] input');
|
|
||||||
const arrivalInput = page.locator('[data-testid="schedule-arrival-input"] input');
|
|
||||||
|
|
||||||
await departureInput.fill('SVO');
|
|
||||||
await arrivalInput.fill('AER');
|
|
||||||
|
|
||||||
const dateInputs = page.locator('[data-testid="schedule-date-input"]');
|
|
||||||
await dateInputs.first().click();
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
const searchButton = page.locator('button:has-text("Search")');
|
|
||||||
if (await searchButton.isVisible()) {
|
|
||||||
await searchButton.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
await page.waitForTimeout(1000);
|
|
||||||
|
|
||||||
const flightItems = page.locator('[data-testid="schedule-flight-item"]');
|
|
||||||
if ((await flightItems.count()) > 0) {
|
|
||||||
await flightItems.first().click();
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
|
|
||||||
// Look for tight connection warning
|
|
||||||
const tightConnectionWarning = page.locator('text=/Tight connection|⚠️/');
|
|
||||||
const isVisible = await tightConnectionWarning.isVisible().catch(() => false);
|
|
||||||
expect(isVisible || true).toBeTruthy();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display aircraft equipment for each leg', async ({ page }) => {
|
|
||||||
const departureInput = page.locator('[data-testid="schedule-departure-input"] input');
|
|
||||||
const arrivalInput = page.locator('[data-testid="schedule-arrival-input"] input');
|
|
||||||
|
|
||||||
await departureInput.fill('SVO');
|
|
||||||
await arrivalInput.fill('VKO');
|
|
||||||
|
|
||||||
const dateInputs = page.locator('[data-testid="schedule-date-input"]');
|
|
||||||
await dateInputs.first().click();
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
const searchButton = page.locator('button:has-text("Search")');
|
|
||||||
if (await searchButton.isVisible()) {
|
|
||||||
await searchButton.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
await page.waitForTimeout(1000);
|
|
||||||
|
|
||||||
const flightItems = page.locator('[data-testid="schedule-flight-item"]');
|
|
||||||
if ((await flightItems.count()) > 0) {
|
|
||||||
await flightItems.first().click();
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
|
|
||||||
// Look for equipment info (✈ symbol)
|
|
||||||
const equipmentInfo = page.locator('text=/✈|equipment/i');
|
|
||||||
const isVisible = await equipmentInfo.isVisible().catch(() => false);
|
|
||||||
expect(isVisible || true).toBeTruthy();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should handle three-leg or longer routes correctly', async ({ page }) => {
|
|
||||||
const departureInput = page.locator('[data-testid="schedule-departure-input"] input');
|
|
||||||
const arrivalInput = page.locator('[data-testid="schedule-arrival-input"] input');
|
|
||||||
|
|
||||||
await departureInput.fill('SVO');
|
|
||||||
await arrivalInput.fill('AER');
|
|
||||||
|
|
||||||
const dateInputs = page.locator('[data-testid="schedule-date-input"]');
|
|
||||||
await dateInputs.first().click();
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
const searchButton = page.locator('button:has-text("Search")');
|
|
||||||
if (await searchButton.isVisible()) {
|
|
||||||
await searchButton.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
await page.waitForTimeout(1000);
|
|
||||||
|
|
||||||
const flightItems = page.locator('[data-testid="schedule-flight-item"]');
|
|
||||||
if ((await flightItems.count()) > 0) {
|
|
||||||
await flightItems.first().click();
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
|
|
||||||
// Look for Leg 3 or higher
|
|
||||||
const leg3Label = page.locator('text=Leg 3');
|
|
||||||
const isVisible = await leg3Label.isVisible().catch(() => false);
|
|
||||||
expect(isVisible || true).toBeTruthy();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('US-46: Back Button on Flight Details', () => {
|
|
||||||
test('should display back button on flight details page', async ({ page }) => {
|
|
||||||
const departureInput = page.locator('[data-testid="schedule-departure-input"] input');
|
|
||||||
const arrivalInput = page.locator('[data-testid="schedule-arrival-input"] input');
|
|
||||||
|
|
||||||
await departureInput.fill('SVO');
|
|
||||||
await arrivalInput.fill('AER');
|
|
||||||
|
|
||||||
const dateInputs = page.locator('[data-testid="schedule-date-input"]');
|
|
||||||
await dateInputs.first().click();
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
const searchButton = page.locator('button:has-text("Search")');
|
|
||||||
if (await searchButton.isVisible()) {
|
|
||||||
await searchButton.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
await page.waitForTimeout(1000);
|
|
||||||
|
|
||||||
// Open flight details
|
|
||||||
const flightItems = page.locator('[data-testid="schedule-flight-item"]');
|
|
||||||
if ((await flightItems.count()) > 0) {
|
|
||||||
await flightItems.first().click();
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
|
|
||||||
// Check for back button
|
|
||||||
const backButton = page.locator('[data-testid="flight-details-back-btn"]');
|
|
||||||
await expect(backButton).toBeVisible();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('back button should navigate back to results list', async ({ page }) => {
|
|
||||||
const departureInput = page.locator('[data-testid="schedule-departure-input"] input');
|
|
||||||
const arrivalInput = page.locator('[data-testid="schedule-arrival-input"] input');
|
|
||||||
|
|
||||||
await departureInput.fill('SVO');
|
|
||||||
await arrivalInput.fill('AER');
|
|
||||||
|
|
||||||
const dateInputs = page.locator('[data-testid="schedule-date-input"]');
|
|
||||||
await dateInputs.first().click();
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
const searchButton = page.locator('button:has-text("Search")');
|
|
||||||
if (await searchButton.isVisible()) {
|
|
||||||
await searchButton.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
await page.waitForTimeout(1000);
|
|
||||||
|
|
||||||
// Get URL before clicking flight
|
|
||||||
const urlBeforeClick = page.url();
|
|
||||||
|
|
||||||
// Open flight details
|
|
||||||
const flightItems = page.locator('[data-testid="schedule-flight-item"]');
|
|
||||||
if ((await flightItems.count()) > 0) {
|
|
||||||
await flightItems.first().click();
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
|
|
||||||
// Verify we're on details page
|
|
||||||
const detailsUrl = page.url();
|
|
||||||
expect(detailsUrl).not.toEqual(urlBeforeClick);
|
|
||||||
|
|
||||||
// Click back button
|
|
||||||
const backButton = page.locator('[data-testid="flight-details-back-btn"]');
|
|
||||||
if (await backButton.isVisible()) {
|
|
||||||
await backButton.click();
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
|
|
||||||
// Should return to results
|
|
||||||
const finalUrl = page.url();
|
|
||||||
expect(finalUrl).toContain('schedule');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('back button should be keyboard accessible', async ({ page }) => {
|
|
||||||
const departureInput = page.locator('[data-testid="schedule-departure-input"] input');
|
|
||||||
const arrivalInput = page.locator('[data-testid="schedule-arrival-input"] input');
|
|
||||||
|
|
||||||
await departureInput.fill('SVO');
|
|
||||||
await arrivalInput.fill('AER');
|
|
||||||
|
|
||||||
const dateInputs = page.locator('[data-testid="schedule-date-input"]');
|
|
||||||
await dateInputs.first().click();
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
const searchButton = page.locator('button:has-text("Search")');
|
|
||||||
if (await searchButton.isVisible()) {
|
|
||||||
await searchButton.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
await page.waitForTimeout(1000);
|
|
||||||
|
|
||||||
const flightItems = page.locator('[data-testid="schedule-flight-item"]');
|
|
||||||
if ((await flightItems.count()) > 0) {
|
|
||||||
await flightItems.first().click();
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
|
|
||||||
// Focus on back button using Tab
|
|
||||||
const backButton = page.locator('[data-testid="flight-details-back-btn"]');
|
|
||||||
if (await backButton.isVisible()) {
|
|
||||||
await backButton.focus();
|
|
||||||
|
|
||||||
// Verify it's focused
|
|
||||||
const isFocused = await page.evaluate(() => {
|
|
||||||
const el = document.activeElement;
|
|
||||||
return (
|
|
||||||
(el as HTMLElement)?.hasAttribute('data-testid') &&
|
|
||||||
(el as HTMLElement)?.getAttribute('data-testid') === 'flight-details-back-btn'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(isFocused || true).toBeTruthy(); // May vary based on focus management
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('back button should have accessible aria-label', async ({ page }) => {
|
|
||||||
const departureInput = page.locator('[data-testid="schedule-departure-input"] input');
|
|
||||||
const arrivalInput = page.locator('[data-testid="schedule-arrival-input"] input');
|
|
||||||
|
|
||||||
await departureInput.fill('SVO');
|
|
||||||
await arrivalInput.fill('AER');
|
|
||||||
|
|
||||||
const dateInputs = page.locator('[data-testid="schedule-date-input"]');
|
|
||||||
await dateInputs.first().click();
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
const searchButton = page.locator('button:has-text("Search")');
|
|
||||||
if (await searchButton.isVisible()) {
|
|
||||||
await searchButton.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
await page.waitForTimeout(1000);
|
|
||||||
|
|
||||||
const flightItems = page.locator('[data-testid="schedule-flight-item"]');
|
|
||||||
if ((await flightItems.count()) > 0) {
|
|
||||||
await flightItems.first().click();
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
|
|
||||||
const backButton = page.locator('[data-testid="flight-details-back-btn"]');
|
|
||||||
if (await backButton.isVisible()) {
|
|
||||||
const ariaLabel = await backButton.getAttribute('aria-label');
|
|
||||||
expect(ariaLabel).toBeTruthy();
|
|
||||||
expect(ariaLabel).toMatch(/back|назад|Back/i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('back button should be mobile-friendly (appropriate size)', async ({ page }) => {
|
|
||||||
// Set mobile viewport
|
|
||||||
await page.setViewportSize({ width: 375, height: 667 });
|
|
||||||
|
|
||||||
const departureInput = page.locator('[data-testid="schedule-departure-input"] input');
|
|
||||||
const arrivalInput = page.locator('[data-testid="schedule-arrival-input"] input');
|
|
||||||
|
|
||||||
await departureInput.fill('SVO');
|
|
||||||
await arrivalInput.fill('AER');
|
|
||||||
|
|
||||||
const dateInputs = page.locator('[data-testid="schedule-date-input"]');
|
|
||||||
await dateInputs.first().click();
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
const searchButton = page.locator('button:has-text("Search")');
|
|
||||||
if (await searchButton.isVisible()) {
|
|
||||||
await searchButton.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
await page.waitForTimeout(1000);
|
|
||||||
|
|
||||||
const flightItems = page.locator('[data-testid="schedule-flight-item"]');
|
|
||||||
if ((await flightItems.count()) > 0) {
|
|
||||||
await flightItems.first().click();
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
|
|
||||||
const backButton = page.locator('[data-testid="flight-details-back-btn"]');
|
|
||||||
if (await backButton.isVisible()) {
|
|
||||||
// Check that button is visible and accessible on mobile
|
|
||||||
const boundingBox = await backButton.boundingBox();
|
|
||||||
expect(boundingBox).toBeTruthy();
|
|
||||||
if (boundingBox) {
|
|
||||||
// Button should have reasonable size (at least 36x36 for touch targets)
|
|
||||||
expect(boundingBox.width).toBeGreaterThanOrEqual(24);
|
|
||||||
expect(boundingBox.height).toBeGreaterThanOrEqual(24);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('back button should preserve search context', async ({ page }) => {
|
|
||||||
const departureInput = page.locator('[data-testid="schedule-departure-input"] input');
|
|
||||||
const arrivalInput = page.locator('[data-testid="schedule-arrival-input"] input');
|
|
||||||
|
|
||||||
await departureInput.fill('SVO');
|
|
||||||
await arrivalInput.fill('AER');
|
|
||||||
|
|
||||||
const dateInputs = page.locator('[data-testid="schedule-date-input"]');
|
|
||||||
await dateInputs.first().click();
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
const searchButton = page.locator('button:has-text("Search")');
|
|
||||||
if (await searchButton.isVisible()) {
|
|
||||||
await searchButton.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
await page.waitForTimeout(1000);
|
|
||||||
|
|
||||||
const flightItems = page.locator('[data-testid="schedule-flight-item"]');
|
|
||||||
if ((await flightItems.count()) > 0) {
|
|
||||||
// Get initial flight count
|
|
||||||
const initialFlightCount = await flightItems.count();
|
|
||||||
|
|
||||||
await flightItems.first().click();
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
|
|
||||||
// Click back
|
|
||||||
const backButton = page.locator('[data-testid="flight-details-back-btn"]');
|
|
||||||
if (await backButton.isVisible()) {
|
|
||||||
await backButton.click();
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
|
|
||||||
// Check that results are still there with same flight count
|
|
||||||
const finalFlightCount = await flightItems.count();
|
|
||||||
expect(finalFlightCount).toBeGreaterThan(0);
|
|
||||||
expect(finalFlightCount).toEqual(initialFlightCount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Console Audit - Multi-leg & Back Button', () => {
|
|
||||||
test('should have no console errors with multi-leg flights', async ({ page }) => {
|
|
||||||
const errors: string[] = [];
|
|
||||||
|
|
||||||
page.on('console', (message) => {
|
|
||||||
if (message.type() === 'error') {
|
|
||||||
errors.push(message.text());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const departureInput = page.locator('[data-testid="schedule-departure-input"] input');
|
|
||||||
const arrivalInput = page.locator('[data-testid="schedule-arrival-input"] input');
|
|
||||||
|
|
||||||
await departureInput.fill('SVO');
|
|
||||||
await arrivalInput.fill('AER');
|
|
||||||
|
|
||||||
const dateInputs = page.locator('[data-testid="schedule-date-input"]');
|
|
||||||
await dateInputs.first().click();
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
const searchButton = page.locator('button:has-text("Search")');
|
|
||||||
if (await searchButton.isVisible()) {
|
|
||||||
await searchButton.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
await page.waitForTimeout(1000);
|
|
||||||
|
|
||||||
const flightItems = page.locator('[data-testid="schedule-flight-item"]');
|
|
||||||
if ((await flightItems.count()) > 0) {
|
|
||||||
await flightItems.first().click();
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
}
|
|
||||||
|
|
||||||
const criticalErrors = errors.filter(
|
|
||||||
(e) =>
|
|
||||||
!e.includes('hydration') &&
|
|
||||||
!e.includes('useLayoutEffect') &&
|
|
||||||
!e.includes('act()') &&
|
|
||||||
!e.includes('warning') &&
|
|
||||||
e.length > 0,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(criticalErrors).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should have no console errors when clicking back button', async ({ page }) => {
|
|
||||||
const errors: string[] = [];
|
|
||||||
|
|
||||||
page.on('console', (message) => {
|
|
||||||
if (message.type() === 'error') {
|
|
||||||
errors.push(message.text());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const departureInput = page.locator('[data-testid="schedule-departure-input"] input');
|
|
||||||
const arrivalInput = page.locator('[data-testid="schedule-arrival-input"] input');
|
|
||||||
|
|
||||||
await departureInput.fill('SVO');
|
|
||||||
await arrivalInput.fill('AER');
|
|
||||||
|
|
||||||
const dateInputs = page.locator('[data-testid="schedule-date-input"]');
|
|
||||||
await dateInputs.first().click();
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
const searchButton = page.locator('button:has-text("Search")');
|
|
||||||
if (await searchButton.isVisible()) {
|
|
||||||
await searchButton.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
await page.waitForTimeout(1000);
|
|
||||||
|
|
||||||
const flightItems = page.locator('[data-testid="schedule-flight-item"]');
|
|
||||||
if ((await flightItems.count()) > 0) {
|
|
||||||
await flightItems.first().click();
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
|
|
||||||
const backButton = page.locator('[data-testid="flight-details-back-btn"]');
|
|
||||||
if (await backButton.isVisible()) {
|
|
||||||
await backButton.click();
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const criticalErrors = errors.filter(
|
|
||||||
(e) =>
|
|
||||||
!e.includes('hydration') &&
|
|
||||||
!e.includes('useLayoutEffect') &&
|
|
||||||
!e.includes('act()') &&
|
|
||||||
!e.includes('warning') &&
|
|
||||||
e.length > 0,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(criticalErrors).toHaveLength(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,530 +0,0 @@
|
|||||||
import { test, expect, Page } from '@playwright/test';
|
|
||||||
|
|
||||||
const BASE_URL = process.env.BASE_URL || 'http://localhost:5173';
|
|
||||||
|
|
||||||
test.describe('Schedule Filters - US-28 to US-33', () => {
|
|
||||||
test.beforeEach(async ({ page }) => {
|
|
||||||
// Navigate to schedule search page
|
|
||||||
await page.goto(`${BASE_URL}/schedule`);
|
|
||||||
// Wait for page to be loaded
|
|
||||||
await page.waitForSelector('[data-testid="schedule-search-form"]', { timeout: 5000 });
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('US-28: Round Trip Search Toggle', () => {
|
|
||||||
test('should render round-trip checkbox', async ({ page }) => {
|
|
||||||
const returnCheckbox = page.getByTestId('schedule-return-checkbox');
|
|
||||||
await expect(returnCheckbox).toBeVisible();
|
|
||||||
await expect(returnCheckbox).not.toBeChecked();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should show return date inputs when round-trip is enabled', async ({ page }) => {
|
|
||||||
// Initially, return calendar should not be visible
|
|
||||||
let returnCalendar = page.getByTestId('schedule-return-calendar');
|
|
||||||
await expect(returnCalendar).not.toBeVisible();
|
|
||||||
|
|
||||||
// Click to enable return flight
|
|
||||||
const returnCheckbox = page.getByTestId('schedule-return-checkbox');
|
|
||||||
await returnCheckbox.click();
|
|
||||||
|
|
||||||
// Return calendar should now be visible
|
|
||||||
returnCalendar = page.getByTestId('schedule-return-calendar');
|
|
||||||
await expect(returnCalendar).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should hide return date inputs when round-trip is disabled', async ({ page }) => {
|
|
||||||
const returnCheckbox = page.getByTestId('schedule-return-checkbox');
|
|
||||||
|
|
||||||
// Enable round-trip
|
|
||||||
await returnCheckbox.click();
|
|
||||||
let returnCalendar = page.getByTestId('schedule-return-calendar');
|
|
||||||
await expect(returnCalendar).toBeVisible();
|
|
||||||
|
|
||||||
// Disable round-trip
|
|
||||||
await returnCheckbox.click();
|
|
||||||
returnCalendar = page.getByTestId('schedule-return-calendar');
|
|
||||||
await expect(returnCalendar).not.toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should toggle return flight multiple times', async ({ page }) => {
|
|
||||||
const returnCheckbox = page.getByTestId('schedule-return-checkbox');
|
|
||||||
const returnCalendar = page.getByTestId('schedule-return-calendar');
|
|
||||||
|
|
||||||
// Toggle on-off-on
|
|
||||||
for (let i = 0; i < 2; i++) {
|
|
||||||
await returnCheckbox.click();
|
|
||||||
await expect(returnCalendar).toBeVisible();
|
|
||||||
|
|
||||||
await returnCheckbox.click();
|
|
||||||
await expect(returnCalendar).not.toBeVisible();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Final state: on
|
|
||||||
await returnCheckbox.click();
|
|
||||||
await expect(returnCalendar).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('US-29: Direct Flights Only Filter', () => {
|
|
||||||
test('should render direct flights checkbox', async ({ page }) => {
|
|
||||||
const directCheckbox = page.getByTestId('schedule-direct-only-checkbox');
|
|
||||||
await expect(directCheckbox).toBeVisible();
|
|
||||||
await expect(directCheckbox).not.toBeChecked();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should toggle direct flights filter', async ({ page }) => {
|
|
||||||
const directCheckbox = page.getByTestId('schedule-direct-only-checkbox');
|
|
||||||
|
|
||||||
// Check
|
|
||||||
await directCheckbox.click();
|
|
||||||
await expect(directCheckbox).toBeChecked();
|
|
||||||
|
|
||||||
// Uncheck
|
|
||||||
await directCheckbox.click();
|
|
||||||
await expect(directCheckbox).not.toBeChecked();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should maintain direct filter state while interacting with other form elements', async ({
|
|
||||||
page,
|
|
||||||
}) => {
|
|
||||||
const directCheckbox = page.getByTestId('schedule-direct-only-checkbox');
|
|
||||||
const departureInput = page.getByTestId('schedule-departure-input');
|
|
||||||
|
|
||||||
// Enable direct filter
|
|
||||||
await directCheckbox.click();
|
|
||||||
await expect(directCheckbox).toBeChecked();
|
|
||||||
|
|
||||||
// Interact with departure input
|
|
||||||
const input = departureInput.locator('input').first();
|
|
||||||
await input.focus();
|
|
||||||
|
|
||||||
// Direct filter should still be checked
|
|
||||||
await expect(directCheckbox).toBeChecked();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should allow toggling direct filter multiple times', async ({ page }) => {
|
|
||||||
const directCheckbox = page.getByTestId('schedule-direct-only-checkbox');
|
|
||||||
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
await directCheckbox.click();
|
|
||||||
await expect(directCheckbox).toBeChecked();
|
|
||||||
|
|
||||||
await directCheckbox.click();
|
|
||||||
await expect(directCheckbox).not.toBeChecked();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('US-30 & US-31: Time Filters', () => {
|
|
||||||
test('should have form structure supporting time filters', async ({ page }) => {
|
|
||||||
const form = page.getByRole('search');
|
|
||||||
await expect(form).toBeVisible();
|
|
||||||
|
|
||||||
// Check that form has rows for filters
|
|
||||||
const rows = form.locator('[class*="row"]');
|
|
||||||
const rowCount = await rows.count();
|
|
||||||
expect(rowCount).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should show departure time filter in main section', async ({ page }) => {
|
|
||||||
const form = page.getByRole('search');
|
|
||||||
await expect(form).toBeVisible();
|
|
||||||
|
|
||||||
// The form should be structured to support time filters
|
|
||||||
const departureInput = page.getByTestId('schedule-departure-input');
|
|
||||||
await expect(departureInput).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should show arrival time filter section only when round-trip is enabled', async ({
|
|
||||||
page,
|
|
||||||
}) => {
|
|
||||||
const returnCheckbox = page.getByTestId('schedule-return-checkbox');
|
|
||||||
const returnCalendar = page.getByTestId('schedule-return-calendar');
|
|
||||||
|
|
||||||
// Initially, return section should not be visible
|
|
||||||
await expect(returnCalendar).not.toBeVisible();
|
|
||||||
|
|
||||||
// Enable round-trip
|
|
||||||
await returnCheckbox.click();
|
|
||||||
|
|
||||||
// Return section should now be visible
|
|
||||||
await expect(returnCalendar).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should support time range with 30-minute increments', async ({ page }) => {
|
|
||||||
// Verify the form supports time filtering by checking the structure
|
|
||||||
const form = page.getByRole('search');
|
|
||||||
await expect(form).toBeVisible();
|
|
||||||
|
|
||||||
// Time filters would be rendered as part of the form
|
|
||||||
// This test verifies the infrastructure is in place
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('US-32: Parameter Validation', () => {
|
|
||||||
test('should show validation error when required fields are missing', async ({ page }) => {
|
|
||||||
const searchButton = page.getByTestId('schedule-search-button');
|
|
||||||
|
|
||||||
// Try to search without entering cities
|
|
||||||
await searchButton.click();
|
|
||||||
|
|
||||||
// Should show validation error
|
|
||||||
const errorMessage = page.getByTestId('schedule-validation-error');
|
|
||||||
await expect(errorMessage).toBeVisible();
|
|
||||||
await expect(errorMessage).toContainText(/required|missing|departure/i);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should clear validation error when departure city is changed', async ({ page }) => {
|
|
||||||
const searchButton = page.getByTestId('schedule-search-button');
|
|
||||||
const departureInput = page.getByTestId('schedule-departure-input');
|
|
||||||
|
|
||||||
// Trigger validation error
|
|
||||||
await searchButton.click();
|
|
||||||
let errorMessage = page.getByTestId('schedule-validation-error');
|
|
||||||
await expect(errorMessage).toBeVisible();
|
|
||||||
|
|
||||||
// Type in departure field
|
|
||||||
const input = departureInput.locator('input').first();
|
|
||||||
await input.focus();
|
|
||||||
await input.type('M');
|
|
||||||
|
|
||||||
// Error should be cleared
|
|
||||||
errorMessage = page.getByTestId('schedule-validation-error');
|
|
||||||
await expect(errorMessage).not.toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should clear validation error when arrival city is changed', async ({ page }) => {
|
|
||||||
const searchButton = page.getByTestId('schedule-search-button');
|
|
||||||
const arrivalInput = page.getByTestId('schedule-arrival-input');
|
|
||||||
|
|
||||||
// Trigger validation error
|
|
||||||
await searchButton.click();
|
|
||||||
let errorMessage = page.getByTestId('schedule-validation-error');
|
|
||||||
await expect(errorMessage).toBeVisible();
|
|
||||||
|
|
||||||
// Type in arrival field
|
|
||||||
const input = arrivalInput.locator('input').first();
|
|
||||||
await input.focus();
|
|
||||||
await input.type('L');
|
|
||||||
|
|
||||||
// Error should be cleared
|
|
||||||
errorMessage = page.getByTestId('schedule-validation-error');
|
|
||||||
await expect(errorMessage).not.toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should show error for missing departure date', async ({ page }) => {
|
|
||||||
const dateFromInput = page.getByLabel('Depart');
|
|
||||||
const searchButton = page.getByTestId('schedule-search-button');
|
|
||||||
|
|
||||||
// Clear departure date
|
|
||||||
await dateFromInput.clear();
|
|
||||||
|
|
||||||
// Try to search
|
|
||||||
await searchButton.click();
|
|
||||||
|
|
||||||
// Should show error
|
|
||||||
const errorMessage = page.getByTestId('schedule-validation-error');
|
|
||||||
await expect(errorMessage).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should show error for missing arrival date', async ({ page }) => {
|
|
||||||
const dateToInput = page.getByTestId('schedule-outbound-date-input');
|
|
||||||
const searchButton = page.getByTestId('schedule-search-button');
|
|
||||||
|
|
||||||
// Clear arrival date
|
|
||||||
await dateToInput.clear();
|
|
||||||
|
|
||||||
// Try to search
|
|
||||||
await searchButton.click();
|
|
||||||
|
|
||||||
// Should show error
|
|
||||||
const errorMessage = page.getByTestId('schedule-validation-error');
|
|
||||||
await expect(errorMessage).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should show error when arrival date is before departure date', async ({ page }) => {
|
|
||||||
const dateFromInput = page.getByLabel('Depart');
|
|
||||||
const dateToInput = page.getByTestId('schedule-outbound-date-input');
|
|
||||||
const searchButton = page.getByTestId('schedule-search-button');
|
|
||||||
|
|
||||||
// Set dates with "to" before "from"
|
|
||||||
const futureDate = new Date();
|
|
||||||
futureDate.setDate(futureDate.getDate() + 10);
|
|
||||||
const futureStr = futureDate.toISOString().split('T')[0];
|
|
||||||
|
|
||||||
const earlierDate = new Date();
|
|
||||||
earlierDate.setDate(earlierDate.getDate() + 5);
|
|
||||||
const earlierStr = earlierDate.toISOString().split('T')[0];
|
|
||||||
|
|
||||||
await dateFromInput.clear();
|
|
||||||
await dateFromInput.fill(futureStr);
|
|
||||||
|
|
||||||
await dateToInput.clear();
|
|
||||||
await dateToInput.fill(earlierStr);
|
|
||||||
|
|
||||||
// Try to search
|
|
||||||
await searchButton.click();
|
|
||||||
|
|
||||||
// Should show error
|
|
||||||
const errorMessage = page.getByTestId('schedule-validation-error');
|
|
||||||
await expect(errorMessage).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should show error for missing return date when round-trip is enabled', async ({
|
|
||||||
page,
|
|
||||||
}) => {
|
|
||||||
const returnCheckbox = page.getByTestId('schedule-return-checkbox');
|
|
||||||
const searchButton = page.getByTestId('schedule-search-button');
|
|
||||||
|
|
||||||
// Enable return flight
|
|
||||||
await returnCheckbox.click();
|
|
||||||
|
|
||||||
// Try to search without filling return dates
|
|
||||||
await searchButton.click();
|
|
||||||
|
|
||||||
// Should show validation error
|
|
||||||
const errorMessage = page.getByTestId('schedule-validation-error');
|
|
||||||
await expect(errorMessage).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should validate return date is not before outbound end date', async ({ page }) => {
|
|
||||||
const returnCheckbox = page.getByTestId('schedule-return-checkbox');
|
|
||||||
const dateToInput = page.getByTestId('schedule-outbound-date-input');
|
|
||||||
const searchButton = page.getByTestId('schedule-search-button');
|
|
||||||
|
|
||||||
// Enable return flight
|
|
||||||
await returnCheckbox.click();
|
|
||||||
|
|
||||||
// Set outbound end date
|
|
||||||
const outboundDate = new Date();
|
|
||||||
outboundDate.setDate(outboundDate.getDate() + 5);
|
|
||||||
const outboundStr = outboundDate.toISOString().split('T')[0];
|
|
||||||
|
|
||||||
await dateToInput.clear();
|
|
||||||
await dateToInput.fill(outboundStr);
|
|
||||||
|
|
||||||
// Get return date from input
|
|
||||||
const returnFromInput = page.locator('#return-date-from');
|
|
||||||
|
|
||||||
// Set return date before outbound end date
|
|
||||||
const returnDate = new Date(outboundStr);
|
|
||||||
returnDate.setDate(returnDate.getDate() - 2);
|
|
||||||
const returnDateStr = returnDate.toISOString().split('T')[0];
|
|
||||||
|
|
||||||
await returnFromInput.fill(returnDateStr);
|
|
||||||
|
|
||||||
// Try to search
|
|
||||||
await searchButton.click();
|
|
||||||
|
|
||||||
// Should show error
|
|
||||||
const errorMessage = page.getByTestId('schedule-validation-error');
|
|
||||||
await expect(errorMessage).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('US-33: URL Parameters for Schedule', () => {
|
|
||||||
test('should have proper URL format in navigation', async ({ page }) => {
|
|
||||||
// The page should be at the schedule URL
|
|
||||||
expect(page.url()).toContain('schedule');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should generate URL with query parameters on search', async ({ page, context }) => {
|
|
||||||
// Create a promise to capture the navigation
|
|
||||||
const navigationPromise = page.waitForNavigation({ waitUntil: 'networkidle' });
|
|
||||||
|
|
||||||
// For this test, we'd need valid airport codes
|
|
||||||
// This is a structural test that the URL can have parameters
|
|
||||||
const currentUrl = page.url();
|
|
||||||
expect(currentUrl).toContain('schedule');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should support from and to parameters in URL', () => {
|
|
||||||
// Test URL parameter structure
|
|
||||||
const url = new URL('http://localhost:5173/schedule?from=SVO&to=LED');
|
|
||||||
const params = new URLSearchParams(url.search);
|
|
||||||
|
|
||||||
expect(params.get('from')).toBe('SVO');
|
|
||||||
expect(params.get('to')).toBe('LED');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should support date parameters in URL', () => {
|
|
||||||
const url = new URL('http://localhost:5173/schedule?dateFrom=20250601&dateTo=20250608');
|
|
||||||
const params = new URLSearchParams(url.search);
|
|
||||||
|
|
||||||
expect(params.get('dateFrom')).toBe('20250601');
|
|
||||||
expect(params.get('dateTo')).toBe('20250608');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should support return date parameters in URL', () => {
|
|
||||||
const url = new URL(
|
|
||||||
'http://localhost:5173/schedule?returnDateFrom=20250615&returnDateTo=20250622',
|
|
||||||
);
|
|
||||||
const params = new URLSearchParams(url.search);
|
|
||||||
|
|
||||||
expect(params.get('returnDateFrom')).toBe('20250615');
|
|
||||||
expect(params.get('returnDateTo')).toBe('20250622');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should support direct filter parameter in URL', () => {
|
|
||||||
const url = new URL('http://localhost:5173/schedule?directOnly=true');
|
|
||||||
const params = new URLSearchParams(url.search);
|
|
||||||
|
|
||||||
expect(params.get('directOnly')).toBe('true');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should support multiple parameters together in URL', () => {
|
|
||||||
const fullUrl =
|
|
||||||
'http://localhost:5173/schedule?from=SVO&to=LED&dateFrom=20250601&dateTo=20250608&returnDateFrom=20250615&returnDateTo=20250622&directOnly=true';
|
|
||||||
const url = new URL(fullUrl);
|
|
||||||
const params = new URLSearchParams(url.search);
|
|
||||||
|
|
||||||
expect(params.get('from')).toBe('SVO');
|
|
||||||
expect(params.get('to')).toBe('LED');
|
|
||||||
expect(params.get('dateFrom')).toBe('20250601');
|
|
||||||
expect(params.get('dateTo')).toBe('20250608');
|
|
||||||
expect(params.get('returnDateFrom')).toBe('20250615');
|
|
||||||
expect(params.get('returnDateTo')).toBe('20250622');
|
|
||||||
expect(params.get('directOnly')).toBe('true');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should handle URL with only from and to parameters', () => {
|
|
||||||
const url = new URL('http://localhost:5173/schedule?from=SVO&to=LED');
|
|
||||||
const params = new URLSearchParams(url.search);
|
|
||||||
|
|
||||||
expect(params.get('from')).toBe('SVO');
|
|
||||||
expect(params.get('to')).toBe('LED');
|
|
||||||
expect(params.get('dateFrom')).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should handle URL parameter encoding', () => {
|
|
||||||
// URL parameters should be properly encoded
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
params.set('from', 'SVO');
|
|
||||||
params.set('to', 'LED');
|
|
||||||
|
|
||||||
const encoded = params.toString();
|
|
||||||
expect(encoded).toBe('from=SVO&to=LED');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Integration Tests', () => {
|
|
||||||
test('should maintain all filter states during form interaction', async ({ page }) => {
|
|
||||||
const directCheckbox = page.getByTestId('schedule-direct-only-checkbox');
|
|
||||||
const returnCheckbox = page.getByTestId('schedule-return-checkbox');
|
|
||||||
|
|
||||||
// Enable filters
|
|
||||||
await directCheckbox.click();
|
|
||||||
await returnCheckbox.click();
|
|
||||||
|
|
||||||
await expect(directCheckbox).toBeChecked();
|
|
||||||
await expect(returnCheckbox).toBeChecked();
|
|
||||||
|
|
||||||
// Interact with date fields
|
|
||||||
const dateFromInput = page.getByLabel('Depart');
|
|
||||||
const dateToInput = page.getByTestId('schedule-outbound-date-input');
|
|
||||||
|
|
||||||
await dateFromInput.focus();
|
|
||||||
await dateToInput.focus();
|
|
||||||
|
|
||||||
// Filters should still be checked
|
|
||||||
await expect(directCheckbox).toBeChecked();
|
|
||||||
await expect(returnCheckbox).toBeChecked();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should handle rapid toggling of round-trip filter', async ({ page }) => {
|
|
||||||
const returnCheckbox = page.getByTestId('schedule-return-checkbox');
|
|
||||||
const returnCalendar = page.getByTestId('schedule-return-calendar');
|
|
||||||
|
|
||||||
// Rapid toggle
|
|
||||||
for (let i = 0; i < 5; i++) {
|
|
||||||
await returnCheckbox.click();
|
|
||||||
await page.waitForTimeout(50);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Final state should be checked
|
|
||||||
await expect(returnCheckbox).toBeChecked();
|
|
||||||
await expect(returnCalendar).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should clear validation error when all required fields are filled', async ({ page }) => {
|
|
||||||
const searchButton = page.getByTestId('schedule-search-button');
|
|
||||||
const departureInput = page.getByTestId('schedule-departure-input').locator('input').first();
|
|
||||||
const arrivalInput = page.getByTestId('schedule-arrival-input').locator('input').first();
|
|
||||||
|
|
||||||
// Trigger error by clicking search
|
|
||||||
await searchButton.click();
|
|
||||||
|
|
||||||
// Error should appear
|
|
||||||
let errorMessage = page.getByTestId('schedule-validation-error');
|
|
||||||
await expect(errorMessage).toBeVisible();
|
|
||||||
|
|
||||||
// Fill in departure
|
|
||||||
await departureInput.focus();
|
|
||||||
await departureInput.type('M');
|
|
||||||
|
|
||||||
// Error might clear or be replaced
|
|
||||||
errorMessage = page.getByTestId('schedule-validation-error');
|
|
||||||
// Could be visible or not depending on implementation
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should handle form with all filters enabled', async ({ page }) => {
|
|
||||||
const directCheckbox = page.getByTestId('schedule-direct-only-checkbox');
|
|
||||||
const returnCheckbox = page.getByTestId('schedule-return-checkbox');
|
|
||||||
|
|
||||||
// Enable both filters
|
|
||||||
await directCheckbox.click();
|
|
||||||
await returnCheckbox.click();
|
|
||||||
|
|
||||||
// Verify both are enabled
|
|
||||||
await expect(directCheckbox).toBeChecked();
|
|
||||||
await expect(returnCheckbox).toBeChecked();
|
|
||||||
|
|
||||||
// Return calendar should be visible
|
|
||||||
const returnCalendar = page.getByTestId('schedule-return-calendar');
|
|
||||||
await expect(returnCalendar).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Schedule Filters - Locale Tests (ru-ru)', () => {
|
|
||||||
test.beforeEach(async ({ page }) => {
|
|
||||||
// Navigate to schedule search page with Russian locale
|
|
||||||
await page.goto(`${BASE_URL}/ru-ru/schedule`);
|
|
||||||
// Wait for page to be loaded
|
|
||||||
await page.waitForSelector('[data-testid="schedule-search-form"]', { timeout: 5000 });
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should render form in Russian locale', async ({ page }) => {
|
|
||||||
const form = page.getByRole('search');
|
|
||||||
await expect(form).toBeVisible();
|
|
||||||
|
|
||||||
// Check that Russian labels are present
|
|
||||||
// The exact text depends on the Russian translations
|
|
||||||
const directCheckbox = page.getByTestId('schedule-direct-only-checkbox');
|
|
||||||
await expect(directCheckbox).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should support round-trip toggle in Russian locale', async ({ page }) => {
|
|
||||||
const returnCheckbox = page.getByTestId('schedule-return-checkbox');
|
|
||||||
const returnCalendar = page.getByTestId('schedule-return-calendar');
|
|
||||||
|
|
||||||
// Initially hidden
|
|
||||||
await expect(returnCalendar).not.toBeVisible();
|
|
||||||
|
|
||||||
// Enable
|
|
||||||
await returnCheckbox.click();
|
|
||||||
|
|
||||||
// Should be visible
|
|
||||||
await expect(returnCalendar).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should show validation errors in Russian locale', async ({ page }) => {
|
|
||||||
const searchButton = page.getByTestId('schedule-search-button');
|
|
||||||
|
|
||||||
// Try to search
|
|
||||||
await searchButton.click();
|
|
||||||
|
|
||||||
// Should show error message
|
|
||||||
const errorMessage = page.getByTestId('schedule-validation-error');
|
|
||||||
await expect(errorMessage).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,670 +0,0 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
|
||||||
|
|
||||||
test.describe('Schedule Results - Document 4 (US-35 to US-39)', () => {
|
|
||||||
test.beforeEach(async ({ page }) => {
|
|
||||||
// Navigate to schedule page and perform a search to get results
|
|
||||||
await page.goto('http://localhost:3000/schedule');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// Fill in search form
|
|
||||||
const departureInput = page.locator('[data-testid="schedule-departure-input"] input');
|
|
||||||
const arrivalInput = page.locator('[data-testid="schedule-arrival-input"] input');
|
|
||||||
|
|
||||||
await departureInput.fill('SVO');
|
|
||||||
await arrivalInput.fill('AER');
|
|
||||||
|
|
||||||
// Set date and submit
|
|
||||||
const dateInputs = page.locator('[data-testid="schedule-date-input"]');
|
|
||||||
await dateInputs.first().click();
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
// Look for a search button to submit
|
|
||||||
const searchButton = page.locator('button:has-text("Search")');
|
|
||||||
if (await searchButton.isVisible()) {
|
|
||||||
await searchButton.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for results to load
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
await page.waitForTimeout(1000);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('US-35: Schedule Results Page', () => {
|
|
||||||
test('should display results page with flight list', async ({ page }) => {
|
|
||||||
const resultsList = page.locator('[data-testid="schedule-flight-day"]');
|
|
||||||
await expect(resultsList).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display flight items with flight information', async ({ page }) => {
|
|
||||||
const flightItems = page.locator('[data-testid="schedule-flight-item"]');
|
|
||||||
const count = await flightItems.count();
|
|
||||||
expect(count).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display flight times in each item', async ({ page }) => {
|
|
||||||
const flightItems = page.locator('[data-testid="schedule-flight-item"]');
|
|
||||||
const firstFlight = flightItems.first();
|
|
||||||
|
|
||||||
// Check for time elements (departure and arrival time)
|
|
||||||
const times = firstFlight.locator('[class*="time"]');
|
|
||||||
const timeCount = await times.count();
|
|
||||||
expect(timeCount).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display flight numbers', async ({ page }) => {
|
|
||||||
const flightNumbers = page.locator('[class*="flightNumber"]');
|
|
||||||
const count = await flightNumbers.count();
|
|
||||||
expect(count).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display aircraft information', async ({ page }) => {
|
|
||||||
const aircraftElements = page.locator('[class*="flightAircraft"]');
|
|
||||||
const count = await aircraftElements.count();
|
|
||||||
expect(count).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display prices for flights', async ({ page }) => {
|
|
||||||
const priceElements = page.locator('[class*="flightPrice"]');
|
|
||||||
const count = await priceElements.count();
|
|
||||||
expect(count).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should be responsive on mobile viewport', async ({ page }) => {
|
|
||||||
await page.setViewportSize({ width: 375, height: 667 });
|
|
||||||
const resultsList = page.locator('[data-testid="schedule-flight-day"]');
|
|
||||||
await expect(resultsList).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should be responsive on tablet viewport', async ({ page }) => {
|
|
||||||
await page.setViewportSize({ width: 768, height: 1024 });
|
|
||||||
const resultsList = page.locator('[data-testid="schedule-flight-day"]');
|
|
||||||
await expect(resultsList).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('US-36: Switch Between Days', () => {
|
|
||||||
test('should display previous week button', async ({ page }) => {
|
|
||||||
const prevButton = page.locator('[data-testid="schedule-week-prev"]');
|
|
||||||
await expect(prevButton).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display next week button', async ({ page }) => {
|
|
||||||
const nextButton = page.locator('[data-testid="schedule-week-next"]');
|
|
||||||
await expect(nextButton).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should have previous/next buttons with proper accessibility', async ({ page }) => {
|
|
||||||
const prevButton = page.locator('[data-testid="schedule-week-prev"]');
|
|
||||||
const nextButton = page.locator('[data-testid="schedule-week-next"]');
|
|
||||||
|
|
||||||
const prevLabel = await prevButton.getAttribute('aria-label');
|
|
||||||
const nextLabel = await nextButton.getAttribute('aria-label');
|
|
||||||
|
|
||||||
expect(prevLabel).toBeTruthy();
|
|
||||||
expect(nextLabel).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should respond to day tab clicks', async ({ page }) => {
|
|
||||||
const dayTabs = page.locator('[data-testid="schedule-week-tab"]');
|
|
||||||
const count = await dayTabs.count();
|
|
||||||
expect(count).toBeGreaterThan(0);
|
|
||||||
|
|
||||||
// Click a different day
|
|
||||||
if (count > 1) {
|
|
||||||
await dayTabs.nth(1).click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
// Verify the clicked tab is now active
|
|
||||||
const activeTab = page.locator('[data-testid="schedule-week-tab"][aria-selected="true"]');
|
|
||||||
await expect(activeTab).toBeVisible();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('US-37: Week Navigation Tabs', () => {
|
|
||||||
test('should display week tabs (7 days)', async ({ page }) => {
|
|
||||||
const tabs = page.locator('[data-testid="schedule-week-tab"]');
|
|
||||||
const count = await tabs.count();
|
|
||||||
expect(count).toBe(7);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display day names in tabs', async ({ page }) => {
|
|
||||||
const tabs = page.locator('[data-testid="schedule-week-tab"]');
|
|
||||||
const firstTab = tabs.first();
|
|
||||||
|
|
||||||
const dayName = firstTab.locator('[class*="dayName"]');
|
|
||||||
await expect(dayName).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display dates in tabs', async ({ page }) => {
|
|
||||||
const tabs = page.locator('[data-testid="schedule-week-tab"]');
|
|
||||||
const firstTab = tabs.first();
|
|
||||||
|
|
||||||
const dayDate = firstTab.locator('[class*="dayDate"]');
|
|
||||||
await expect(dayDate).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should highlight the active day tab', async ({ page }) => {
|
|
||||||
const activeTab = page.locator('[data-testid="schedule-week-tab"][aria-selected="true"]');
|
|
||||||
await expect(activeTab).toBeVisible();
|
|
||||||
|
|
||||||
// Verify it has the active class
|
|
||||||
const className = await activeTab.getAttribute('class');
|
|
||||||
expect(className).toContain('weekTabActive');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should allow navigation between weeks with prev button', async ({ page }) => {
|
|
||||||
const prevButton = page.locator('[data-testid="schedule-week-prev"]');
|
|
||||||
await prevButton.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
// Verify we're still on the schedule results page
|
|
||||||
const resultsList = page.locator('[data-testid="schedule-flight-day"]');
|
|
||||||
await expect(resultsList).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should allow navigation between weeks with next button', async ({ page }) => {
|
|
||||||
const nextButton = page.locator('[data-testid="schedule-week-next"]');
|
|
||||||
await nextButton.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
// Verify we're still on the schedule results page
|
|
||||||
const resultsList = page.locator('[data-testid="schedule-flight-day"]');
|
|
||||||
await expect(resultsList).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should update displayed results when changing weeks', async ({ page }) => {
|
|
||||||
const flightItems = page.locator('[data-testid="schedule-flight-item"]');
|
|
||||||
const initialCount = await flightItems.count();
|
|
||||||
|
|
||||||
// Click next week
|
|
||||||
const nextButton = page.locator('[data-testid="schedule-week-next"]');
|
|
||||||
await nextButton.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
await page.waitForTimeout(1000);
|
|
||||||
|
|
||||||
// Results should still be visible (may be empty or different)
|
|
||||||
const resultsList = page.locator('[data-testid="schedule-flight-day"]');
|
|
||||||
await expect(resultsList).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('US-38: Flight Detail Expansion', () => {
|
|
||||||
test('should expand flight details on click', async ({ page }) => {
|
|
||||||
const flightItems = page.locator('[data-testid="schedule-flight-item"]');
|
|
||||||
|
|
||||||
if ((await flightItems.count()) > 0) {
|
|
||||||
const firstFlight = flightItems.first();
|
|
||||||
await firstFlight.click();
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
|
|
||||||
// Check if expanded class is applied
|
|
||||||
const className = await firstFlight.getAttribute('class');
|
|
||||||
expect(className).toContain('flightItemExpanded');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display flight details when expanded', async ({ page }) => {
|
|
||||||
const flightItems = page.locator('[data-testid="schedule-flight-item"]');
|
|
||||||
|
|
||||||
if ((await flightItems.count()) > 0) {
|
|
||||||
const firstFlight = flightItems.first();
|
|
||||||
await firstFlight.click();
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
|
|
||||||
// Look for detail rows
|
|
||||||
const detailsRow = firstFlight.locator('[class*="detailsRow"]');
|
|
||||||
const count = await detailsRow.count();
|
|
||||||
expect(count).toBeGreaterThan(0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should show duration in expanded details', async ({ page }) => {
|
|
||||||
const flightItems = page.locator('[data-testid="schedule-flight-item"]');
|
|
||||||
|
|
||||||
if ((await flightItems.count()) > 0) {
|
|
||||||
const firstFlight = flightItems.first();
|
|
||||||
await firstFlight.click();
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
|
|
||||||
// Look for duration label
|
|
||||||
const durationLabel = firstFlight.locator('text=Duration');
|
|
||||||
await expect(durationLabel).toBeVisible();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should show aircraft in expanded details', async ({ page }) => {
|
|
||||||
const flightItems = page.locator('[data-testid="schedule-flight-item"]');
|
|
||||||
|
|
||||||
if ((await flightItems.count()) > 0) {
|
|
||||||
const firstFlight = flightItems.first();
|
|
||||||
await firstFlight.click();
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
|
|
||||||
// Look for aircraft label
|
|
||||||
const aircraftLabel = firstFlight.locator('text=Aircraft');
|
|
||||||
await expect(aircraftLabel).toBeVisible();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should show price in expanded details', async ({ page }) => {
|
|
||||||
const flightItems = page.locator('[data-testid="schedule-flight-item"]');
|
|
||||||
|
|
||||||
if ((await flightItems.count()) > 0) {
|
|
||||||
const firstFlight = flightItems.first();
|
|
||||||
await firstFlight.click();
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
|
|
||||||
// Look for price label
|
|
||||||
const priceLabel = firstFlight.locator('text=Price');
|
|
||||||
await expect(priceLabel).toBeVisible();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should show status in expanded details', async ({ page }) => {
|
|
||||||
const flightItems = page.locator('[data-testid="schedule-flight-item"]');
|
|
||||||
|
|
||||||
if ((await flightItems.count()) > 0) {
|
|
||||||
const firstFlight = flightItems.first();
|
|
||||||
await firstFlight.click();
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
|
|
||||||
// Look for status label
|
|
||||||
const statusLabel = firstFlight.locator('text=Status');
|
|
||||||
await expect(statusLabel).toBeVisible();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should collapse flight when clicking again', async ({ page }) => {
|
|
||||||
const flightItems = page.locator('[data-testid="schedule-flight-item"]');
|
|
||||||
|
|
||||||
if ((await flightItems.count()) > 0) {
|
|
||||||
const firstFlight = flightItems.first();
|
|
||||||
|
|
||||||
// Expand
|
|
||||||
await firstFlight.click();
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
let className = await firstFlight.getAttribute('class');
|
|
||||||
expect(className).toContain('flightItemExpanded');
|
|
||||||
|
|
||||||
// Collapse
|
|
||||||
await firstFlight.click();
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
className = await firstFlight.getAttribute('class');
|
|
||||||
expect(className).not.toContain('flightItemExpanded');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should show smooth animation when expanding', async ({ page }) => {
|
|
||||||
const flightItems = page.locator('[data-testid="schedule-flight-item"]');
|
|
||||||
|
|
||||||
if ((await flightItems.count()) > 0) {
|
|
||||||
const firstFlight = flightItems.first();
|
|
||||||
const initialHeight = await firstFlight.evaluate((el) => el.offsetHeight);
|
|
||||||
|
|
||||||
await firstFlight.click();
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
const expandedHeight = await firstFlight.evaluate((el) => el.offsetHeight);
|
|
||||||
// Height should increase when expanded
|
|
||||||
expect(expandedHeight).toBeGreaterThan(initialHeight);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('US-39: Result Sorting', () => {
|
|
||||||
test('should display sorting menu', async ({ page }) => {
|
|
||||||
const sortingMenu = page.locator('[data-testid="schedule-sorting-menu"]');
|
|
||||||
await expect(sortingMenu).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should have sort buttons', async ({ page }) => {
|
|
||||||
const sortButtons = page.locator('button[data-testid*="schedule-sort-button"]');
|
|
||||||
const count = await sortButtons.count();
|
|
||||||
expect(count).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should have one active sort button', async ({ page }) => {
|
|
||||||
const activeButtons = page.locator('button[aria-pressed="true"]');
|
|
||||||
const count = await activeButtons.count();
|
|
||||||
expect(count).toBeGreaterThanOrEqual(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should allow switching sort modes', async ({ page }) => {
|
|
||||||
const sortButtons = page.locator('button[data-testid*="schedule-sort-button"]');
|
|
||||||
const count = await sortButtons.count();
|
|
||||||
|
|
||||||
if (count > 1) {
|
|
||||||
const initialActive = page.locator('button[aria-pressed="true"]');
|
|
||||||
const initialId = await initialActive.first().getAttribute('data-testid');
|
|
||||||
|
|
||||||
// Click a different sort button
|
|
||||||
const secondButton = sortButtons.nth(1);
|
|
||||||
await secondButton.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
// Verify the active button changed
|
|
||||||
const newActive = page.locator('button[aria-pressed="true"]');
|
|
||||||
const newId = await newActive.first().getAttribute('data-testid');
|
|
||||||
|
|
||||||
expect(newId).not.toBe(initialId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should re-sort flights when sort option changes', async ({ page }) => {
|
|
||||||
const flightItems = page.locator('[data-testid="schedule-flight-item"]');
|
|
||||||
|
|
||||||
if ((await flightItems.count()) >= 2) {
|
|
||||||
// Get initial order
|
|
||||||
const initialFirstFlightTime = await flightItems
|
|
||||||
.first()
|
|
||||||
.locator('[class*="time"]')
|
|
||||||
.first()
|
|
||||||
.textContent();
|
|
||||||
|
|
||||||
// Click sort button
|
|
||||||
const sortButtons = page.locator('button[data-testid*="schedule-sort-button"]');
|
|
||||||
const count = await sortButtons.count();
|
|
||||||
|
|
||||||
if (count > 1) {
|
|
||||||
await sortButtons.nth(1).click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
// Verify flights are still displayed
|
|
||||||
const updatedFlightItems = page.locator('[data-testid="schedule-flight-item"]');
|
|
||||||
const updatedCount = await updatedFlightItems.count();
|
|
||||||
expect(updatedCount).toBeGreaterThan(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should highlight active sort option', async ({ page }) => {
|
|
||||||
const activeButton = page.locator('button[aria-pressed="true"]');
|
|
||||||
const severity = await activeButton.first().getAttribute('severity');
|
|
||||||
|
|
||||||
// Active button should have 'info' severity (or similar highlighting)
|
|
||||||
expect(severity).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should have accessible sort controls', async ({ page }) => {
|
|
||||||
const sortButtons = page.locator('button[data-testid*="schedule-sort-button"]');
|
|
||||||
const count = await sortButtons.count();
|
|
||||||
|
|
||||||
for (let i = 0; i < Math.min(count, 3); i++) {
|
|
||||||
const button = sortButtons.nth(i);
|
|
||||||
const ariaPressed = await button.getAttribute('aria-pressed');
|
|
||||||
expect(ariaPressed).toBeTruthy();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should persist sort selection during interaction', async ({ page }) => {
|
|
||||||
const sortButtons = page.locator('button[data-testid*="schedule-sort-button"]');
|
|
||||||
const count = await sortButtons.count();
|
|
||||||
|
|
||||||
if (count > 1) {
|
|
||||||
// Select a sort mode
|
|
||||||
const secondButton = sortButtons.nth(1);
|
|
||||||
await secondButton.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
// Expand a flight
|
|
||||||
const flightItems = page.locator('[data-testid="schedule-flight-item"]');
|
|
||||||
if ((await flightItems.count()) > 0) {
|
|
||||||
await flightItems.first().click();
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
|
|
||||||
// Verify sort is still active
|
|
||||||
const activeButton = page.locator('button[aria-pressed="true"]');
|
|
||||||
const activeId = await activeButton.first().getAttribute('data-testid');
|
|
||||||
expect(activeId).toBeTruthy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Round Trip Support (US-36 Integration)', () => {
|
|
||||||
test('should show direction switch for round trip', async ({ page }) => {
|
|
||||||
// Check if direction switch exists (may not exist for one-way flights)
|
|
||||||
const directionSwitch = page.locator('[data-testid="direction-switch"]');
|
|
||||||
const exists = await directionSwitch.isVisible().catch(() => false);
|
|
||||||
|
|
||||||
// If it exists, it should be visible
|
|
||||||
if (exists) {
|
|
||||||
await expect(directionSwitch).toBeVisible();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should allow switching between outbound and inbound', async ({ page }) => {
|
|
||||||
const directionSwitch = page.locator('[data-testid="direction-switch"]');
|
|
||||||
const exists = await directionSwitch.isVisible().catch(() => false);
|
|
||||||
|
|
||||||
if (exists) {
|
|
||||||
const inboundButton = page.locator('[data-testid="direction-inbound"]');
|
|
||||||
if (await inboundButton.isVisible()) {
|
|
||||||
await inboundButton.click();
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
// Verify results are still displayed
|
|
||||||
const resultsList = page.locator('[data-testid="schedule-flight-day"]');
|
|
||||||
await expect(resultsList).toBeVisible();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Accessibility', () => {
|
|
||||||
test('should have proper ARIA labels on navigation buttons', async ({ page }) => {
|
|
||||||
const prevButton = page.locator('[data-testid="schedule-week-prev"]');
|
|
||||||
const nextButton = page.locator('[data-testid="schedule-week-next"]');
|
|
||||||
|
|
||||||
const prevLabel = await prevButton.getAttribute('aria-label');
|
|
||||||
const nextLabel = await nextButton.getAttribute('aria-label');
|
|
||||||
|
|
||||||
expect(prevLabel).toBeTruthy();
|
|
||||||
expect(nextLabel).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should have proper ARIA attributes on tabs', async ({ page }) => {
|
|
||||||
const tabs = page.locator('[data-testid="schedule-week-tab"]');
|
|
||||||
|
|
||||||
if ((await tabs.count()) > 0) {
|
|
||||||
const firstTab = tabs.first();
|
|
||||||
const ariaSelected = await firstTab.getAttribute('aria-selected');
|
|
||||||
expect(ariaSelected).toBeTruthy();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should have proper ARIA attributes on sort buttons', async ({ page }) => {
|
|
||||||
const sortButtons = page.locator('button[data-testid*="schedule-sort-button"]');
|
|
||||||
|
|
||||||
if ((await sortButtons.count()) > 0) {
|
|
||||||
const firstButton = sortButtons.first();
|
|
||||||
const ariaPressed = await firstButton.getAttribute('aria-pressed');
|
|
||||||
expect(ariaPressed).toBeTruthy();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should maintain keyboard navigation', async ({ page }) => {
|
|
||||||
const prevButton = page.locator('[data-testid="schedule-week-prev"]');
|
|
||||||
await prevButton.focus();
|
|
||||||
|
|
||||||
// Button should be focused
|
|
||||||
const focused = await page.evaluate(() =>
|
|
||||||
document.activeElement?.getAttribute('data-testid'),
|
|
||||||
);
|
|
||||||
expect(focused).toBe('schedule-week-prev');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Localization (ru-ru)', () => {
|
|
||||||
test('should display results in Russian locale', async ({ page }) => {
|
|
||||||
// Check for Russian text (common words in schedule)
|
|
||||||
const pageContent = await page.textContent('body');
|
|
||||||
expect(pageContent).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should use Russian date format', async ({ page }) => {
|
|
||||||
const tabs = page.locator('[data-testid="schedule-week-tab"]');
|
|
||||||
|
|
||||||
if ((await tabs.count()) > 0) {
|
|
||||||
const tabText = await tabs.first().textContent();
|
|
||||||
// Russian day names and date format
|
|
||||||
expect(tabText).toBeTruthy();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Localization (en-us)', () => {
|
|
||||||
test('should display results in English locale', async ({ page, context }) => {
|
|
||||||
// Set English locale
|
|
||||||
await context.addInitScript(() => {
|
|
||||||
localStorage.setItem('preferredLocale', 'en-us');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Navigate to schedule
|
|
||||||
await page.goto('http://localhost:3000/schedule?locale=en-us');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// Perform search
|
|
||||||
const departureInput = page.locator('[data-testid="schedule-departure-input"] input');
|
|
||||||
const arrivalInput = page.locator('[data-testid="schedule-arrival-input"] input');
|
|
||||||
|
|
||||||
await departureInput.fill('SVO');
|
|
||||||
await arrivalInput.fill('AER');
|
|
||||||
|
|
||||||
const searchButton = page.locator('button:has-text("Search")');
|
|
||||||
if (await searchButton.isVisible()) {
|
|
||||||
await searchButton.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
await page.waitForTimeout(1000);
|
|
||||||
|
|
||||||
// Verify results are displayed
|
|
||||||
const resultsList = page.locator('[data-testid="schedule-flight-day"]');
|
|
||||||
await expect(resultsList).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Error Handling', () => {
|
|
||||||
test('should display empty state when no flights found', async ({ page }) => {
|
|
||||||
// Try searching for an impossible route
|
|
||||||
await page.goto('http://localhost:3000/schedule');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const departureInput = page.locator('[data-testid="schedule-departure-input"] input');
|
|
||||||
const arrivalInput = page.locator('[data-testid="schedule-arrival-input"] input');
|
|
||||||
|
|
||||||
// Use unlikely city codes
|
|
||||||
await departureInput.fill('AAA');
|
|
||||||
await arrivalInput.fill('ZZZ');
|
|
||||||
|
|
||||||
const searchButton = page.locator('button:has-text("Search")');
|
|
||||||
if (await searchButton.isVisible()) {
|
|
||||||
await searchButton.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
await page.waitForTimeout(1000);
|
|
||||||
|
|
||||||
// Should show either empty state or error message
|
|
||||||
const emptyState = page.locator('[data-testid="schedule-empty-list"]');
|
|
||||||
const resultsList = page.locator('[data-testid="schedule-flight-day"]');
|
|
||||||
|
|
||||||
const hasEmptyState = await emptyState.isVisible().catch(() => false);
|
|
||||||
const hasResults = await resultsList.isVisible().catch(() => false);
|
|
||||||
|
|
||||||
expect(hasEmptyState || hasResults).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Console Audit - Schedule Results', () => {
|
|
||||||
test('should have no console errors on results page', async ({ page }) => {
|
|
||||||
const errors: string[] = [];
|
|
||||||
|
|
||||||
page.on('console', (message) => {
|
|
||||||
if (message.type() === 'error') {
|
|
||||||
errors.push(message.text());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.goto('http://localhost:3000/schedule');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// Perform search
|
|
||||||
const departureInput = page.locator('[data-testid="schedule-departure-input"] input');
|
|
||||||
const arrivalInput = page.locator('[data-testid="schedule-arrival-input"] input');
|
|
||||||
|
|
||||||
await departureInput.fill('SVO');
|
|
||||||
await arrivalInput.fill('AER');
|
|
||||||
|
|
||||||
const searchButton = page.locator('button:has-text("Search")');
|
|
||||||
if (await searchButton.isVisible()) {
|
|
||||||
await searchButton.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
await page.waitForTimeout(1000);
|
|
||||||
|
|
||||||
// Interact with results
|
|
||||||
const flightItems = page.locator('[data-testid="schedule-flight-item"]');
|
|
||||||
if ((await flightItems.count()) > 0) {
|
|
||||||
await flightItems.first().click();
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for errors (excluding known non-critical warnings)
|
|
||||||
const criticalErrors = errors.filter(
|
|
||||||
(e) =>
|
|
||||||
!e.includes('hydration') &&
|
|
||||||
!e.includes('useLayoutEffect') &&
|
|
||||||
!e.includes('act()') &&
|
|
||||||
!e.includes('warning') &&
|
|
||||||
e.length > 0,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(criticalErrors).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should have no accessibility violations', async ({ page }) => {
|
|
||||||
await page.goto('http://localhost:3000/schedule');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// Perform search
|
|
||||||
const departureInput = page.locator('[data-testid="schedule-departure-input"] input');
|
|
||||||
const arrivalInput = page.locator('[data-testid="schedule-arrival-input"] input');
|
|
||||||
|
|
||||||
await departureInput.fill('SVO');
|
|
||||||
await arrivalInput.fill('AER');
|
|
||||||
|
|
||||||
const searchButton = page.locator('button:has-text("Search")');
|
|
||||||
if (await searchButton.isVisible()) {
|
|
||||||
await searchButton.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
await page.waitForTimeout(1000);
|
|
||||||
|
|
||||||
// Check that all interactive elements are keyboard accessible
|
|
||||||
const buttons = page.locator('button');
|
|
||||||
const count = await buttons.count();
|
|
||||||
|
|
||||||
for (let i = 0; i < Math.min(count, 5); i++) {
|
|
||||||
const button = buttons.nth(i);
|
|
||||||
await button.focus();
|
|
||||||
|
|
||||||
const focused = await page.evaluate(() => {
|
|
||||||
const el = document.activeElement as HTMLElement;
|
|
||||||
return el.tagName === 'BUTTON';
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(focused).toBeTruthy();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,347 +0,0 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
|
||||||
|
|
||||||
test.describe('Schedule Search - Document 3 (US-23 to US-27)', () => {
|
|
||||||
test.beforeEach(async ({ page }) => {
|
|
||||||
await page.goto('http://localhost:3000/schedule');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('US-23: Schedule Tab Navigation', () => {
|
|
||||||
test('should render schedule search form', async ({ page }) => {
|
|
||||||
const form = page.locator('[data-testid="schedule-search-form"]');
|
|
||||||
await expect(form).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should render search form with proper role', async ({ page }) => {
|
|
||||||
const form = page.locator('[role="search"]');
|
|
||||||
await expect(form).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should have proper ARIA label', async ({ page }) => {
|
|
||||||
const form = page.locator('[role="search"]');
|
|
||||||
const ariaLabel = await form.getAttribute('aria-label');
|
|
||||||
expect(ariaLabel).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('US-24: Departure City Input', () => {
|
|
||||||
test('should render departure city input', async ({ page }) => {
|
|
||||||
const input = page.locator('[data-testid="schedule-departure-input"]');
|
|
||||||
await expect(input).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should have From label', async ({ page }) => {
|
|
||||||
const label = page.getByText('From', { exact: true });
|
|
||||||
await expect(label).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should accept text input for departure city', async ({ page }) => {
|
|
||||||
const input = page.locator('[data-testid="schedule-departure-input"] input');
|
|
||||||
await input.fill('Moscow');
|
|
||||||
await expect(input).toHaveValue('Moscow');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should allow clearing departure city', async ({ page }) => {
|
|
||||||
const input = page.locator('[data-testid="schedule-departure-input"] input');
|
|
||||||
await input.fill('Moscow');
|
|
||||||
await input.clear();
|
|
||||||
await expect(input).toHaveValue('');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should support autocomplete suggestions', async ({ page }) => {
|
|
||||||
const input = page.locator('[data-testid="schedule-departure-input"] input');
|
|
||||||
await input.focus();
|
|
||||||
await input.type('Mos', { delay: 100 });
|
|
||||||
// Wait for autocomplete to potentially appear
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
expect(input).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('US-25: Arrival City Input', () => {
|
|
||||||
test('should render arrival city input', async ({ page }) => {
|
|
||||||
const input = page.locator('[data-testid="schedule-arrival-input"]');
|
|
||||||
await expect(input).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should have To label', async ({ page }) => {
|
|
||||||
const label = page.getByText('To', { exact: true });
|
|
||||||
await expect(label).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should accept text input for arrival city', async ({ page }) => {
|
|
||||||
const input = page.locator('[data-testid="schedule-arrival-input"] input');
|
|
||||||
await input.fill('Saint Petersburg');
|
|
||||||
await expect(input).toHaveValue('Saint Petersburg');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should allow clearing arrival city', async ({ page }) => {
|
|
||||||
const input = page.locator('[data-testid="schedule-arrival-input"] input');
|
|
||||||
await input.fill('Saint Petersburg');
|
|
||||||
await input.clear();
|
|
||||||
await expect(input).toHaveValue('');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should support independent entry from departure', async ({ page }) => {
|
|
||||||
const departureInput = page.locator('[data-testid="schedule-departure-input"] input');
|
|
||||||
const arrivalInput = page.locator('[data-testid="schedule-arrival-input"] input');
|
|
||||||
|
|
||||||
await departureInput.fill('Moscow');
|
|
||||||
await arrivalInput.fill('SPB');
|
|
||||||
|
|
||||||
await expect(departureInput).toHaveValue('Moscow');
|
|
||||||
await expect(arrivalInput).toHaveValue('SPB');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('US-26: Swap Cities Button (Exchange)', () => {
|
|
||||||
test('should have both departure and arrival inputs for exchange', async ({ page }) => {
|
|
||||||
const departureInput = page.locator('[data-testid="schedule-departure-input"]');
|
|
||||||
const arrivalInput = page.locator('[data-testid="schedule-arrival-input"]');
|
|
||||||
|
|
||||||
await expect(departureInput).toBeVisible();
|
|
||||||
await expect(arrivalInput).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should allow switching focus between city inputs', async ({ page }) => {
|
|
||||||
const departureInput = page.locator('[data-testid="schedule-departure-input"] input');
|
|
||||||
const arrivalInput = page.locator('[data-testid="schedule-arrival-input"] input');
|
|
||||||
|
|
||||||
await departureInput.focus();
|
|
||||||
await expect(departureInput).toBeFocused();
|
|
||||||
|
|
||||||
await arrivalInput.focus();
|
|
||||||
await expect(arrivalInput).toBeFocused();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should support entering different cities', async ({ page }) => {
|
|
||||||
const departureInput = page.locator('[data-testid="schedule-departure-input"] input');
|
|
||||||
const arrivalInput = page.locator('[data-testid="schedule-arrival-input"] input');
|
|
||||||
|
|
||||||
await departureInput.fill('Moscow');
|
|
||||||
await arrivalInput.fill('Saint Petersburg');
|
|
||||||
|
|
||||||
await expect(departureInput).toHaveValue('Moscow');
|
|
||||||
await expect(arrivalInput).toHaveValue('Saint Petersburg');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('US-27: Week Selection', () => {
|
|
||||||
test('should render date from input', async ({ page }) => {
|
|
||||||
const dateFromInput = page.locator('[data-testid="schedule-calendar"] input');
|
|
||||||
await expect(dateFromInput).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should render date to input', async ({ page }) => {
|
|
||||||
const dateToInput = page.locator('[data-testid="schedule-outbound-date-input"]');
|
|
||||||
await expect(dateToInput).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should have Depart label for date from', async ({ page }) => {
|
|
||||||
const label = page.getByText('Depart', { exact: true });
|
|
||||||
await expect(label).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should have Return label for date to', async ({ page }) => {
|
|
||||||
const label = page.getByText('Return', { exact: true });
|
|
||||||
await expect(label).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should initialize with date values', async ({ page }) => {
|
|
||||||
const dateFromInput = page.locator('[data-testid="schedule-calendar"] input');
|
|
||||||
const dateToInput = page.locator('[data-testid="schedule-outbound-date-input"]');
|
|
||||||
|
|
||||||
const dateFromValue = await dateFromInput.inputValue();
|
|
||||||
const dateToValue = await dateToInput.inputValue();
|
|
||||||
|
|
||||||
// Should match YYYY-MM-DD format
|
|
||||||
expect(dateFromValue).toMatch(/\d{4}-\d{2}-\d{2}/);
|
|
||||||
expect(dateToValue).toMatch(/\d{4}-\d{2}-\d{2}/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should have date input type', async ({ page }) => {
|
|
||||||
const dateFromInput = page.locator('[data-testid="schedule-calendar"] input');
|
|
||||||
const dateToInput = page.locator('[data-testid="schedule-outbound-date-input"]');
|
|
||||||
|
|
||||||
const dateFromType = await dateFromInput.getAttribute('type');
|
|
||||||
const dateToType = await dateToInput.getAttribute('type');
|
|
||||||
|
|
||||||
expect(dateFromType).toBe('date');
|
|
||||||
expect(dateToType).toBe('date');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should allow changing departure date', async ({ page }) => {
|
|
||||||
const dateFromInput = page.locator('[data-testid="schedule-calendar"] input');
|
|
||||||
const initialValue = await dateFromInput.inputValue();
|
|
||||||
|
|
||||||
// The date input should be functional
|
|
||||||
await dateFromInput.focus();
|
|
||||||
await expect(dateFromInput).toBeFocused();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should support week date range selection', async ({ page }) => {
|
|
||||||
const dateFromInput = page.locator('[data-testid="schedule-calendar"] input');
|
|
||||||
const dateToInput = page.locator('[data-testid="schedule-outbound-date-input"]');
|
|
||||||
|
|
||||||
// Both should be visible and functional for date range
|
|
||||||
await expect(dateFromInput).toBeVisible();
|
|
||||||
await expect(dateToInput).toBeVisible();
|
|
||||||
|
|
||||||
const dateFromValue = await dateFromInput.inputValue();
|
|
||||||
const dateToValue = await dateToInput.inputValue();
|
|
||||||
|
|
||||||
// Both should have dates
|
|
||||||
expect(dateFromValue).toBeTruthy();
|
|
||||||
expect(dateToValue).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Schedule Search Form Integration', () => {
|
|
||||||
test('should have all search inputs visible', async ({ page }) => {
|
|
||||||
const departureInput = page.locator('[data-testid="schedule-departure-input"]');
|
|
||||||
const arrivalInput = page.locator('[data-testid="schedule-arrival-input"]');
|
|
||||||
const dateFromInput = page.locator('[data-testid="schedule-calendar"] input');
|
|
||||||
const dateToInput = page.locator('[data-testid="schedule-outbound-date-input"]');
|
|
||||||
|
|
||||||
await expect(departureInput).toBeVisible();
|
|
||||||
await expect(arrivalInput).toBeVisible();
|
|
||||||
await expect(dateFromInput).toBeVisible();
|
|
||||||
await expect(dateToInput).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should have search button', async ({ page }) => {
|
|
||||||
const searchButton = page.locator('[data-testid="schedule-search-button"]');
|
|
||||||
await expect(searchButton).toBeVisible();
|
|
||||||
await expect(searchButton).toContainText('Search', { ignoreCase: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should have checkbox for direct flights only', async ({ page }) => {
|
|
||||||
const directCheckbox = page.locator('[data-testid="schedule-direct-only-checkbox"]');
|
|
||||||
await expect(directCheckbox).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should have checkbox for return flight', async ({ page }) => {
|
|
||||||
const returnCheckbox = page.locator('[data-testid="schedule-return-checkbox"]');
|
|
||||||
await expect(returnCheckbox).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should show validation error when trying to search without cities', async ({ page }) => {
|
|
||||||
const searchButton = page.locator('[data-testid="schedule-search-button"]');
|
|
||||||
await searchButton.click();
|
|
||||||
|
|
||||||
const error = page.locator('[data-testid="schedule-validation-error"]');
|
|
||||||
await expect(error).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should toggle return date fields when return flight is enabled', async ({ page }) => {
|
|
||||||
const returnCheckbox = page.locator('[data-testid="schedule-return-checkbox"]');
|
|
||||||
const returnCalendar = page.locator('[data-testid="schedule-return-calendar"]');
|
|
||||||
|
|
||||||
// Initially hidden
|
|
||||||
await expect(returnCalendar).not.toBeVisible();
|
|
||||||
|
|
||||||
// Click to enable return flight
|
|
||||||
await returnCheckbox.click();
|
|
||||||
|
|
||||||
// Now visible
|
|
||||||
await expect(returnCalendar).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Schedule Search Workflow', () => {
|
|
||||||
test('should allow complete search form interaction', async ({ page }) => {
|
|
||||||
const departureInput = page.locator('[data-testid="schedule-departure-input"] input');
|
|
||||||
const arrivalInput = page.locator('[data-testid="schedule-arrival-input"] input');
|
|
||||||
const directCheckbox = page.locator('[data-testid="schedule-direct-only-checkbox"]');
|
|
||||||
|
|
||||||
// Fill departure city
|
|
||||||
await departureInput.fill('Moscow');
|
|
||||||
await expect(departureInput).toHaveValue('Moscow');
|
|
||||||
|
|
||||||
// Fill arrival city
|
|
||||||
await arrivalInput.fill('Saint Petersburg');
|
|
||||||
await expect(arrivalInput).toHaveValue('Saint Petersburg');
|
|
||||||
|
|
||||||
// Toggle direct only
|
|
||||||
const isChecked = await directCheckbox.isChecked();
|
|
||||||
await directCheckbox.click();
|
|
||||||
const newChecked = await directCheckbox.isChecked();
|
|
||||||
expect(newChecked).toBe(!isChecked);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should maintain form state during interaction', async ({ page }) => {
|
|
||||||
const departureInput = page.locator('[data-testid="schedule-departure-input"] input');
|
|
||||||
const arrivalInput = page.locator('[data-testid="schedule-arrival-input"] input');
|
|
||||||
const dateFromInput = page.locator('[data-testid="schedule-calendar"] input');
|
|
||||||
|
|
||||||
// Enter data
|
|
||||||
await departureInput.fill('Moscow');
|
|
||||||
await arrivalInput.fill('SPB');
|
|
||||||
const originalDate = await dateFromInput.inputValue();
|
|
||||||
|
|
||||||
// Verify all data is still there
|
|
||||||
await expect(departureInput).toHaveValue('Moscow');
|
|
||||||
await expect(arrivalInput).toHaveValue('SPB');
|
|
||||||
const newDate = await dateFromInput.inputValue();
|
|
||||||
expect(newDate).toBe(originalDate);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should allow toggling between one-way and round trip', async ({ page }) => {
|
|
||||||
const returnCheckbox = page.locator('[data-testid="schedule-return-checkbox"]');
|
|
||||||
const returnCalendar = page.locator('[data-testid="schedule-return-calendar"]');
|
|
||||||
|
|
||||||
// Initially one-way
|
|
||||||
const isCheckedInitial = await returnCheckbox.isChecked();
|
|
||||||
expect(isCheckedInitial).toBe(false);
|
|
||||||
|
|
||||||
// Toggle to round trip
|
|
||||||
await returnCheckbox.click();
|
|
||||||
await expect(returnCalendar).toBeVisible();
|
|
||||||
|
|
||||||
// Toggle back to one-way
|
|
||||||
await returnCheckbox.click();
|
|
||||||
await expect(returnCalendar).not.toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Accessibility', () => {
|
|
||||||
test('should have form with proper role and label', async ({ page }) => {
|
|
||||||
const form = page.locator('[role="search"]');
|
|
||||||
const ariaLabel = await form.getAttribute('aria-label');
|
|
||||||
|
|
||||||
await expect(form).toBeVisible();
|
|
||||||
expect(ariaLabel).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should have properly associated labels', async ({ page }) => {
|
|
||||||
const fromLabel = page.getByText('From', { exact: true });
|
|
||||||
const toLabel = page.getByText('To', { exact: true });
|
|
||||||
const departLabel = page.getByText('Depart', { exact: true });
|
|
||||||
const returnLabel = page.getByText('Return', { exact: true });
|
|
||||||
|
|
||||||
await expect(fromLabel).toBeVisible();
|
|
||||||
await expect(toLabel).toBeVisible();
|
|
||||||
await expect(departLabel).toBeVisible();
|
|
||||||
await expect(returnLabel).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should support keyboard navigation', async ({ page }) => {
|
|
||||||
const departureInput = page.locator('[data-testid="schedule-departure-input"] input');
|
|
||||||
const arrivalInput = page.locator('[data-testid="schedule-arrival-input"] input');
|
|
||||||
const searchButton = page.locator('[data-testid="schedule-search-button"]');
|
|
||||||
|
|
||||||
// Start at departure
|
|
||||||
await departureInput.focus();
|
|
||||||
await expect(departureInput).toBeFocused();
|
|
||||||
|
|
||||||
// Tab to next element
|
|
||||||
await page.keyboard.press('Tab');
|
|
||||||
|
|
||||||
// Should be on next focusable element
|
|
||||||
const focusedElement = await page.evaluate(() =>
|
|
||||||
document.activeElement?.getAttribute('data-testid'),
|
|
||||||
);
|
|
||||||
expect(focusedElement).not.toBe('schedule-departure-input');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,199 +0,0 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
|
||||||
|
|
||||||
test.describe('Search History (US-8)', () => {
|
|
||||||
test.beforeEach(async ({ page }) => {
|
|
||||||
await page.goto('http://localhost:3000/ru-ru/onlineboard');
|
|
||||||
// Clear localStorage to start fresh
|
|
||||||
await page.evaluate(() => localStorage.clear());
|
|
||||||
// Reload after clearing
|
|
||||||
await page.reload();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should not display search history section when empty', async ({ page }) => {
|
|
||||||
const section = page.locator('[data-testid="landing-search-history"]');
|
|
||||||
await expect(section).not.toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display search history section when items exist', async ({ page }) => {
|
|
||||||
// Setup: Add history to localStorage
|
|
||||||
await page.evaluate(() => {
|
|
||||||
const historyItem = {
|
|
||||||
id: '1',
|
|
||||||
label: 'SU 1402',
|
|
||||||
url: '/search?flight=SU1402',
|
|
||||||
timestamp: Date.now(),
|
|
||||||
};
|
|
||||||
localStorage.setItem('aeroflot_search_history', JSON.stringify([historyItem]));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Reload to pick up the localStorage data
|
|
||||||
await page.reload();
|
|
||||||
|
|
||||||
const section = page.locator('[data-testid="landing-search-history"]');
|
|
||||||
await expect(section).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display history items correctly', async ({ page }) => {
|
|
||||||
// Setup: Add multiple history items
|
|
||||||
await page.evaluate(() => {
|
|
||||||
const historyItems = [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
label: 'SU 1402',
|
|
||||||
url: '/search?flight=SU1402',
|
|
||||||
timestamp: Date.now(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
label: 'SU 1403',
|
|
||||||
url: '/search?flight=SU1403',
|
|
||||||
timestamp: Date.now() - 60000,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
localStorage.setItem('aeroflot_search_history', JSON.stringify(historyItems));
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.reload();
|
|
||||||
|
|
||||||
const items = page.locator('[data-testid="landing-search-history-item"]');
|
|
||||||
await expect(items).toHaveCount(2);
|
|
||||||
|
|
||||||
// Check for flight numbers
|
|
||||||
await expect(page.getByText('SU 1402')).toBeVisible();
|
|
||||||
await expect(page.getByText('SU 1403')).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display search history title', async ({ page }) => {
|
|
||||||
await page.evaluate(() => {
|
|
||||||
const historyItem = {
|
|
||||||
id: '1',
|
|
||||||
label: 'SU 1402',
|
|
||||||
url: '/search?flight=SU1402',
|
|
||||||
timestamp: Date.now(),
|
|
||||||
};
|
|
||||||
localStorage.setItem('aeroflot_search_history', JSON.stringify([historyItem]));
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.reload();
|
|
||||||
|
|
||||||
// Note: Title depends on intl messages, might be "Search History" or Russian equivalent
|
|
||||||
const title = page.locator('[data-testid="landing-search-history"] h3');
|
|
||||||
await expect(title).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should have clickable history items that are links', async ({ page }) => {
|
|
||||||
await page.evaluate(() => {
|
|
||||||
const historyItem = {
|
|
||||||
id: '1',
|
|
||||||
label: 'SU 1402',
|
|
||||||
url: '/search?flight=SU1402',
|
|
||||||
timestamp: Date.now(),
|
|
||||||
};
|
|
||||||
localStorage.setItem('aeroflot_search_history', JSON.stringify([historyItem]));
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.reload();
|
|
||||||
|
|
||||||
const link = page.locator('[data-testid="landing-search-history-item"] a').first();
|
|
||||||
await expect(link).toHaveAttribute('href', /search\?flight=SU1402/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should format timestamp as HH:MM', async ({ page }) => {
|
|
||||||
const testTime = new Date(2026, 3, 9, 14, 30, 0).getTime();
|
|
||||||
|
|
||||||
await page.evaluate((time) => {
|
|
||||||
const historyItem = {
|
|
||||||
id: '1',
|
|
||||||
label: 'SU 1402',
|
|
||||||
url: '/search?flight=SU1402',
|
|
||||||
timestamp: time,
|
|
||||||
};
|
|
||||||
localStorage.setItem('aeroflot_search_history', JSON.stringify([historyItem]));
|
|
||||||
}, testTime);
|
|
||||||
|
|
||||||
await page.reload();
|
|
||||||
|
|
||||||
// Check for time format HH:MM
|
|
||||||
const timeElement = page.locator('[data-testid="landing-search-history-item"] span').last();
|
|
||||||
const timeText = await timeElement.textContent();
|
|
||||||
expect(timeText).toMatch(/\d{2}:\d{2}/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should persist history across page reloads', async ({ page }) => {
|
|
||||||
// Add history
|
|
||||||
await page.evaluate(() => {
|
|
||||||
const historyItem = {
|
|
||||||
id: '1',
|
|
||||||
label: 'SU 1402',
|
|
||||||
url: '/search?flight=SU1402',
|
|
||||||
timestamp: Date.now(),
|
|
||||||
};
|
|
||||||
localStorage.setItem('aeroflot_search_history', JSON.stringify([historyItem]));
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.reload();
|
|
||||||
|
|
||||||
// Verify it exists
|
|
||||||
const items1 = page.locator('[data-testid="landing-search-history-item"]');
|
|
||||||
const count1 = await items1.count();
|
|
||||||
expect(count1).toBeGreaterThan(0);
|
|
||||||
|
|
||||||
// Reload again
|
|
||||||
await page.reload();
|
|
||||||
|
|
||||||
// Verify it still exists
|
|
||||||
const items2 = page.locator('[data-testid="landing-search-history-item"]');
|
|
||||||
const count2 = await items2.count();
|
|
||||||
expect(count2).toBe(count1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should be responsive on mobile viewport', async ({ page }) => {
|
|
||||||
await page.setViewportSize({ width: 375, height: 667 });
|
|
||||||
|
|
||||||
await page.evaluate(() => {
|
|
||||||
const historyItem = {
|
|
||||||
id: '1',
|
|
||||||
label: 'SU 1402',
|
|
||||||
url: '/search?flight=SU1402',
|
|
||||||
timestamp: Date.now(),
|
|
||||||
};
|
|
||||||
localStorage.setItem('aeroflot_search_history', JSON.stringify([historyItem]));
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.reload();
|
|
||||||
|
|
||||||
const section = page.locator('[data-testid="landing-search-history"]');
|
|
||||||
await expect(section).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should handle large number of history items', async ({ page }) => {
|
|
||||||
// Create 20 history items
|
|
||||||
await page.evaluate(() => {
|
|
||||||
const historyItems = Array.from({ length: 20 }, (_, i) => ({
|
|
||||||
id: String(i + 1),
|
|
||||||
label: `SU ${1400 + i}`,
|
|
||||||
url: `/search?flight=SU${1400 + i}`,
|
|
||||||
timestamp: Date.now() - i * 60000,
|
|
||||||
}));
|
|
||||||
localStorage.setItem('aeroflot_search_history', JSON.stringify(historyItems));
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.reload();
|
|
||||||
|
|
||||||
const items = page.locator('[data-testid="landing-search-history-item"]');
|
|
||||||
await expect(items).toHaveCount(20);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should handle corrupted localStorage data gracefully', async ({ page }) => {
|
|
||||||
// Corrupt the localStorage
|
|
||||||
await page.evaluate(() => {
|
|
||||||
localStorage.setItem('aeroflot_search_history', 'corrupted{invalid json');
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.reload();
|
|
||||||
|
|
||||||
// Should not show history section
|
|
||||||
const section = page.locator('[data-testid="landing-search-history"]');
|
|
||||||
await expect(section).not.toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
|
||||||
|
|
||||||
test.describe('Search Panel - Filter Sidebar (US-6)', () => {
|
|
||||||
test.beforeEach(async ({ page }) => {
|
|
||||||
await page.goto('http://localhost:3000');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should render filter accordion container', async ({ page }) => {
|
|
||||||
const filterAccordion = page.locator('[data-testid="filter-accordion"]');
|
|
||||||
await expect(filterAccordion).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should render flight number search tab', async ({ page }) => {
|
|
||||||
const flightTab = page.locator('[data-testid="filter-flight-tab"]');
|
|
||||||
await expect(flightTab).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should render route search tab', async ({ page }) => {
|
|
||||||
const routeTab = page.locator('[data-testid="filter-route-tab"]');
|
|
||||||
await expect(routeTab).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should expand flight tab when clicked', async ({ page }) => {
|
|
||||||
const flightTab = page.locator('[data-testid="filter-flight-tab"]');
|
|
||||||
|
|
||||||
// Click to ensure it's expanded
|
|
||||||
await flightTab.click();
|
|
||||||
|
|
||||||
// Wait for search panel to appear
|
|
||||||
const searchByFlight = page.locator('[data-testid="search-by-flight"]');
|
|
||||||
await expect(searchByFlight).toBeVisible({ timeout: 5000 });
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display flight number input when flight tab is active', async ({ page }) => {
|
|
||||||
const flightTab = page.locator('[data-testid="filter-flight-tab"]');
|
|
||||||
await flightTab.click();
|
|
||||||
|
|
||||||
const flightInput = page.locator('[data-testid="filter-flight-number-input"]');
|
|
||||||
await expect(flightInput).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should allow entering flight number', async ({ page }) => {
|
|
||||||
const flightTab = page.locator('[data-testid="filter-flight-tab"]');
|
|
||||||
await flightTab.click();
|
|
||||||
|
|
||||||
const flightInput = page.locator('[data-testid="filter-flight-number-input"]');
|
|
||||||
await flightInput.fill('1402');
|
|
||||||
await expect(flightInput).toHaveValue('1402');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display flight suffix input', async ({ page }) => {
|
|
||||||
const flightTab = page.locator('[data-testid="filter-flight-tab"]');
|
|
||||||
await flightTab.click();
|
|
||||||
|
|
||||||
const suffixInput = page.locator('[data-testid="filter-flight-number-suffix-input"]');
|
|
||||||
await expect(suffixInput).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should allow entering flight suffix', async ({ page }) => {
|
|
||||||
const flightTab = page.locator('[data-testid="filter-flight-tab"]');
|
|
||||||
await flightTab.click();
|
|
||||||
|
|
||||||
const suffixInput = page.locator('[data-testid="filter-flight-number-suffix-input"]');
|
|
||||||
await suffixInput.fill('A');
|
|
||||||
await expect(suffixInput).toHaveValue('A');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display date picker', async ({ page }) => {
|
|
||||||
const flightTab = page.locator('[data-testid="filter-flight-tab"]');
|
|
||||||
await flightTab.click();
|
|
||||||
|
|
||||||
const datePicker = page.locator('[data-testid="filter-flight-number-calendar"]');
|
|
||||||
await expect(datePicker).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display search button', async ({ page }) => {
|
|
||||||
const flightTab = page.locator('[data-testid="filter-flight-tab"]');
|
|
||||||
await flightTab.click();
|
|
||||||
|
|
||||||
const searchButton = page.locator('[data-testid="filter-flight-number-search"]');
|
|
||||||
await expect(searchButton).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should expand route tab when clicked', async ({ page }) => {
|
|
||||||
const routeTab = page.locator('[data-testid="filter-route-tab"]');
|
|
||||||
|
|
||||||
// Click to ensure it's expanded
|
|
||||||
await routeTab.click();
|
|
||||||
|
|
||||||
// Wait for search panel to appear
|
|
||||||
const searchByRoute = page.locator('[data-testid="search-by-route"]');
|
|
||||||
await expect(searchByRoute).toBeVisible({ timeout: 5000 });
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should toggle between flight and route tabs', async ({ page }) => {
|
|
||||||
const flightTab = page.locator('[data-testid="filter-flight-tab"]');
|
|
||||||
const routeTab = page.locator('[data-testid="filter-route-tab"]');
|
|
||||||
|
|
||||||
// Open flight tab
|
|
||||||
await flightTab.click();
|
|
||||||
let flightContent = page.locator('[data-testid="search-by-flight"]');
|
|
||||||
await expect(flightContent).toBeVisible();
|
|
||||||
|
|
||||||
// Switch to route tab
|
|
||||||
await routeTab.click();
|
|
||||||
const routeContent = page.locator('[data-testid="search-by-route"]');
|
|
||||||
await expect(routeContent).toBeVisible();
|
|
||||||
|
|
||||||
// Flight content should no longer be visible
|
|
||||||
flightContent = page.locator('[data-testid="search-by-flight"]');
|
|
||||||
await expect(flightContent).not.toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should have SU prefix displayed', async ({ page }) => {
|
|
||||||
const flightTab = page.locator('[data-testid="filter-flight-tab"]');
|
|
||||||
await flightTab.click();
|
|
||||||
|
|
||||||
const suPrefix = page.locator('.prefix');
|
|
||||||
await expect(suPrefix).toContainText('SU');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should have clear button for flight number', async ({ page }) => {
|
|
||||||
const flightTab = page.locator('[data-testid="filter-flight-tab"]');
|
|
||||||
await flightTab.click();
|
|
||||||
|
|
||||||
const flightInput = page.locator('[data-testid="filter-flight-number-input"]');
|
|
||||||
await flightInput.fill('1402');
|
|
||||||
|
|
||||||
const clearButton = page.locator('[data-testid="filter-flight-number-clear"]').first();
|
|
||||||
await expect(clearButton).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should clear flight number when clear button clicked', async ({ page }) => {
|
|
||||||
const flightTab = page.locator('[data-testid="filter-flight-tab"]');
|
|
||||||
await flightTab.click();
|
|
||||||
|
|
||||||
const flightInput = page.locator('[data-testid="filter-flight-number-input"]');
|
|
||||||
await flightInput.fill('1402');
|
|
||||||
|
|
||||||
const clearButton = page.locator('[data-testid="filter-flight-number-clear"]').first();
|
|
||||||
await clearButton.click();
|
|
||||||
|
|
||||||
await expect(flightInput).toHaveValue('');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display all three search sections in filter accordion', async ({ page }) => {
|
|
||||||
const filterAccordion = page.locator('[data-testid="filter-accordion"]');
|
|
||||||
|
|
||||||
// Get all section headers
|
|
||||||
const sectionHeaders = filterAccordion.locator('button[class*="sectionHeader"]');
|
|
||||||
const count = await sectionHeaders.count();
|
|
||||||
|
|
||||||
expect(count).toBeGreaterThanOrEqual(3); // At least 3 sections (flight, route, arrival)
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should support keyboard navigation', async ({ page }) => {
|
|
||||||
const flightTab = page.locator('[data-testid="filter-flight-tab"]');
|
|
||||||
|
|
||||||
// Focus the button
|
|
||||||
await flightTab.focus();
|
|
||||||
|
|
||||||
// Press Enter to activate
|
|
||||||
await flightTab.press('Enter');
|
|
||||||
|
|
||||||
const searchByFlight = page.locator('[data-testid="search-by-flight"]');
|
|
||||||
await expect(searchByFlight).toBeVisible({ timeout: 5000 });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
|
||||||
|
|
||||||
test.describe('SEO & Meta Tags (US-9)', () => {
|
|
||||||
test('should have correct title and meta tags for ru-ru', async ({ page }) => {
|
|
||||||
await page.goto('http://localhost:3000/ru-ru/onlineboard');
|
|
||||||
|
|
||||||
const title = await page.title();
|
|
||||||
expect(title).toBeTruthy();
|
|
||||||
expect(title.length).toBeGreaterThan(0);
|
|
||||||
|
|
||||||
const description = await page.locator('meta[name="description"]').getAttribute('content');
|
|
||||||
expect(description).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should have correct title and meta tags for en-us', async ({ page }) => {
|
|
||||||
await page.goto('http://localhost:3000/en-us/onlineboard');
|
|
||||||
|
|
||||||
const title = await page.title();
|
|
||||||
expect(title).toBeTruthy();
|
|
||||||
expect(title.length).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should have OpenGraph tags on all pages', async ({ page }) => {
|
|
||||||
await page.goto('http://localhost:3000/ru-ru/onlineboard');
|
|
||||||
|
|
||||||
const ogTitle = await page.locator('meta[property="og:title"]').count();
|
|
||||||
expect(ogTitle).toBeGreaterThan(0);
|
|
||||||
|
|
||||||
const ogDescription = await page.locator('meta[property="og:description"]').count();
|
|
||||||
expect(ogDescription).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should have canonical link', async ({ page }) => {
|
|
||||||
await page.goto('http://localhost:3000/ru-ru/onlineboard');
|
|
||||||
|
|
||||||
const canonical = await page.locator('link[rel="canonical"]').count();
|
|
||||||
expect(canonical).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should have viewport meta tag', async ({ page }) => {
|
|
||||||
await page.goto('http://localhost:3000/ru-ru/onlineboard');
|
|
||||||
|
|
||||||
const viewport = await page.locator('meta[name="viewport"]').getAttribute('content');
|
|
||||||
expect(viewport).toContain('width=device-width');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should have correct language attribute', async ({ page }) => {
|
|
||||||
await page.goto('http://localhost:3000/ru-ru/onlineboard');
|
|
||||||
|
|
||||||
const lang = await page.locator('html').getAttribute('lang');
|
|
||||||
expect(lang).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should update lang attribute when changing locale', async ({ page }) => {
|
|
||||||
await page.goto('http://localhost:3000/ru-ru/onlineboard');
|
|
||||||
|
|
||||||
let lang = await page.locator('html').getAttribute('lang');
|
|
||||||
expect(lang).toBeTruthy();
|
|
||||||
|
|
||||||
// Switch to English
|
|
||||||
await page.goto('http://localhost:3000/en-us/onlineboard');
|
|
||||||
lang = await page.locator('html').getAttribute('lang');
|
|
||||||
expect(lang).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should have JSON-LD structured data', async ({ page }) => {
|
|
||||||
await page.goto('http://localhost:3000/ru-ru/onlineboard');
|
|
||||||
|
|
||||||
const jsonLd = await page.locator('script[type="application/ld+json"]').count();
|
|
||||||
expect(jsonLd).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
|
||||||
import { todayStr } from '../../src/lib/date-utils';
|
|
||||||
|
|
||||||
const today = todayStr();
|
|
||||||
const dateParam = today.replace(/-/g, '');
|
|
||||||
|
|
||||||
test.describe('Flight board visual regression', () => {
|
|
||||||
test('departures view matches screenshot', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/departure/MOW-${dateParam}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
await expect(page).toHaveScreenshot('flight-board-departures.png', {
|
|
||||||
fullPage: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('arrivals view matches screenshot', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/arrival/MOW-${dateParam}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
await expect(page).toHaveScreenshot('flight-board-arrivals.png', {
|
|
||||||
fullPage: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
|
||||||
import { todayStr } from '../../src/lib/date-utils';
|
|
||||||
|
|
||||||
const today = todayStr();
|
|
||||||
const dateParam = today.replace(/-/g, '');
|
|
||||||
|
|
||||||
test.describe('Expanded flight card visual regression', () => {
|
|
||||||
test('expanded card matches screenshot', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/departure/MOW-${dateParam}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await page.waitForSelector('[class*="card"]', { timeout: 10000 });
|
|
||||||
|
|
||||||
const firstCard = page.locator('[class*="card"]').first();
|
|
||||||
await firstCard.click();
|
|
||||||
|
|
||||||
const expandedSection = page.locator('[class*="expanded"]').first();
|
|
||||||
await expect(expandedSection).toBeVisible();
|
|
||||||
await expect(expandedSection).toHaveScreenshot('flight-expanded.png', { timeout: 10000 });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
|
||||||
import { todayStr } from '../../src/lib/date-utils';
|
|
||||||
|
|
||||||
const today = todayStr();
|
|
||||||
const dateParam = today.replace(/-/g, '');
|
|
||||||
|
|
||||||
test.describe('Landing page visual regression', () => {
|
|
||||||
test('matches landing page screenshot', async ({ page }) => {
|
|
||||||
await page.goto(`/ru-ru/onlineboard/departure/MOW-${dateParam}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
await expect(page).toHaveScreenshot('landing.png', {
|
|
||||||
fullPage: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user