Files
ridebus-pwa/e2e/app.spec.ts
Zakhar Timoshenko 6fe039cefa feat: add StopsMap and WalkingDirections components for enhanced map functionality
- Implemented StopsMap component to display bus stops on a map using MapLibre GL.
- Integrated walking directions feature in WalkingDirections component to calculate routes from user location to selected bus stop.
- Added geo utility functions for coordinate validation and distance calculation.
- Created route grouping and stop filtering logic in routeGroups module.
- Developed custom hooks for managing stop routes with React Query.
- Added tests for routing and stop functionalities to ensure reliability.
2026-10-08 00:18:10 +03:00

215 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<HTMLElement>('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<void>(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<void>(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([]);
});