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

1207 lines
46 KiB
TypeScript

import { test, expect } from '../support/cross-app-fixtures';
import { mockAllAPIs } from '../support/cross-app-fixtures';
import { S, tid } from '../support/selectors';
/**
* User Stories 17-30: Advanced Search & Filter Scenarios
*
* 17: User clears search inputs
* 18: User swaps departure and arrival cities
* 19: User selects date from calendar
* 20: User searches with date range
* 21: User views search history
* 22: User searches from search history
* 23: User searches with invalid city
* 24: User searches with empty fields
* 25: User searches with date before today
* 26: User searches with same departure and arrival
* 27: User searches with special characters
* 28: User searches with Unicode characters
* 29: User searches with very long city name
* 30: User searches with rapid attempts
*/
test.describe('User Stories 17-30: Advanced Search & Filter Scenarios', () => {
test.beforeEach(async ({ page, localePath }) => {
await mockAllAPIs(page);
await page.goto(localePath('onlineboard'));
await page.waitForLoadState('networkidle');
});
// ─────────────────────────────────────────────────────────────────────────
// Story 17: User clears search inputs
// ─────────────────────────────────────────────────────────────────────────
test('17.1: User clears departure city input', async ({ page, app, localePath }) => {
await page.goto(localePath('onlineboard'));
await page.waitForLoadState('networkidle');
const routeTab = page.locator(tid(S.FILTER_ROUTE_TAB, app));
const fallback = page.locator('[data-testid="route-filter"]');
const tabEl = (await routeTab.count()) > 0 ? routeTab : fallback;
const isExpanded = await page
.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app))
.isVisible()
.catch(() => false);
if (!isExpanded) {
const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
if ((await headerLink.count()) > 0) {
await headerLink.click();
} else {
await tabEl.click();
}
await page.waitForTimeout(500);
}
const departureInput = page.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app));
await departureInput.fill('Moscow');
await page.waitForTimeout(500);
const clearButton = departureInput
.locator(
'button[aria-label*="clear"], button[aria-label*="Clear"], .p-input-icon-right button, [data-testid*="clear"]',
)
.first();
if ((await clearButton.count()) > 0) {
await clearButton.click();
await page.waitForTimeout(300);
const value = await departureInput.inputValue();
expect(value).toBe('');
}
});
test('17.2: User clears arrival city input', async ({ page, app, localePath }) => {
await page.goto(localePath('onlineboard'));
await page.waitForLoadState('networkidle');
const routeTab = page.locator(tid(S.FILTER_ROUTE_TAB, app));
const fallback = page.locator('[data-testid="route-filter"]');
const tabEl = (await routeTab.count()) > 0 ? routeTab : fallback;
const isExpanded = await page
.locator(tid(S.FILTER_ROUTE_ARRIVAL_INPUT, app))
.isVisible()
.catch(() => false);
if (!isExpanded) {
const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
if ((await headerLink.count()) > 0) {
await headerLink.click();
} else {
await tabEl.click();
}
await page.waitForTimeout(500);
}
const arrivalInput = page.locator(tid(S.FILTER_ROUTE_ARRIVAL_INPUT, app));
await arrivalInput.fill('Sochi');
await page.waitForTimeout(500);
const clearButton = arrivalInput
.locator(
'button[aria-label*="clear"], button[aria-label*="Clear"], .p-input-icon-right button, [data-testid*="clear"]',
)
.first();
if ((await clearButton.count()) > 0) {
await clearButton.click();
await page.waitForTimeout(300);
const value = await arrivalInput.inputValue();
expect(value).toBe('');
}
});
test('17.3: User clears flight number input', async ({ page, app, localePath }) => {
await page.goto(localePath('onlineboard'));
await page.waitForLoadState('networkidle');
const flightTab = page.locator(tid(S.FILTER_FLIGHT_TAB, app));
const fallback = page.locator('[data-testid="flight-filter"]');
const tabEl = (await flightTab.count()) > 0 ? flightTab : fallback;
const isExpanded = await page
.locator(tid(S.FILTER_FLIGHT_NUMBER_INPUT, app))
.isVisible()
.catch(() => false);
if (!isExpanded) {
const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
if ((await headerLink.count()) > 0) {
await headerLink.click();
} else {
await tabEl.click();
}
await page.waitForTimeout(500);
}
const flightInput = page.locator(tid(S.FILTER_FLIGHT_NUMBER_INPUT, app));
await flightInput.fill('SU1234');
await page.waitForTimeout(500);
const clearButton = flightInput
.locator(
'button[aria-label*="clear"], button[aria-label*="Clear"], .p-input-icon-right button, [data-testid*="clear"]',
)
.first();
if ((await clearButton.count()) > 0) {
await clearButton.click();
await page.waitForTimeout(300);
const value = await flightInput.inputValue();
expect(value).toBe('');
}
});
test('17.4: User clears all search inputs at once', async ({ page, app, localePath }) => {
await page.goto(localePath('onlineboard'));
await page.waitForLoadState('networkidle');
const routeTab = page.locator(tid(S.FILTER_ROUTE_TAB, app));
const fallback = page.locator('[data-testid="route-filter"]');
const tabEl = (await routeTab.count()) > 0 ? routeTab : fallback;
const isExpanded = await page
.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app))
.isVisible()
.catch(() => false);
if (!isExpanded) {
const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
if ((await headerLink.count()) > 0) {
await headerLink.click();
} else {
await tabEl.click();
}
await page.waitForTimeout(500);
}
const departureInput = page.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app));
const arrivalInput = page.locator(tid(S.FILTER_ROUTE_ARRIVAL_INPUT, app));
await departureInput.fill('Moscow');
await arrivalInput.fill('Sochi');
await page.waitForTimeout(500);
const clearButtons = page.locator(
'button[aria-label*="clear"], button[aria-label*="Clear"], .p-input-icon-right button, [data-testid*="clear"]',
);
const count = await clearButtons.count();
if (count > 0) {
for (let i = 0; i < Math.min(count, 5); i++) {
await clearButtons.nth(i).click();
await page.waitForTimeout(200);
}
}
const depValue = await departureInput.inputValue();
const arrValue = await arrivalInput.inputValue();
expect(depValue).toBe('');
expect(arrValue).toBe('');
});
// ─────────────────────────────────────────────────────────────────────────
// Story 18: User swaps departure and arrival cities
// ─────────────────────────────────────────────────────────────────────────
test('18.1: User swaps departure and arrival cities', async ({ page, app, localePath }) => {
await page.goto(localePath('onlineboard'));
await page.waitForLoadState('networkidle');
const routeTab = page.locator(tid(S.FILTER_ROUTE_TAB, app));
const fallback = page.locator('[data-testid="route-filter"]');
const tabEl = (await routeTab.count()) > 0 ? routeTab : fallback;
const isExpanded = await page
.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app))
.isVisible()
.catch(() => false);
if (!isExpanded) {
const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
if ((await headerLink.count()) > 0) {
await headerLink.click();
} else {
await tabEl.click();
}
await page.waitForTimeout(500);
}
const departureInput = page.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app));
const arrivalInput = page.locator(tid(S.FILTER_ROUTE_ARRIVAL_INPUT, app));
const swapButton = page.locator(tid(S.FILTER_ROUTE_SWAP_BUTTON, app));
await departureInput.fill('Moscow');
await arrivalInput.fill('Sochi');
await page.waitForTimeout(500);
const depBefore = await departureInput.inputValue();
const arrBefore = await arrivalInput.inputValue();
if ((await swapButton.count()) > 0) {
await swapButton.click();
await page.waitForTimeout(500);
const depAfter = await departureInput.inputValue();
const arrAfter = await arrivalInput.inputValue();
expect(depAfter).toBe(arrBefore);
expect(arrAfter).toBe(depBefore);
}
});
test('18.2: Swap button is visible and enabled', async ({ page, app, localePath }) => {
await page.goto(localePath('onlineboard'));
await page.waitForLoadState('networkidle');
const routeTab = page.locator(tid(S.FILTER_ROUTE_TAB, app));
const fallback = page.locator('[data-testid="route-filter"]');
const tabEl = (await routeTab.count()) > 0 ? routeTab : fallback;
const isExpanded = await page
.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app))
.isVisible()
.catch(() => false);
if (!isExpanded) {
const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
if ((await headerLink.count()) > 0) {
await headerLink.click();
} else {
await tabEl.click();
}
await page.waitForTimeout(500);
}
const swapButton = page.locator(tid(S.FILTER_ROUTE_SWAP_BUTTON, app));
await expect(swapButton).toBeVisible();
await expect(swapButton).toBeEnabled();
});
// ─────────────────────────────────────────────────────────────────────────
// Story 19: User selects date from calendar
// ─────────────────────────────────────────────────────────────────────────
test('19.1: Calendar input is visible and clickable', async ({ page, app, localePath }) => {
await page.goto(localePath('onlineboard'));
await page.waitForLoadState('networkidle');
const routeTab = page.locator(tid(S.FILTER_ROUTE_TAB, app));
const fallback = page.locator('[data-testid="route-filter"]');
const tabEl = (await routeTab.count()) > 0 ? routeTab : fallback;
const isExpanded = await page
.locator(tid(S.FILTER_ROUTE_CALENDAR, app))
.isVisible()
.catch(() => false);
if (!isExpanded) {
const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
if ((await headerLink.count()) > 0) {
await headerLink.click();
} else {
await tabEl.click();
}
await page.waitForTimeout(500);
}
const calendarInput = page.locator(tid(S.FILTER_ROUTE_CALENDAR, app));
await expect(calendarInput).toBeVisible();
await expect(calendarInput).toBeEnabled();
});
test('19.2: Calendar overlay opens on click', async ({ page, app, localePath }) => {
await page.goto(localePath('onlineboard'));
await page.waitForLoadState('networkidle');
const routeTab = page.locator(tid(S.FILTER_ROUTE_TAB, app));
const fallback = page.locator('[data-testid="route-filter"]');
const tabEl = (await routeTab.count()) > 0 ? routeTab : fallback;
const isExpanded = await page
.locator(tid(S.FILTER_ROUTE_CALENDAR, app))
.isVisible()
.catch(() => false);
if (!isExpanded) {
const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
if ((await headerLink.count()) > 0) {
await headerLink.click();
} else {
await tabEl.click();
}
await page.waitForTimeout(500);
}
const calendarInput = page.locator(tid(S.FILTER_ROUTE_CALENDAR, app));
await calendarInput.click();
await page.waitForTimeout(500);
const calendarOverlay = page.locator(
'.p-calendar-panel, .p-datepicker, .ng-tns-c-date-picker, [role="dialog"]',
);
await expect(calendarOverlay.first()).toBeVisible({ timeout: 5000 });
});
test('19.3: User selects future date from calendar', async ({ page, app, localePath }) => {
await page.goto(localePath('onlineboard'));
await page.waitForLoadState('networkidle');
const routeTab = page.locator(tid(S.FILTER_ROUTE_TAB, app));
const fallback = page.locator('[data-testid="route-filter"]');
const tabEl = (await routeTab.count()) > 0 ? routeTab : fallback;
const isExpanded = await page
.locator(tid(S.FILTER_ROUTE_CALENDAR, app))
.isVisible()
.catch(() => false);
if (!isExpanded) {
const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
if ((await headerLink.count()) > 0) {
await headerLink.click();
} else {
await tabEl.click();
}
await page.waitForTimeout(500);
}
const calendarInput = page.locator(tid(S.FILTER_ROUTE_CALENDAR, app));
await calendarInput.click();
await page.waitForTimeout(500);
const calendarOverlay = page.locator(
'.p-calendar-panel, .p-datepicker, .ng-tns-c-date-picker, [role="dialog"]',
);
await expect(calendarOverlay.first()).toBeVisible({ timeout: 5000 });
const futureDate = calendarOverlay.locator(
'td.p-datepicker-day:not(.p-disabled):not(.p-datepicker-today):nth-child(>7)',
);
const count = await futureDate.count();
if (count > 0) {
await futureDate.first().click();
await page.waitForTimeout(300);
}
});
test('19.4: Selected date displays in correct format', async ({ page, app, localePath }) => {
await page.goto(localePath('onlineboard'));
await page.waitForLoadState('networkidle');
const routeTab = page.locator(tid(S.FILTER_ROUTE_TAB, app));
const fallback = page.locator('[data-testid="route-filter"]');
const tabEl = (await routeTab.count()) > 0 ? routeTab : fallback;
const isExpanded = await page
.locator(tid(S.FILTER_ROUTE_CALENDAR, app))
.isVisible()
.catch(() => false);
if (!isExpanded) {
const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
if ((await headerLink.count()) > 0) {
await headerLink.click();
} else {
await tabEl.click();
}
await page.waitForTimeout(500);
}
const calendarInput = page.locator(tid(S.FILTER_ROUTE_CALENDAR, app));
await calendarInput.click();
await page.waitForTimeout(500);
const calendarOverlay = page.locator(
'.p-calendar-panel, .p-datepicker, .ng-tns-c-date-picker, [role="dialog"]',
);
await expect(calendarOverlay.first()).toBeVisible({ timeout: 5000 });
const todayCell = calendarOverlay.locator('td.p-datepicker-today');
if ((await todayCell.count()) > 0) {
await todayCell.first().click();
await page.waitForTimeout(300);
const inputValue = await calendarInput.inputValue();
expect(inputValue.length).toBeGreaterThan(0);
const dateRegex = /\d{1,2}[.\\/-]\d{1,2}[.\\/-]\d{2,4}/;
expect(inputValue).toMatch(dateRegex);
}
});
// ─────────────────────────────────────────────────────────────────────────
// Story 20: User searches with date range
// ─────────────────────────────────────────────────────────────────────────
test('20.1: Schedule page has date range inputs', async ({ page, app, localePath }) => {
await page.goto(localePath('schedule'));
await page.waitForLoadState('networkidle');
const outboundCalendar = page.locator(tid(S.SCHEDULE_CALENDAR, app));
const returnCalendar = page.locator(tid(S.SCHEDULE_RETURN_CALENDAR, app));
const outboundVisible = await outboundCalendar.count();
const returnVisible = await returnCalendar.count();
expect(outboundVisible).toBeGreaterThan(0);
expect(returnVisible).toBeGreaterThan(0);
});
test('20.2: User selects outbound date', async ({ page, app, localePath }) => {
await page.goto(localePath('schedule'));
await page.waitForLoadState('networkidle');
const outboundCalendar = page.locator(tid(S.SCHEDULE_CALENDAR, app));
await outboundCalendar.click();
await page.waitForTimeout(500);
const calendarOverlay = page.locator(
'.p-calendar-panel, .p-datepicker, .ng-tns-c-date-picker, [role="dialog"]',
);
await expect(calendarOverlay.first()).toBeVisible({ timeout: 5000 });
const todayCell = calendarOverlay.locator('td.p-datepicker-today');
if ((await todayCell.count()) > 0) {
await todayCell.first().click();
await page.waitForTimeout(300);
}
});
test('20.3: User selects return date', async ({ page, app, localePath }) => {
await page.goto(localePath('schedule'));
await page.waitForLoadState('networkidle');
const returnCalendar = page.locator(tid(S.SCHEDULE_RETURN_CALENDAR, app));
await returnCalendar.click();
await page.waitForTimeout(500);
const calendarOverlay = page.locator(
'.p-calendar-panel, .p-datepicker, .ng-tns-c-date-picker, [role="dialog"]',
);
await expect(calendarOverlay.first()).toBeVisible({ timeout: 5000 });
const todayCell = calendarOverlay.locator('td.p-datepicker-today');
if ((await todayCell.count()) > 0) {
await todayCell.first().click();
await page.waitForTimeout(300);
}
});
test('20.4: Date range search executes successfully', async ({ page, app, localePath }) => {
await page.goto(localePath('schedule'));
await page.waitForLoadState('networkidle');
const departureInput = page.locator(tid(S.SCHEDULE_DEPARTURE_INPUT, app));
const arrivalInput = page.locator(tid(S.SCHEDULE_ARRIVAL_INPUT, app));
const outboundCalendar = page.locator(tid(S.SCHEDULE_CALENDAR, app));
const returnCalendar = page.locator(tid(S.SCHEDULE_RETURN_CALENDAR, app));
const searchButton = page.locator(tid(S.SCHEDULE_SEARCH_BUTTON, app));
await departureInput.fill('Moscow');
await arrivalInput.fill('Sochi');
await outboundCalendar.click();
await page.waitForTimeout(500);
const outboundOverlay = page.locator(
'.p-calendar-panel, .p-datepicker, .ng-tns-c-date-picker, [role="dialog"]',
);
const todayCell = outboundOverlay.locator('td.p-datepicker-today');
if ((await todayCell.count()) > 0) {
await todayCell.first().click();
await page.waitForTimeout(300);
}
await returnCalendar.click();
await page.waitForTimeout(500);
const returnOverlay = page.locator(
'.p-calendar-panel, .p-datepicker, .ng-tns-c-date-picker, [role="dialog"]',
);
const futureCell = returnOverlay.locator('td.p-datepicker-day:not(.p-disabled):nth-child(>7)');
const count = await futureCell.count();
if (count > 0) {
await futureCell.first().click();
await page.waitForTimeout(300);
}
await searchButton.click();
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
const results = page.locator(
'schedule-result, .schedule-result, [data-testid*="schedule-flight"], .schedule__item',
);
const countResults = await results.count();
expect(countResults).toBeGreaterThanOrEqual(0);
});
// ─────────────────────────────────────────────────────────────────────────
// Story 21: User views search history
// ─────────────────────────────────────────────────────────────────────────
test('21.1: Search history section exists on landing', async ({ page, app, localePath }) => {
await page.goto(localePath('onlineboard'));
await page.waitForLoadState('networkidle');
const historySection = page.locator(
'search-history, [data-testid="landing-search-history"], .search-history, [class*="search-history"]',
);
const count = await historySection.count();
expect(count).toBeGreaterThan(0);
});
test('21.2: Search history is empty by default', async ({ page, app, localePath }) => {
await page.goto(localePath('onlineboard'));
await page.waitForLoadState('networkidle');
const historySection = page.locator(
'search-history, [data-testid="landing-search-history"], .search-history, [class*="search-history"]',
);
const historyItems = historySection.locator(
'.history-item, [data-testid="landing-search-history-item"], .search-history__item',
);
const count = await historyItems.count();
expect(count).toBeGreaterThanOrEqual(0);
});
test('21.3: Search history appears after performing search', async ({
page,
app,
localePath,
}) => {
const today = formatToday();
await page.goto(`/${localePath('onlineboard')}/departure/MOW-${today}`);
await page.waitForLoadState('networkidle');
await page.waitForTimeout(2000);
await page.goto(localePath('onlineboard'));
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
const historyItems = page.locator(
'search-history .history-item, [data-testid="landing-search-history-item"], .search-history__item',
);
const count = await historyItems.count();
if (count > 0) {
expect(count).toBeGreaterThanOrEqual(1);
}
});
// ─────────────────────────────────────────────────────────────────────────
// Story 22: User searches from search history
// ─────────────────────────────────────────────────────────────────────────
test('22.1: Clicking history item re-executes search', async ({ page, app, localePath }) => {
const today = formatToday();
await page.goto(`/${localePath('onlineboard')}/departure/MOW-${today}`);
await page.waitForLoadState('networkidle');
await page.waitForTimeout(2000);
await page.goto(localePath('onlineboard'));
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
const historyItems = page.locator(
'search-history .history-item, [data-testid="landing-search-history-item"], .search-history__item',
);
const count = await historyItems.count();
if (count > 0) {
const urlBefore = page.url();
await historyItems.first().click();
await page.waitForTimeout(1000);
const urlAfter = page.url();
expect(urlAfter).not.toBe(urlBefore);
}
});
test('22.2: History item URL matches search parameters', async ({ page, app, localePath }) => {
const today = formatToday();
await page.goto(`/${localePath('onlineboard')}/departure/MOW-${today}`);
await page.waitForLoadState('networkidle');
await page.waitForTimeout(2000);
await page.goto(localePath('onlineboard'));
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
const historyItems = page.locator(
'search-history .history-item, [data-testid="landing-search-history-item"], .search-history__item',
);
const count = await historyItems.count();
if (count > 0) {
await historyItems.first().click();
await page.waitForTimeout(1000);
const url = page.url();
expect(url).toContain('departure');
expect(url).toContain('MOW');
}
});
// ─────────────────────────────────────────────────────────────────────────
// Story 23: User searches with invalid city
// ─────────────────────────────────────────────────────────────────────────
test('23.1: Invalid city shows error message', async ({ page, app, localePath }) => {
await page.goto(localePath('onlineboard'));
await page.waitForLoadState('networkidle');
const routeTab = page.locator(tid(S.FILTER_ROUTE_TAB, app));
const fallback = page.locator('[data-testid="route-filter"]');
const tabEl = (await routeTab.count()) > 0 ? routeTab : fallback;
const isExpanded = await page
.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app))
.isVisible()
.catch(() => false);
if (!isExpanded) {
const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
if ((await headerLink.count()) > 0) {
await headerLink.click();
} else {
await tabEl.click();
}
await page.waitForTimeout(500);
}
const departureInput = page.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app));
await departureInput.fill('INVALIDCITYXYZ');
await page.waitForTimeout(500);
const searchButton = page.locator(tid(S.FILTER_ROUTE_SEARCH, app));
await searchButton.click();
await page.waitForTimeout(1000);
const errorMessages = page.locator(
'.p-error, .error-message, [role="alert"], .ng-invalid.ng-dirty',
);
const count = await errorMessages.count();
if (count > 0) {
expect(count).toBeGreaterThanOrEqual(1);
}
});
test('23.2: Invalid city search does not return flights', async ({ page, app, localePath }) => {
await page.goto(localePath('onlineboard'));
await page.waitForLoadState('networkidle');
const routeTab = page.locator(tid(S.FILTER_ROUTE_TAB, app));
const fallback = page.locator('[data-testid="route-filter"]');
const tabEl = (await routeTab.count()) > 0 ? routeTab : fallback;
const isExpanded = await page
.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app))
.isVisible()
.catch(() => false);
if (!isExpanded) {
const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
if ((await headerLink.count()) > 0) {
await headerLink.click();
} else {
await tabEl.click();
}
await page.waitForTimeout(500);
}
const departureInput = page.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app));
await departureInput.fill('INVALIDCITYXYZ');
await page.waitForTimeout(500);
const searchButton = page.locator(tid(S.FILTER_ROUTE_SEARCH, app));
await searchButton.click();
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
const flightResults = page.locator(
'flight-result, .flight-result, [data-testid*="flight-result"], .flight__item',
);
const count = await flightResults.count();
expect(count).toBeGreaterThanOrEqual(0);
});
// ─────────────────────────────────────────────────────────────────────────
// Story 24: User searches with empty fields
// ─────────────────────────────────────────────────────────────────────────
test('24.1: Empty search shows validation error', async ({ page, app, localePath }) => {
await page.goto(localePath('onlineboard'));
await page.waitForLoadState('networkidle');
const routeTab = page.locator(tid(S.FILTER_ROUTE_TAB, app));
const fallback = page.locator('[data-testid="route-filter"]');
const tabEl = (await routeTab.count()) > 0 ? routeTab : fallback;
const isExpanded = await page
.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app))
.isVisible()
.catch(() => false);
if (!isExpanded) {
const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
if ((await headerLink.count()) > 0) {
await headerLink.click();
} else {
await tabEl.click();
}
await page.waitForTimeout(500);
}
const searchButton = page.locator(tid(S.FILTER_ROUTE_SEARCH, app));
await searchButton.click();
await page.waitForTimeout(500);
const errorMessages = page.locator(
'.p-error, .error-message, [role="alert"], .ng-invalid.ng-dirty',
);
const count = await errorMessages.count();
if (count > 0) {
expect(count).toBeGreaterThanOrEqual(1);
}
});
test('24.2: Empty search does not navigate away', async ({ page, app, localePath }) => {
const urlBefore = page.url();
await page.goto(localePath('onlineboard'));
await page.waitForLoadState('networkidle');
const routeTab = page.locator(tid(S.FILTER_ROUTE_TAB, app));
const fallback = page.locator('[data-testid="route-filter"]');
const tabEl = (await routeTab.count()) > 0 ? routeTab : fallback;
const isExpanded = await page
.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app))
.isVisible()
.catch(() => false);
if (!isExpanded) {
const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
if ((await headerLink.count()) > 0) {
await headerLink.click();
} else {
await tabEl.click();
}
await page.waitForTimeout(500);
}
const searchButton = page.locator(tid(S.FILTER_ROUTE_SEARCH, app));
await searchButton.click();
await page.waitForTimeout(500);
const urlAfter = page.url();
expect(urlAfter).toBe(urlBefore);
});
// ─────────────────────────────────────────────────────────────────────────
// Story 25: User searches with date before today
// ─────────────────────────────────────────────────────────────────────────
test('25.1: Past date shows validation error', async ({ page, app, localePath }) => {
await page.goto(localePath('onlineboard'));
await page.waitForLoadState('networkidle');
const routeTab = page.locator(tid(S.FILTER_ROUTE_TAB, app));
const fallback = page.locator('[data-testid="route-filter"]');
const tabEl = (await routeTab.count()) > 0 ? routeTab : fallback;
const isExpanded = await page
.locator(tid(S.FILTER_ROUTE_CALENDAR, app))
.isVisible()
.catch(() => false);
if (!isExpanded) {
const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
if ((await headerLink.count()) > 0) {
await headerLink.click();
} else {
await tabEl.click();
}
await page.waitForTimeout(500);
}
const calendarInput = page.locator(tid(S.FILTER_ROUTE_CALENDAR, app));
await calendarInput.click();
await page.waitForTimeout(500);
const calendarOverlay = page.locator(
'.p-calendar-panel, .p-datepicker, .ng-tns-c-date-picker, [role="dialog"]',
);
await expect(calendarOverlay.first()).toBeVisible({ timeout: 5000 });
const pastDate = calendarOverlay.locator('td.p-datepicker-day.p-disabled');
const count = await pastDate.count();
if (count > 0) {
await pastDate.first().click();
await page.waitForTimeout(300);
const searchButton = page.locator(tid(S.FILTER_ROUTE_SEARCH, app));
await searchButton.click();
await page.waitForTimeout(500);
const errorMessages = page.locator(
'.p-error, .error-message, [role="alert"], .ng-invalid.ng-dirty',
);
const errorCount = await errorMessages.count();
if (errorCount > 0) {
expect(errorCount).toBeGreaterThanOrEqual(1);
}
}
});
// ─────────────────────────────────────────────────────────────────────────
// Story 26: User searches with same departure and arrival
// ─────────────────────────────────────────────────────────────────────────
test('26.1: Same departure and arrival shows validation error', async ({
page,
app,
localePath,
}) => {
await page.goto(localePath('onlineboard'));
await page.waitForLoadState('networkidle');
const routeTab = page.locator(tid(S.FILTER_ROUTE_TAB, app));
const fallback = page.locator('[data-testid="route-filter"]');
const tabEl = (await routeTab.count()) > 0 ? routeTab : fallback;
const isExpanded = await page
.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app))
.isVisible()
.catch(() => false);
if (!isExpanded) {
const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
if ((await headerLink.count()) > 0) {
await headerLink.click();
} else {
await tabEl.click();
}
await page.waitForTimeout(500);
}
const departureInput = page.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app));
const arrivalInput = page.locator(tid(S.FILTER_ROUTE_ARRIVAL_INPUT, app));
await departureInput.fill('Moscow');
await arrivalInput.fill('Moscow');
await page.waitForTimeout(500);
const searchButton = page.locator(tid(S.FILTER_ROUTE_SEARCH, app));
await searchButton.click();
await page.waitForTimeout(500);
const errorMessages = page.locator(
'.p-error, .error-message, [role="alert"], .ng-invalid.ng-dirty',
);
const count = await errorMessages.count();
if (count > 0) {
expect(count).toBeGreaterThanOrEqual(1);
}
});
// ─────────────────────────────────────────────────────────────────────────
// Story 27: User searches with special characters
// ─────────────────────────────────────────────────────────────────────────
test('27.1: Special characters in flight number', async ({ page, app, localePath }) => {
await page.goto(localePath('onlineboard'));
await page.waitForLoadState('networkidle');
const flightTab = page.locator(tid(S.FILTER_FLIGHT_TAB, app));
const fallback = page.locator('[data-testid="flight-filter"]');
const tabEl = (await flightTab.count()) > 0 ? flightTab : fallback;
const isExpanded = await page
.locator(tid(S.FILTER_FLIGHT_NUMBER_INPUT, app))
.isVisible()
.catch(() => false);
if (!isExpanded) {
const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
if ((await headerLink.count()) > 0) {
await headerLink.click();
} else {
await tabEl.click();
}
await page.waitForTimeout(500);
}
const flightInput = page.locator(tid(S.FILTER_FLIGHT_NUMBER_INPUT, app));
await flightInput.fill('SU@#$%123');
await page.waitForTimeout(500);
const searchButton = page.locator(tid(S.FILTER_FLIGHT_NUMBER_SEARCH, app));
await searchButton.click();
await page.waitForTimeout(1000);
const errorMessages = page.locator(
'.p-error, .error-message, [role="alert"], .ng-invalid.ng-dirty',
);
const count = await errorMessages.count();
if (count > 0) {
expect(count).toBeGreaterThanOrEqual(1);
}
});
test('27.2: Special characters in city name', async ({ page, app, localePath }) => {
await page.goto(localePath('onlineboard'));
await page.waitForLoadState('networkidle');
const routeTab = page.locator(tid(S.FILTER_ROUTE_TAB, app));
const fallback = page.locator('[data-testid="route-filter"]');
const tabEl = (await routeTab.count()) > 0 ? routeTab : fallback;
const isExpanded = await page
.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app))
.isVisible()
.catch(() => false);
if (!isExpanded) {
const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
if ((await headerLink.count()) > 0) {
await headerLink.click();
} else {
await tabEl.click();
}
await page.waitForTimeout(500);
}
const departureInput = page.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app));
await departureInput.fill('Moscow@#$%');
await page.waitForTimeout(500);
const searchButton = page.locator(tid(S.FILTER_ROUTE_SEARCH, app));
await searchButton.click();
await page.waitForTimeout(1000);
const errorMessages = page.locator(
'.p-error, .error-message, [role="alert"], .ng-invalid.ng-dirty',
);
const count = await errorMessages.count();
if (count > 0) {
expect(count).toBeGreaterThanOrEqual(1);
}
});
// ─────────────────────────────────────────────────────────────────────────
// Story 28: User searches with Unicode characters
// ─────────────────────────────────────────────────────────────────────────
test('28.1: Unicode characters in city name', async ({ page, app, localePath }) => {
await page.goto(localePath('onlineboard'));
await page.waitForLoadState('networkidle');
const routeTab = page.locator(tid(S.FILTER_ROUTE_TAB, app));
const fallback = page.locator('[data-testid="route-filter"]');
const tabEl = (await routeTab.count()) > 0 ? routeTab : fallback;
const isExpanded = await page
.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app))
.isVisible()
.catch(() => false);
if (!isExpanded) {
const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
if ((await headerLink.count()) > 0) {
await headerLink.click();
} else {
await tabEl.click();
}
await page.waitForTimeout(500);
}
const departureInput = page.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app));
await departureInput.fill('Москва');
await page.waitForTimeout(500);
const searchButton = page.locator(tid(S.FILTER_ROUTE_SEARCH, app));
await searchButton.click();
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
const results = page.locator(
'flight-result, .flight-result, [data-testid*="flight-result"], .flight__item',
);
const count = await results.count();
expect(count).toBeGreaterThanOrEqual(0);
});
test('28.2: Unicode characters in flight number', async ({ page, app, localePath }) => {
await page.goto(localePath('onlineboard'));
await page.waitForLoadState('networkidle');
const flightTab = page.locator(tid(S.FILTER_FLIGHT_TAB, app));
const fallback = page.locator('[data-testid="flight-filter"]');
const tabEl = (await flightTab.count()) > 0 ? flightTab : fallback;
const isExpanded = await page
.locator(tid(S.FILTER_FLIGHT_NUMBER_INPUT, app))
.isVisible()
.catch(() => false);
if (!isExpanded) {
const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
if ((await headerLink.count()) > 0) {
await headerLink.click();
} else {
await tabEl.click();
}
await page.waitForTimeout(500);
}
const flightInput = page.locator(tid(S.FILTER_FLIGHT_NUMBER_INPUT, app));
await flightInput.fill('СУ1234');
await page.waitForTimeout(500);
const searchButton = page.locator(tid(S.FILTER_FLIGHT_NUMBER_SEARCH, app));
await searchButton.click();
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
const results = page.locator(
'flight-result, .flight-result, [data-testid*="flight-result"], .flight__item',
);
const count = await results.count();
expect(count).toBeGreaterThanOrEqual(0);
});
// ─────────────────────────────────────────────────────────────────────────
// Story 29: User searches with very long city name
// ─────────────────────────────────────────────────────────────────────────
test('29.1: Very long city name is accepted', async ({ page, app, localePath }) => {
await page.goto(localePath('onlineboard'));
await page.waitForLoadState('networkidle');
const routeTab = page.locator(tid(S.FILTER_ROUTE_TAB, app));
const fallback = page.locator('[data-testid="route-filter"]');
const tabEl = (await routeTab.count()) > 0 ? routeTab : fallback;
const isExpanded = await page
.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app))
.isVisible()
.catch(() => false);
if (!isExpanded) {
const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
if ((await headerLink.count()) > 0) {
await headerLink.click();
} else {
await tabEl.click();
}
await page.waitForTimeout(500);
}
const departureInput = page.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app));
const longName = 'Москва' + ' '.repeat(100) + 'Moscow';
await departureInput.fill(longName);
await page.waitForTimeout(500);
const searchButton = page.locator(tid(S.FILTER_ROUTE_SEARCH, app));
await searchButton.click();
await page.waitForTimeout(1000);
const errorMessages = page.locator(
'.p-error, .error-message, [role="alert"], .ng-invalid.ng-dirty',
);
const count = await errorMessages.count();
if (count > 0) {
expect(count).toBeGreaterThanOrEqual(1);
}
});
// ─────────────────────────────────────────────────────────────────────────
// Story 30: User searches with rapid attempts
// ─────────────────────────────────────────────────────────────────────────
test('30.1: Rapid searches do not crash the app', async ({ page, app, localePath }) => {
await page.goto(localePath('onlineboard'));
await page.waitForLoadState('networkidle');
const routeTab = page.locator(tid(S.FILTER_ROUTE_TAB, app));
const fallback = page.locator('[data-testid="route-filter"]');
const tabEl = (await routeTab.count()) > 0 ? routeTab : fallback;
const isExpanded = await page
.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app))
.isVisible()
.catch(() => false);
if (!isExpanded) {
const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
if ((await headerLink.count()) > 0) {
await headerLink.click();
} else {
await tabEl.click();
}
await page.waitForTimeout(500);
}
const departureInput = page.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app));
const searchButton = page.locator(tid(S.FILTER_ROUTE_SEARCH, app));
for (let i = 0; i < 5; i++) {
await departureInput.fill(`City${i}`);
await page.waitForTimeout(100);
await searchButton.click();
await page.waitForTimeout(200);
}
const consoleErrors: string[] = [];
page.on('console', (msg) => {
if (msg.type() === 'error') {
consoleErrors.push(msg.text());
}
});
await page.waitForTimeout(1000);
expect(consoleErrors.length).toBeLessThanOrEqual(0);
});
test('30.2: Rapid searches show loading state', async ({ page, app, localePath }) => {
await page.goto(localePath('onlineboard'));
await page.waitForLoadState('networkidle');
const routeTab = page.locator(tid(S.FILTER_ROUTE_TAB, app));
const fallback = page.locator('[data-testid="route-filter"]');
const tabEl = (await routeTab.count()) > 0 ? routeTab : fallback;
const isExpanded = await page
.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app))
.isVisible()
.catch(() => false);
if (!isExpanded) {
const headerLink = tabEl.locator('.p-accordion-header-link, .p-accordion-header a').first();
if ((await headerLink.count()) > 0) {
await headerLink.click();
} else {
await tabEl.click();
}
await page.waitForTimeout(500);
}
const departureInput = page.locator(tid(S.FILTER_ROUTE_DEPARTURE_INPUT, app));
const searchButton = page.locator(tid(S.FILTER_ROUTE_SEARCH, app));
const loader = page.locator(tid(S.BOARD_LOADER, app));
for (let i = 0; i < 3; i++) {
await departureInput.fill(`City${i}`);
await page.waitForTimeout(100);
await searchButton.click();
await page.waitForTimeout(200);
const loaderVisible = await loader.count();
if (loaderVisible > 0) {
await loader.first().waitFor({ state: 'hidden', timeout: 10000 });
}
}
});
});
function formatToday(): string {
const d = new Date();
return `${d.getFullYear()}${String(d.getMonth() + 1).padStart(2, '0')}${String(d.getDate()).padStart(2, '0')}`;
}