- 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.
102 lines
6.5 KiB
TypeScript
102 lines
6.5 KiB
TypeScript
import { test, expect, type Page } from './fixtures';
|
|
|
|
async function offlineReady(page: Page) {
|
|
await page.evaluate(async () => {
|
|
await navigator.serviceWorker.ready;
|
|
if (!navigator.serviceWorker.controller) await new Promise<void>(resolve => navigator.serviceWorker.addEventListener('controllerchange', () => resolve(), { once: true }));
|
|
});
|
|
}
|
|
|
|
test('four sections fit phones and tablets and the home picker stays compact', async ({ page }) => {
|
|
await page.goto('/?demo=1&city=1&stop=101');
|
|
await expect(page.locator('.departureRow')).toHaveCount(8);
|
|
await expect(page.locator('.compactPicker .stopItem')).toHaveCount(0);
|
|
for (const width of [320, 390, 760, 761, 820, 1050, 1120, 1440]) {
|
|
await page.setViewportSize({ width, height: 900 });
|
|
const nav = page.getByRole('navigation', { name: width <= 760 ? 'Навигация на телефоне' : 'Основная навигация' });
|
|
await expect(nav.getByRole('link')).toHaveCount(4);
|
|
for (const name of ['Главная', 'Остановки', 'Избранное', 'Настройки']) await expect(nav.getByRole('link', { name: new RegExp(`^${name}`) })).toBeVisible();
|
|
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
|
|
expect((await page.locator('.compactPicker').boundingBox())!.height).toBeLessThan(300);
|
|
}
|
|
await page.setViewportSize({ width: 390, height: 844 });
|
|
const nav = page.getByRole('navigation', { name: 'Навигация на телефоне' });
|
|
for (const [name, path] of [['Остановки', '/stops'], ['Избранное', '/favorites'], ['Настройки', '/settings'], ['Главная', '/']]) {
|
|
await nav.getByRole('link', { name: new RegExp(`^${name}`) }).click();
|
|
await expect.poll(() => new URL(page.url()).pathname).toBe(path);
|
|
await expect(nav.getByRole('link', { name: new RegExp(`^${name}`) })).toHaveAttribute('aria-current', 'page');
|
|
}
|
|
});
|
|
|
|
test('choosing a stop from the home dialog closes it and scrolls to departures on mobile', async ({ page }) => {
|
|
await page.setViewportSize({ width: 390, height: 844 });
|
|
await page.addInitScript(() => {
|
|
const original = HTMLElement.prototype.scrollIntoView;
|
|
(window as any).stopScrolls = [];
|
|
HTMLElement.prototype.scrollIntoView = function(options) {
|
|
if (this.id === 'departures') (window as any).stopScrolls.push(options);
|
|
original.call(this, options);
|
|
};
|
|
});
|
|
await page.goto('/?demo=1&city=1');
|
|
const trigger = page.getByRole('button', { name: 'Выбрать остановку', exact: true });
|
|
await trigger.click();
|
|
const dialog = page.getByRole('dialog', { name: 'Выбрать остановку', exact: true });
|
|
await expect(dialog).toBeVisible();
|
|
// Opening the chooser does not focus search or summon the phone keyboard.
|
|
await expect(dialog.getByRole('searchbox')).not.toBeFocused();
|
|
await dialog.getByRole('searchbox').fill('Площадь');
|
|
await dialog.locator('.stopItem').click();
|
|
await expect(dialog).toHaveCount(0);
|
|
await expect(page.locator('#board-title')).toHaveText('Площадь Свободы');
|
|
await expect(page.locator('#departures')).toBeFocused();
|
|
await expect.poll(() => page.evaluate(() => (window as any).stopScrolls.at(-1)?.behavior)).toBe('smooth');
|
|
await expect.poll(() => page.locator('#board-title').evaluate(element => element.getBoundingClientRect().top)).toBeLessThan(180);
|
|
await expect(trigger).toContainText('Площадь Свободы');
|
|
await page.emulateMedia({ reducedMotion: 'reduce' });
|
|
await trigger.click();
|
|
await dialog.getByRole('searchbox').fill('Площадь');
|
|
await dialog.locator('.stopItem').click();
|
|
await expect.poll(() => page.evaluate(() => (window as any).stopScrolls.length)).toBe(2);
|
|
await expect.poll(() => page.evaluate(() => (window as any).stopScrolls.at(-1)?.behavior)).toBe('auto');
|
|
});
|
|
|
|
test('catalog opens a stop card and its schedule on the home page while retaining the date', async ({ page }) => {
|
|
await page.setViewportSize({ width: 320, height: 780 });
|
|
await page.goto('/stops?demo=1&city=1&view=day&date=2026-10-08');
|
|
await page.getByRole('searchbox').fill('Площадь');
|
|
await page.locator('.stopItem').click();
|
|
await expect(page.locator('.stopPassport')).toBeFocused();
|
|
await expect(page.locator('#departures')).toHaveCount(0);
|
|
await page.getByRole('button', { name: 'Открыть расписание', exact: true }).click();
|
|
await expect(page.locator('#departures')).toBeFocused();
|
|
await expect(page).toHaveURL(/\/\?.*stop=101.*date=2026-10-08/);
|
|
await expect(page.getByLabel('Дата поездки', { exact: true })).toHaveValue('2026-10-08');
|
|
await expect(page.locator('.hourDeparture')).toHaveCount(32);
|
|
await expect.poll(() => page.locator('#board-title').evaluate(element => element.getBoundingClientRect().top)).toBeLessThan(180);
|
|
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
|
|
});
|
|
|
|
test('disconnecting now shows a single status and older departures receive a warning later', async ({ page, context }) => {
|
|
await page.clock.install({ time: new Date('2026-10-07T09:00:00Z') });
|
|
await page.goto('/?demo=1&city=1&stop=101');
|
|
await expect(page.locator('.departureRow')).toHaveCount(8);
|
|
await page.getByRole('button', { name: 'Добавить остановку в избранное', exact: true }).click();
|
|
await page.getByRole('button', { name: 'Сохранить поездку', exact: true }).click();
|
|
await page.getByRole('dialog').getByRole('button', { name: 'Сохранить поездку', exact: true }).click();
|
|
await expect(page.locator('.quickTripCard')).toHaveCount(1);
|
|
await offlineReady(page);
|
|
await context.setOffline(true);
|
|
await expect(page.locator('.offlineBanner')).toHaveCount(1);
|
|
await expect(page.locator('.savedNotice, .tripSaved, .alertSaved')).toHaveCount(0);
|
|
await expect(page.locator('main')).not.toContainText('устарев');
|
|
await page.reload();
|
|
await expect(page.locator('.departureRow')).toHaveCount(8);
|
|
await expect(page.locator('.savedNotice, .tripSaved, .alertSaved')).toHaveCount(0);
|
|
await page.clock.fastForward('00:31:00');
|
|
await expect(page.locator('.savedNotice')).toContainText('Расписание давно не обновлялось');
|
|
await expect(page.locator('.tripSaved')).toContainText('Расписание давно не обновлялось');
|
|
await page.getByRole('navigation').getByRole('link', { name: /Избранное/ }).click();
|
|
await expect(page.locator('.favoriteStatus')).toContainText('Расписание давно не обновлялось');
|
|
});
|