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.
234 lines
9.5 KiB
TypeScript
234 lines
9.5 KiB
TypeScript
import { test, expect } from '../support/cross-app-fixtures';
|
|
import { mockAllAPIs } from '../support/cross-app-fixtures';
|
|
import { S, tid } from '../support/selectors';
|
|
|
|
test.describe('Navigation & Layout', () => {
|
|
test.beforeEach(async ({ page, localePath }) => {
|
|
await mockAllAPIs(page);
|
|
await page.goto(localePath('onlineboard'));
|
|
await page.waitForLoadState('networkidle');
|
|
});
|
|
|
|
test('1: Tab "Online Board" is visible and active by default', async ({ page, app }) => {
|
|
const tab = page.locator(tid(S.NAV_ONLINEBOARD_TAB, app));
|
|
await expect(tab).toBeVisible();
|
|
await expect(tab).toHaveClass(/active|selected/);
|
|
});
|
|
|
|
test('2: Tab "Schedule" navigates to schedule page', async ({ page, app, locale }) => {
|
|
const tab = page.locator(tid(S.NAV_SCHEDULE_TAB, app));
|
|
await expect(tab).toBeVisible();
|
|
await tab.click();
|
|
await expect(page).toHaveURL(new RegExp(`/${locale}/schedule`));
|
|
});
|
|
|
|
test('3: Tab "Flights Map" navigates to flights map page', async ({ page, app, locale }) => {
|
|
const tab = page.locator(tid(S.NAV_FLIGHTS_MAP_TAB, app));
|
|
await expect(tab).toBeVisible();
|
|
await tab.click();
|
|
await expect(page).toHaveURL(new RegExp(`/${locale}/flights-map`));
|
|
});
|
|
|
|
test('4: Tab active state matches current route', async ({ page, app, locale }) => {
|
|
// Navigate to schedule
|
|
await page.goto(`/${locale}/schedule`);
|
|
await page.waitForLoadState('networkidle');
|
|
const scheduleTab = page.locator(tid(S.NAV_SCHEDULE_TAB, app));
|
|
await expect(scheduleTab).toHaveClass(/active|selected/);
|
|
const boardTab = page.locator(tid(S.NAV_ONLINEBOARD_TAB, app));
|
|
await expect(boardTab).not.toHaveClass(/active|selected/);
|
|
});
|
|
|
|
test('5: Breadcrumbs show correct path on landing', async ({ page, app }) => {
|
|
const breadcrumbs = page.locator(tid(S.LAYOUT_BREADCRUMBS, app));
|
|
// Angular uses PrimeNG p-breadcrumb without data-testid; fall back to tag selector
|
|
const fallback = page.locator('p-breadcrumb, nav[aria-label*="bread"]');
|
|
const target = (await breadcrumbs.count()) > 0 ? breadcrumbs : fallback;
|
|
await expect(target).toBeVisible();
|
|
});
|
|
|
|
test('6: Breadcrumbs show correct path on search results', async ({
|
|
page,
|
|
app,
|
|
locale,
|
|
localePath,
|
|
}) => {
|
|
// Navigate to a departure search
|
|
const path = `onlineboard/departure/MOW-${formatToday()}`;
|
|
const url = localePath(path);
|
|
console.log('Test 6 URL:', url);
|
|
await page.goto(url, {
|
|
waitUntil: 'domcontentloaded',
|
|
});
|
|
console.log('Test 6 Current URL:', page.url());
|
|
const breadcrumbs = page.locator(tid(S.LAYOUT_BREADCRUMBS, app));
|
|
const fallback = page.locator('p-breadcrumb, nav[aria-label*="bread"]');
|
|
const target = (await breadcrumbs.count()) > 0 ? breadcrumbs : fallback;
|
|
await expect(target).toBeVisible();
|
|
// Should have at least 1 link
|
|
const links = target.locator('a');
|
|
expect(await links.count()).toBeGreaterThanOrEqual(1);
|
|
});
|
|
|
|
test('7: Breadcrumbs show correct path on flight details', async ({ page, app, locale }) => {
|
|
// Navigate to a search first, then open details
|
|
await page.goto(`/${locale}/onlineboard/departure/MOW-${formatToday()}`);
|
|
await page.waitForLoadState('networkidle');
|
|
const firstFlight = page.locator(tid(S.BOARD_FLIGHT_RESULT, app)).first();
|
|
// If results exist, click through to details
|
|
const count = await firstFlight.count();
|
|
if (count > 0) {
|
|
await firstFlight.click();
|
|
await page.waitForLoadState('networkidle');
|
|
const breadcrumbs = page.locator(tid(S.LAYOUT_BREADCRUMBS, app));
|
|
const fallback = page.locator('p-breadcrumb, nav[aria-label*="bread"]');
|
|
const target = (await breadcrumbs.count()) > 0 ? breadcrumbs : fallback;
|
|
await expect(target).toBeVisible();
|
|
expect(await target.locator('a').count()).toBeGreaterThanOrEqual(2);
|
|
}
|
|
});
|
|
|
|
test('8: Breadcrumbs links are clickable and navigate correctly', async ({
|
|
page,
|
|
app,
|
|
locale,
|
|
}) => {
|
|
await page.goto(`/${locale}/schedule`);
|
|
await page.waitForLoadState('networkidle');
|
|
const breadcrumbs = page.locator(tid(S.LAYOUT_BREADCRUMBS, app));
|
|
const fallback = page.locator('p-breadcrumb, nav[aria-label*="bread"]');
|
|
const target = (await breadcrumbs.count()) > 0 ? breadcrumbs : fallback;
|
|
const links = target.locator('a');
|
|
if ((await links.count()) > 0) {
|
|
// The first breadcrumb link typically navigates home or to main section
|
|
const firstHref = await links.first().getAttribute('href');
|
|
expect(firstHref).toBeTruthy();
|
|
}
|
|
});
|
|
|
|
test('9: Locale switcher button shows current locale code', async ({ page, app }) => {
|
|
const switcher = page.locator(tid(S.LAYOUT_LOCALE_SWITCHER, app));
|
|
if ((await switcher.count()) === 0) {
|
|
test.skip(true, 'Locale switcher not present in this app');
|
|
return;
|
|
}
|
|
await expect(switcher).toBeVisible();
|
|
});
|
|
|
|
test('10: Locale switcher dropdown opens on click', async ({ page, app }) => {
|
|
const switcher = page.locator(tid(S.LAYOUT_LOCALE_SWITCHER, app));
|
|
if ((await switcher.count()) === 0) {
|
|
test.skip(true, 'Locale switcher not present in this app');
|
|
return;
|
|
}
|
|
await switcher.click();
|
|
const options = page.locator(tid(S.LAYOUT_LOCALE_OPTION, app));
|
|
await expect(options.first()).toBeVisible();
|
|
});
|
|
|
|
test('11: Locale switcher shows all available locales', async ({ page, app }) => {
|
|
const switcher = page.locator(tid(S.LAYOUT_LOCALE_SWITCHER, app));
|
|
if ((await switcher.count()) === 0) {
|
|
test.skip(true, 'Locale switcher not present in this app');
|
|
return;
|
|
}
|
|
await switcher.click();
|
|
const options = page.locator(tid(S.LAYOUT_LOCALE_OPTION, app));
|
|
expect(await options.count()).toBeGreaterThanOrEqual(9);
|
|
});
|
|
|
|
test('12: Locale switcher changes URL prefix on selection', async ({ page, app, locale }) => {
|
|
const switcher = page.locator(tid(S.LAYOUT_LOCALE_SWITCHER, app));
|
|
if ((await switcher.count()) === 0) {
|
|
test.skip(true, 'Locale switcher not present in this app');
|
|
return;
|
|
}
|
|
await switcher.click();
|
|
const targetLocale = locale === 'en-us' ? 'ru-ru' : 'en-us';
|
|
const option = page.locator(
|
|
`${tid(S.LAYOUT_LOCALE_OPTION, app)}[data-locale="${targetLocale}"], ${tid(S.LAYOUT_LOCALE_OPTION, app)}:has-text("${targetLocale === 'en-us' ? 'English' : 'Русский'}")`,
|
|
);
|
|
if ((await option.count()) > 0) {
|
|
await option.first().click();
|
|
await expect(page).toHaveURL(new RegExp(`/${targetLocale}/`));
|
|
}
|
|
});
|
|
|
|
test('13: Locale switcher closes on outside click', async ({ page, app }) => {
|
|
const switcher = page.locator(tid(S.LAYOUT_LOCALE_SWITCHER, app));
|
|
if ((await switcher.count()) === 0) {
|
|
test.skip(true, 'Locale switcher not present in this app');
|
|
return;
|
|
}
|
|
await switcher.click();
|
|
const options = page.locator(tid(S.LAYOUT_LOCALE_OPTION, app));
|
|
await expect(options.first()).toBeVisible();
|
|
await page.locator('body').click({ position: { x: 0, y: 0 } });
|
|
await expect(options.first()).toBeHidden();
|
|
});
|
|
|
|
test('14: Feedback button is visible in layout', async ({ page, app }) => {
|
|
const button = page.locator(tid(S.LAYOUT_FEEDBACK_BUTTON, app));
|
|
if ((await button.count()) === 0) {
|
|
test.skip(true, 'Feedback button not present in this app');
|
|
return;
|
|
}
|
|
await expect(button).toBeVisible();
|
|
});
|
|
|
|
test('15: Feedback button opens feedback form on click', async ({ page, app }) => {
|
|
const button = page.locator(tid(S.LAYOUT_FEEDBACK_BUTTON, app));
|
|
if ((await button.count()) === 0) {
|
|
test.skip(true, 'Feedback button not present in this app');
|
|
return;
|
|
}
|
|
await button.click();
|
|
await expect(page.locator('[role="dialog"], .feedback-form, .modal')).toBeVisible({
|
|
timeout: 5000,
|
|
});
|
|
});
|
|
|
|
test('16: Scroll-to-top button appears after scrolling down', async ({ page, app }) => {
|
|
const scrollBtn = page.locator(tid(S.LAYOUT_SCROLL_TOP_BUTTON, app));
|
|
// Some apps may not have this feature
|
|
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
|
|
await page.waitForTimeout(500);
|
|
if ((await scrollBtn.count()) === 0) {
|
|
test.skip(true, 'Scroll-to-top button not present in this app');
|
|
return;
|
|
}
|
|
await expect(scrollBtn).toBeVisible({ timeout: 5000 });
|
|
});
|
|
|
|
test('17: Scroll-to-top button scrolls page to top on click', async ({ page, app }) => {
|
|
const scrollBtn = page.locator(tid(S.LAYOUT_SCROLL_TOP_BUTTON, app));
|
|
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
|
|
await page.waitForTimeout(500);
|
|
if ((await scrollBtn.count()) === 0) {
|
|
test.skip(true, 'Scroll-to-top button not present in this app');
|
|
return;
|
|
}
|
|
await expect(scrollBtn).toBeVisible({ timeout: 5000 });
|
|
await scrollBtn.click();
|
|
await page.waitForTimeout(500);
|
|
const scrollY = await page.evaluate(() => window.scrollY);
|
|
expect(scrollY).toBeLessThan(100);
|
|
});
|
|
|
|
test('18: Scroll-to-top button hides when at top', async ({ page, app }) => {
|
|
const scrollBtn = page.locator(tid(S.LAYOUT_SCROLL_TOP_BUTTON, app));
|
|
if ((await scrollBtn.count()) === 0) {
|
|
test.skip(true, 'Scroll-to-top button not present in this app');
|
|
return;
|
|
}
|
|
// At top of page, button should be hidden
|
|
await expect(scrollBtn).toBeHidden();
|
|
});
|
|
});
|
|
|
|
function formatToday(timeFrom = '0000', timeTo = '2359'): string {
|
|
const d = new Date();
|
|
const dateStr = `${d.getFullYear()}${String(d.getMonth() + 1).padStart(2, '0')}${String(d.getDate()).padStart(2, '0')}`;
|
|
return `${dateStr}-${timeFrom}${timeTo}`;
|
|
}
|