Files
ridebus-pwa/e2e/settings.spec.ts
Zakhar Timoshenko 1492f179eb feat: add new components for departures, discovery, and navigation
- Implemented SCSS styles for compact board in departures.
- Created styles for feature discovery modal and its elements.
- Added navigation styles for desktop and mobile views.
- Developed FeatureDiscovery component with guided steps for user onboarding.
- Introduced freshness utility to manage data freshness based on timestamps.
- Added tests for freshness utility to ensure correct behavior across various scenarios.
2026-10-07 21:51:05 +03:00

116 lines
7.6 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 } from './fixtures';
test('theme settings are reachable from navigation and preserve timetable and favorites', async ({ page }, info) => {
await page.emulateMedia({ colorScheme: 'light' });
await page.goto('/?demo=1&city=1&stop=101&view=day&date=2026-10-07&route=1');
await expect(page.locator('.hourDeparture')).toHaveCount(8);
await page.getByRole('button', { name: 'Добавить остановку в избранное', exact: true }).click();
await page.getByRole('button', { name: 'Закрыть уведомление', exact: true }).click();
const nav = page.getByRole('navigation', { name: info.project.name === 'mobile' ? 'Навигация на телефоне' : 'Основная навигация' });
await nav.getByRole('link', { name: 'Настройки', exact: true }).click();
await expect(page.getByRole('heading', { name: 'Настройки', exact: true })).toBeVisible();
await expect(nav.getByRole('link', { name: 'Настройки', exact: true })).toHaveAttribute('aria-current', 'page');
await expect(page.getByRole('radio', { name: /^Как на устройстве/ })).toBeChecked();
await page.getByRole('radio', { name: /^Тёмная/ }).check();
await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark');
await expect(page.locator('body')).toHaveCSS('background-color', 'rgb(20, 24, 32)');
await expect(page.locator('meta[name="theme-color"]')).toHaveAttribute('content', '#202735');
await page.reload();
await expect(page.getByRole('radio', { name: /^Тёмная/ })).toBeChecked();
await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark');
await page.screenshot({ path: `test-results/settings-dark-${info.project.name}.png`, fullPage: true, animations: 'disabled' });
for (const width of [320, 760, 761, 820, 1050, 1440]) {
await page.setViewportSize({ width, height: 900 });
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
}
await page.getByRole('link', { name: 'К расписанию', exact: true }).click();
await expect(page).toHaveURL(/city=1&stop=101&view=day&date=2026-10-07&route=1/);
await expect(page.locator('.hourDeparture')).toHaveCount(8);
await expect(page.getByRole('button', { name: 'Убрать остановку из избранного', exact: true })).toBeVisible();
await expect(page.locator('.boardCard')).toHaveCSS('background-color', 'rgb(28, 35, 48)');
await expect(page.locator('.citySelect')).toHaveCSS('background-color', 'rgb(28, 35, 48)');
await page.screenshot({ path: `test-results/schedule-dark-${info.project.name}.png`, fullPage: true, animations: 'disabled' });
await page.getByRole('navigation', { name: 'Основная навигация' }).getByRole('link', { name: /Избранное/ }).click();
await expect(page.locator('.favoriteCard')).toHaveCount(1);
await expect(page.locator('.favoriteRoute').first()).toHaveCSS('background-color', 'rgb(28, 35, 48)');
});
test('system appearance follows live changes while explicit themes override it and support keyboard controls', async ({ page }) => {
await page.emulateMedia({ colorScheme: 'dark' });
let requests = 0;
await page.route('**/api/data?**', route => { requests++; return route.abort(); });
await page.goto('/settings');
await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark');
await expect(page.locator('.themeStatus')).toContainText('тёмная тема устройства');
await page.emulateMedia({ colorScheme: 'light' });
await expect(page.locator('html')).toHaveAttribute('data-theme', 'light');
await expect(page.locator('.themeStatus')).toContainText('светлая тема устройства');
const light = page.getByRole('radio', { name: /^Светлая/ });
await light.check();
await page.emulateMedia({ colorScheme: 'dark' });
await expect(page.locator('html')).toHaveAttribute('data-theme', 'light');
await light.focus(); await page.keyboard.press('ArrowRight');
await expect(page.getByRole('radio', { name: /^Тёмная/ })).toBeChecked();
await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark');
await page.keyboard.press('ArrowRight');
await expect(page.getByRole('radio', { name: /^Как на устройстве/ })).toBeChecked();
await page.emulateMedia({ colorScheme: 'light' });
await expect(page.locator('html')).toHaveAttribute('data-theme', 'light');
expect(requests).toBe(0);
});
test('theme changes synchronize between tabs and settings remain available after an offline reload', async ({ page, context }) => {
await page.goto('/settings');
await page.evaluate(async () => {
await navigator.serviceWorker.ready;
if (!navigator.serviceWorker.controller) await new Promise<void>(resolve => navigator.serviceWorker.addEventListener('controllerchange', () => resolve(), { once: true }));
});
const other = await context.newPage();
await other.goto('/settings');
await other.getByRole('radio', { name: /^Тёмная/ }).check();
await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark');
await expect(page.getByRole('radio', { name: /^Тёмная/ })).toBeChecked();
await context.setOffline(true);
await page.reload();
await expect(page.getByRole('heading', { name: 'Настройки', exact: true })).toBeVisible();
await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark');
await page.getByRole('radio', { name: /^Светлая/ }).check();
await expect(other.locator('html')).toHaveAttribute('data-theme', 'light');
await page.reload();
await expect(page.getByRole('radio', { name: /^Светлая/ })).toBeChecked();
await other.close();
});
test('the saved theme is applied before the application bundle executes', async ({ page }) => {
await page.emulateMedia({ colorScheme: 'light' });
await page.addInitScript(() => localStorage.setItem('ridebus:theme', 'dark'));
const bundle = /\/assets\/index-[^/]+\.js$/;
await page.route(bundle, route => route.fulfill({ status: 200, contentType: 'application/javascript', body: 'export {};' }));
await page.goto('/settings');
await expect(page.locator('#root')).toBeEmpty();
await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark');
await expect(page.locator('html')).toHaveCSS('color-scheme', 'dark');
await expect(page.locator('body')).toHaveCSS('background-color', 'rgb(20, 24, 32)');
await expect(page.locator('meta[name="theme-color"]')).toHaveAttribute('content', '#202735');
await page.unroute(bundle);
await page.reload();
await expect(page.getByRole('radio', { name: /^Тёмная/ })).toBeChecked();
});
test('blocked preference storage does not prevent changing theme or viewing settings', async ({ page }) => {
const errors: string[] = []; page.on('pageerror', error => errors.push(error.message));
await page.emulateMedia({ colorScheme: 'light' });
await page.addInitScript(() => {
Storage.prototype.getItem = () => { throw new DOMException('Blocked', 'SecurityError'); };
Storage.prototype.setItem = () => { throw new DOMException('Blocked', 'SecurityError'); };
});
await page.goto('/settings');
await expect(page.locator('html')).toHaveAttribute('data-theme', 'light');
await page.getByRole('radio', { name: /^Тёмная/ }).check();
await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark');
await expect(page.locator('.themeStatus')).toContainText('браузер не разрешил сохранить выбор');
await page.reload();
await expect(page.getByRole('radio', { name: /^Как на устройстве/ })).toBeChecked();
expect(errors).toEqual([]);
});