Files
ridebus-pwa/e2e/stops.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

149 lines
11 KiB
TypeScript
Raw Permalink 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, type Page } from './fixtures';
async function localBasemap(page: Page) {
await page.route('https://tiles.openfreemap.org/**', route => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ version: 8, sources: {}, layers: [{ id: 'base', type: 'background', paint: { 'background-color': '#f3f3fa' } }] }) }));
}
test('stop card distinguishes platforms, favorites the selected direction and opens its schedule', async ({ page }) => {
await localBasemap(page);
await page.setViewportSize({ width: 390, height: 844 });
await page.goto('/stops?demo=1&city=1');
await expect(page.locator('#departures')).toHaveCount(0);
await page.getByRole('searchbox', { name: 'Поиск остановки' }).fill('Площадь');
await expect(page.locator('.stopItem')).toHaveCount(1);
await page.locator('.stopItem').click();
await expect(page.locator('#stop-passport-title')).toHaveText('Площадь Свободы');
const directions = page.getByRole('group', { name: 'Направления остановки' });
await expect(directions.getByRole('button')).toHaveCount(2);
await directions.getByRole('button', { name: 'В сторону центра', exact: true }).click();
await expect(page).toHaveURL(/stop=102/);
await expect(page.locator('.passportRoute')).toHaveCount(3);
await expect(page.getByRole('button', { name: 'Как добраться пешком', exact: true })).toBeDisabled();
await page.getByRole('button', { name: 'Добавить остановку в избранное', exact: true }).click();
await expect(page.getByRole('button', { name: 'Удалить остановку из избранного', exact: true })).toHaveAttribute('aria-pressed', 'true');
await page.getByRole('button', { name: 'Открыть расписание', exact: true }).click();
await expect.poll(() => new URL(page.url()).pathname).toBe('/');
await expect(page.locator('#departures')).toBeFocused();
await expect(page.locator('.departureRow')).toHaveCount(8);
await expect(page.getByRole('group', { name: 'Направления', exact: true }).getByRole('button', { name: 'В сторону центра', exact: true })).toHaveAttribute('aria-pressed', 'true');
await expect(page.getByRole('button', { name: 'Убрать остановку из избранного', exact: true })).toBeVisible();
});
test('map points are clickable and the map never requests departures before opening a schedule', async ({ page }) => {
await localBasemap(page);
const errors: string[] = [], resources: string[] = [];
page.on('pageerror', error => errors.push(error.message));
const stop = { id: 91, name: 'Место посадки', direction: 'В центр', routesCount: 1, latitude: 55.49, longitude: 28.78 };
await page.route('**/api/data?**', route => {
const resource = new URL(route.request().url()).searchParams.get('resource')!; resources.push(resource);
const data = resource === 'cities' ? [{ id: 1, name: 'Полоцк', slug: 'polotsk', latitude: 55.49, longitude: 28.78 }]
: resource === 'stops' ? [stop] : resource === 'routes' ? [] : { ...stop, routes: [{ id: 1, number: '112С', name: 'Вокзал → Центр' }] };
return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(data) });
});
await page.goto('/stops?city=1');
await expect(page.locator('.stopsMapCanvas')).toHaveAttribute('data-ready', 'true');
await expect(page.locator('.explorerMapMode .explorerCatalog')).toHaveCount(0);
const canvas = page.locator('.maplibregl-canvas');
const bounds = (await canvas.boundingBox())!;
expect(bounds.height).toBeGreaterThan(350);
await canvas.click({ position: { x: bounds.width / 2, y: bounds.height / 2 } });
await expect(page.locator('#stop-passport-title')).toHaveText('Место посадки');
await expect(page.locator('.passportRoute')).toHaveText('112С');
expect(resources).toContain('stop'); expect(resources).not.toContain('departures'); expect(resources).not.toContain('timetable');
expect(errors).toEqual([]);
});
test('nearby discovery uses a single list, chooses the nearest platform and keeps location ephemeral', async ({ page, context }) => {
await localBasemap(page);
await context.grantPermissions(['geolocation']);
await context.setGeolocation({ latitude: 55.48495, longitude: 28.7686, accuracy: 20 });
await page.addInitScript(() => {
const original = navigator.geolocation.getCurrentPosition.bind(navigator.geolocation);
(window as any).locationRequests = 0;
navigator.geolocation.getCurrentPosition = (...args) => { (window as any).locationRequests++; return original(...args); };
});
await page.goto('/stops?demo=1&city=1');
await page.getByRole('button', { name: 'Список', exact: true }).click();
await expect(page.locator('.stopItem')).toHaveCount(5);
expect(await page.evaluate(() => (window as any).locationRequests)).toBe(0);
await page.getByRole('button', { name: 'Рядом со мной', exact: true }).click();
await expect(page.locator('.explorerHint')).toContainText('расстояния по прямой');
await expect(page.locator('.stopItem').first()).toContainText('Площадь Свободы');
await expect(page.locator('.stopItem').first()).toContainText('м');
await page.locator('.stopItem').first().click();
await expect(page).toHaveURL(/stop=101/);
expect(await page.evaluate(() => (window as any).locationRequests)).toBe(1);
const persisted = await page.evaluate(() => JSON.stringify({ ...localStorage }));
expect(persisted).not.toContain('55.48495'); expect(persisted).not.toContain('28.7686');
await page.getByRole('button', { name: 'Закрыть карточку остановки', exact: true }).click();
await page.getByRole('button', { name: 'Все остановки', exact: true }).click();
await expect(page.locator('.stopItem')).toHaveCount(5);
await page.getByRole('combobox', { name: 'Ваш город' }).selectOption('2');
await expect(page.locator('.stopDistanceHint')).toHaveCount(0);
await expect(page.locator('.explorerNearby')).toHaveAttribute('aria-pressed', 'false');
});
test('stop catalog and visited cards remain usable offline without importing the map', async ({ page, context }) => {
await localBasemap(page);
await page.goto('/stops?demo=1&city=1&stop=101');
if ((await page.viewportSize())!.width <= 760) { await page.getByRole('button', { name: 'Список', exact: true }).click(); await page.locator('.stopItem').filter({ hasText: 'Площадь Свободы' }).click(); }
await expect(page.locator('.passportRoute')).toHaveCount(4);
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')).toHaveCount(1);
await expect(page.locator('.maplibregl-canvas')).toHaveCount(0);
await expect(page.locator('.stopItem')).toHaveCount(5);
if ((await page.viewportSize())!.width > 760) await expect(page.locator('.passportRoute')).toHaveCount(4);
await page.getByRole('searchbox').fill('Площадь');
await page.locator('.stopItem').click();
await expect(page.locator('.passportRoute')).toHaveCount(4);
await expect(page.locator('#stop-passport-title')).toHaveText('Площадь Свободы');
await expect(page.locator('main')).not.toContainText('устарев');
});
test('a basemap failure leaves search, stop information and the list usable', async ({ page }) => {
const errors: string[] = []; page.on('pageerror', error => errors.push(error.message));
await page.route('https://tiles.openfreemap.org/**', route => route.abort());
await page.goto('/stops?demo=1&city=1');
await expect(page.locator('.mapProblem')).toContainText('Не удалось загрузить подложку карты');
await page.getByRole('button', { name: 'К списку', exact: true }).click();
await expect(page.locator('.maplibregl-canvas')).toHaveCount(0);
await page.getByRole('searchbox').fill('Больница');
await page.locator('.stopItem').click();
await expect(page.locator('#stop-passport-title')).toHaveText('Больница');
await expect(page.locator('.passportRoute')).toHaveCount(3);
expect(errors).toEqual([]);
});
test('map and list fit narrow phones and respond to the app theme', async ({ page }, info) => {
const styles: string[] = [];
await page.route('https://tiles.openfreemap.org/**', route => {
styles.push(route.request().url());
return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ version: 8, sources: {}, layers: [{ id: 'base', type: 'background', paint: { 'background-color': '#f3f3fa' } }] }) });
});
await page.goto('/stops?demo=1&city=1&stop=101');
await expect(page.locator('.stopsMapCanvas')).toHaveAttribute('data-ready', 'true');
for (const width of [320, 390, 760, 761, 1050, 1440]) {
await page.setViewportSize({ width, height: 900 });
if (width <= 760 && await page.locator('.stopPassport').count()) await expect(page.locator('.stopPassportDialog')).toBeVisible();
if (await page.locator('.stopPassportDialog').count()) await page.getByRole('button', { name: 'Закрыть карточку остановки', exact: true }).click();
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
const map = (await page.locator('.stopsMapCanvas').boundingBox())!;
expect(map.height).toBeGreaterThan(350);
await page.getByRole('button', { name: 'Список', exact: true }).click();
if (width > 760) await expect(page.locator('.stopPassport')).toBeVisible();
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
await page.getByRole('button', { name: 'Карта', exact: true }).click();
await expect(page.locator('.stopsMapCanvas')).toHaveAttribute('data-ready', 'true');
}
await page.getByRole('navigation', { name: 'Основная навигация' }).getByRole('link', { name: 'Настройки' }).click();
await page.getByRole('radio', { name: /^Тёмная/ }).check();
await page.getByRole('navigation', { name: 'Основная навигация' }).getByRole('link', { name: 'Остановки' }).click();
await expect.poll(() => styles.some(url => url.endsWith('/dark'))).toBe(true);
await expect(page.locator('.stopPassport')).toHaveCSS('background-color', 'rgb(28, 35, 48)');
await page.screenshot({ path: `test-results/stops-dark-${info.project.name}.png`, fullPage: true, animations: 'disabled' });
});