375bcfb0fa
Copies Playwright e2e tests (58 specs, 300+ tests) designed for cross-app testing. Adapts API mocks to match real Aeroflot dictionary format (title objects with multilingual keys), adds board/schedule/days endpoint mocks, and provides Angular-specific Playwright config on port 4203.
1207 lines
46 KiB
TypeScript
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')}`;
|
|
}
|