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(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' }); });