- 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.
149 lines
11 KiB
TypeScript
149 lines
11 KiB
TypeScript
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' });
|
||
});
|