import { test, expect } from './fixtures'; test('stop search, route filter and city-scoped favorites survive reload', async ({ page }, info) => { const errors: string[] = []; page.on('pageerror', error => errors.push(error.message)); await page.goto('/stops?demo=1'); await expect(page.getByRole('combobox', { name: 'Ваш город' })).toHaveValue('1'); await page.getByRole('searchbox', { name: 'Поиск остановки' }).fill('Площадь'); await expect(page.locator('.stopItem')).toHaveCount(1); await page.locator('.stopItem').first().click(); await page.getByRole('button', { name: 'Открыть расписание', exact: true }).click(); await expect(page.locator('.departureRow')).toHaveCount(8); const directions = page.getByRole('group', { name: 'Направления', exact: true }); await expect(directions.getByRole('button')).toHaveCount(2); await directions.getByRole('button', { name: 'В сторону центра', exact: true }).click(); await expect(page).toHaveURL(/stop=102/); await expect(directions.getByRole('button', { name: 'В сторону центра', exact: true })).toHaveAttribute('aria-pressed', 'true'); await expect(page.locator('.departureRow')).toHaveCount(8); await directions.getByRole('button', { name: 'В сторону автовокзала', exact: true }).click(); await expect(page).toHaveURL(/stop=101/); await page.getByRole('button', { name: 'Маршрут 4', exact: true }).click(); await expect(page.locator('.departureRow')).toHaveCount(2); await page.getByRole('button', { name: 'Все маршруты', exact: true }).click(); await page.getByRole('button', { name: 'Добавить остановку в избранное', exact: true }).click(); await expect(page.getByRole('button', { name: 'Убрать остановку из избранного', exact: true })).toBeVisible(); if (info.project.name === 'mobile') { const mobileFavorite = page.getByRole('navigation', { name: 'Навигация на телефоне' }).getByRole('link', { name: /Избранное/ }); await expect(mobileFavorite.locator('.mobileBadge')).toHaveText('1'); await expect(mobileFavorite).toHaveText('1Избранное'); const badge = await mobileFavorite.locator('.mobileBadge').boundingBox(); const icon = await mobileFavorite.locator('.navIcon').boundingBox(); expect(badge!.y).toBeLessThan(icon!.y); expect(badge!.x + badge!.width).toBeGreaterThan(icon!.x + icon!.width); } await page.getByRole('button', { name: 'Закрыть уведомление', exact: true }).click(); await page.screenshot({ path: `test-results/ridebus-${info.project.name}.png`, fullPage: true, animations: 'disabled' }); await page.getByRole('navigation', { name: info.project.name === 'mobile' ? 'Навигация на телефоне' : 'Основная навигация', exact: true }).getByRole('link', { name: /Избранное/ }).click(); await expect(page.locator('.favoriteCard')).toHaveCount(1); await expect(page.locator('.favoriteDepartures li')).toHaveCount(3); await page.reload(); await expect(page.locator('.favoriteCard')).toHaveCount(1); await page.getByRole('link', { name: 'Посмотреть расписание', exact: true }).click(); await expect(page.locator('#board-title')).toHaveText('Площадь Свободы'); await expect(page.getByRole('group', { name: 'Направления' }).getByRole('button', { name: 'В сторону автовокзала', exact: true })).toHaveAttribute('aria-pressed', 'true'); await page.getByRole('combobox', { name: 'Ваш город' }).selectOption('2'); await expect(page.locator('#board-title')).toHaveText('Ваша остановка'); await page.getByRole('button', { name: 'Выбрать остановку', exact: true }).click(); const picker = page.getByRole('dialog', { name: 'Выбрать остановку', exact: true }); await picker.getByRole('searchbox', { name: 'Ваша остановка' }).fill('Площадь'); await picker.getByRole('button', { name: /Площадь Строителей/ }).click(); await expect(page.locator('#board-title')).toHaveText('Площадь Строителей'); await expect(page.getByRole('button', { name: 'Добавить остановку в избранное', exact: true })).toBeVisible(); expect(errors).toEqual([]); expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true); }); test('every grouped stop is visible without an eighty-stop cap', async ({ page }) => { await page.route('**/api/data?**', route => { const resource = new URL(route.request().url()).searchParams.get('resource'); const data = resource === 'cities' ? [{ id: 1, name: 'Полоцк', slug: 'polotsk' }] : resource === 'routes' ? [] : Array.from({ length: 115 }, (_, i) => ({ id: i + 1, name: `Остановка ${String(i + 1).padStart(3, '0')}`, direction: '', routesCount: 1 })); return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(data) }); }); await page.goto('/stops'); await page.getByRole('button', { name: 'Список', exact: true }).click(); await expect(page.locator('.stopItem')).toHaveCount(115); await expect(page.locator('.explorerListHeading')).toContainText('115 остановок'); await expect(page.locator('.maplibregl-canvas')).toHaveCount(0); await expect(page.getByRole('button', { name: 'Карта', exact: true })).toBeDisabled(); await page.locator('.stopItem').last().scrollIntoViewIfNeeded(); await expect(page.locator('.stopItem').last()).toBeVisible(); await expect(page.locator('main')).not.toContainText('Показаны первые'); }); test('text is readable on narrow screens and contains no technical API wording', async ({ page }) => { await page.setViewportSize({ width: 320, height: 780 }); await page.goto('/?demo=1&city=1&stop=101'); await expect(page.locator('.departureRow')).toHaveCount(8); const tiny = await page.locator('main, footer, .mobileNav').evaluateAll(roots => roots.flatMap(root => [...root.querySelectorAll('p, label, strong, button, small, span')]).filter(element => { const visible = element.getClientRects().length > 0; return visible && element.textContent?.trim() && Number.parseFloat(getComputedStyle(element).fontSize) < 13 && !element.classList.contains('mobileBadge'); }).map(element => ({ text: element.textContent, size: getComputedStyle(element).fontSize }))); expect(tiny).toEqual([]); expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true); await expect(page.locator('main')).not.toContainText('API'); await expect(page.locator('body')).not.toContainText('МЕНЬШЕ ОЖИДАНИЯ'); await expect(page.getByRole('contentinfo')).toContainText('Расписание в вашем кармане'); }); test('installed service worker serves shell, favorites and saved departures offline', async ({ page, context }) => { await page.goto('/?demo=1&city=1&stop=101'); await expect(page.locator('.departureRow')).toHaveCount(8); await page.evaluate(async () => { await navigator.serviceWorker.ready; if (!navigator.serviceWorker.controller) await new Promise(resolve => navigator.serviceWorker.addEventListener('controllerchange', () => resolve(), { once: true })); }); await context.setOffline(true); await page.reload(); await expect(page.locator('.offlineBanner')).toBeVisible(); await expect(page.locator('.offlineBanner')).toBeVisible(); await expect(page.locator('.savedNotice')).toHaveCount(0); await expect(page.locator('.departureRow')).toHaveCount(8); await page.getByRole('button', { name: 'Добавить остановку в избранное', exact: true }).click(); await expect(page.getByRole('button', { name: 'Убрать остановку из избранного', exact: true })).toBeVisible(); await context.setOffline(false); }); test('API failure is explicit and does not silently substitute demo data', async ({ page }) => { await page.route('**/api/data?**', route => route.fulfill({ status: 503, contentType: 'application/json', body: JSON.stringify({ error: 'api_not_configured' }) })); await page.goto('/'); await expect(page.getByRole('heading', { name: 'Подключаем расписание' })).toBeVisible(); await expect(page.locator('.demoBanner')).toHaveCount(0); await expect(page.locator('.departureRow')).toHaveCount(0); await expect(page.getByRole('button', { name: /Посмотреть пример/ })).toHaveCount(0); await expect(page.locator('.stopsList[aria-label="Остановки"]')).not.toContainText('на примере'); await page.goto('/?demo=1'); await expect(page.locator('.demoBanner')).toBeVisible(); }); test('a stale timetable loses expired departures across the date boundary', async ({ page, context }) => { await page.clock.install({ time: new Date('2026-10-06T20:59:00Z') }); await page.goto('/?demo=1&city=1&stop=101'); await expect(page.locator('.departureRow')).toHaveCount(8); await context.setOffline(true); await page.clock.fastForward('02:00:00'); await expect(page.locator('.departureRow')).toHaveCount(0); await expect(page.getByText('Ближайшие отправления закончились.', { exact: false })).toBeVisible(); }); test('full timetable keeps past trips, switches date and direction, and restores its URL', async ({ page }, info) => { await page.clock.install({ time: new Date('2026-10-07T09:00:00Z') }); await page.goto('/?demo=1&city=1&stop=101'); await page.getByRole('button', { name: 'На дату', exact: true }).click(); await expect(page.locator('.timetableSummary')).toContainText('32 рейса'); await expect(page.locator('.pastDeparture')).not.toHaveCount(0); await expect(page.locator('.hourDeparture')).toHaveCount(32); await page.getByLabel('Дата поездки', { exact: true }).fill('2026-10-08'); await expect(page.locator('.timetableSummary')).toContainText('8 октября'); await expect(page.locator('.hourDeparture')).toHaveCount(32); await page.getByRole('button', { name: 'Маршрут 4', exact: true }).click(); await expect(page.locator('.timetableSummary')).toContainText('8 рейсов'); await expect(page.locator('.hourDeparture')).toHaveCount(8); await expect(page.locator('.hourRoute')).toHaveCount(0); await page.getByRole('group', { name: 'Направления', exact: true }).getByRole('button', { name: 'В сторону центра', exact: true }).click(); await expect(page).toHaveURL(/stop=102/); await expect(page).toHaveURL(/date=2026-10-08/); await page.reload(); await expect(page.getByLabel('Дата поездки', { exact: true })).toHaveValue('2026-10-08'); await expect(page.getByRole('button', { name: 'На дату', exact: true })).toHaveAttribute('aria-pressed', 'true'); await expect(page.locator('.timetableSummary')).toContainText('32 рейса'); await page.screenshot({ path: `test-results/timetable-${info.project.name}.png`, fullPage: true, animations: 'disabled' }); await page.getByRole('button', { name: 'Ближайшие', exact: true }).click(); await expect(page.locator('.timetableSummary')).toHaveCount(0); await expect(page.locator('.departureRow')).toHaveCount(8); }); test('saved full timetable works offline and does not substitute another date', async ({ page, context }) => { await page.goto('/?demo=1&city=1&stop=101&view=day&date=2026-10-07'); await expect(page.locator('.timetableSummary')).toContainText('32 рейса'); await page.evaluate(async () => { await navigator.serviceWorker.ready; if (!navigator.serviceWorker.controller) await new Promise(resolve => navigator.serviceWorker.addEventListener('controllerchange', () => resolve(), { once: true })); }); await context.setOffline(true); await page.reload(); await expect(page.locator('.offlineBanner')).toBeVisible(); await expect(page.locator('.savedNotice')).toHaveCount(0); await expect(page.locator('.hourDeparture')).toHaveCount(32); await page.getByLabel('Дата поездки', { exact: true }).fill('2026-10-09'); await expect(page.getByRole('heading', { name: 'Нет сохранённого расписания', exact: true })).toBeVisible(); await expect(page.locator('.departureRow')).toHaveCount(0); }); test('hours and minutes are consistent in departures and favorites, which expire offline', async ({ page, context }, info) => { const time = Date.parse('2026-10-07T09:00:00Z'); await page.clock.install({ time: new Date(time) }); await page.route('**/api/data?**', route => { const resource = new URL(route.request().url()).searchParams.get('resource'); const data = resource === 'cities' ? [{ id: 1, name: 'Полоцк', slug: 'polotsk' }] : resource === 'stops' ? [{ id: 101, name: 'Вокзал', direction: 'В центр', routesCount: 1 }] : resource === 'alerts' ? [] : { routes: [{ id: 1, number: '112С', name: 'В центр' }], partial: false, updatedAt: new Date(time).toISOString(), departures: [{ routeId: 1, routeNumber: '112С', direction: 'В центр', scheduledTime: '14:01', scheduledAt: new Date(time + 121 * 60000).toISOString() }] }; return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(data) }); }); await page.goto('/?city=1&stop=101'); await expect(page.locator('.departureTime')).toContainText('через 2 ч 1 мин'); await page.getByRole('button', { name: 'Добавить остановку в избранное', exact: true }).click(); await expect(page.getByRole('button', { name: 'Убрать остановку из избранного', exact: true })).toBeVisible(); await page.getByRole('navigation', { name: info.project.name === 'mobile' ? 'Навигация на телефоне' : 'Основная навигация' }).getByRole('link', { name: /Избранное/ }).click(); await expect(page.locator('.favoriteDepartures')).toContainText('через 2 ч 1 мин'); await page.setViewportSize({ width: 320, height: 780 }); expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true); await page.screenshot({ path: `test-results/favorites-${info.project.name}.png`, fullPage: true, animations: 'disabled' }); await context.setOffline(true); await page.clock.fastForward('03:00:00'); await expect(page.locator('.favoriteDepartures li')).toHaveCount(0); await expect(page.locator('.favoriteStatus')).toContainText('Расписание давно не обновлялось'); }); test('about page is accessible from the footer and fits a narrow screen', async ({ page }, info) => { await page.setViewportSize({ width: 320, height: 780 }); const errors: string[] = []; page.on('pageerror', error => errors.push(error.message)); await page.goto('/?demo=1'); await page.getByRole('contentinfo').getByRole('link', { name: 'О проекте', exact: true }).click(); await expect(page).toHaveURL('/about?demo=1'); await expect(page.getByRole('heading', { name: 'Привет, я Захар.', exact: true })).toBeVisible(); await expect(page.locator('.aboutSupport')).toContainText('хостинг, домен'); await expect(page.locator('.supportNote')).toContainText('Ссылку для поддержки добавлю чуть позже'); expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true); await page.screenshot({ path: `test-results/about-${info.project.name}.png`, fullPage: true, animations: 'disabled' }); await page.getByRole('link', { name: 'К расписанию', exact: true }).click(); await expect(page).toHaveURL('/?demo=1'); expect(errors).toEqual([]); });