Files
ridebus-pwa/e2e/settings.spec.ts
Zakhar Timoshenko 81b2c7d675 feat: implement theme settings with light and dark modes
- Added new icons for settings and theme options in Icons.tsx.
- Enhanced local storage functions to return success status in storage.ts.
- Updated styles to support new theme settings in styles.scss.
- Created a new settings page with theme selection options in Settings.tsx.
- Implemented theme management logic in theme.ts, allowing users to choose between system, light, and dark themes.
- Added Playwright tests for theme settings functionality in settings.spec.ts.
- Introduced new SCSS file for settings styles in _settings.scss.
2026-10-07 03:33:50 +03:00

116 lines
7.5 KiB
TypeScript
Raw 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 '@playwright/test';
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('link', { name: 'В избранное', exact: true }).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([]);
});