Files
TREK/client/tests/unit/mobile/settings/MSettingsGeneral.test.tsx
T
Maurice 7ce31756a3 feat(settings): every date picker starts its week on the day you pick
Settings > General > Language & region gets "Week starts on" (Monday,
Sunday or Saturday), and Admin > User Defaults a default for everyone who
has not picked one. Monday stays the built-in value, so nothing changes
for anyone who leaves it alone.

Both calendar implementations follow it: CustomDatePicker, used by trip
dates, bookings, costs, to-dos and the admin school holidays on desktop
and phone, and the journey date picker, whose weekday headers were
hardcoded English and now speak the app language. The grid math and the
header row live in one helper (utils/calendarWeek.ts). The day names on
the options come from Intl, so only the label and a hint needed
translating. Vacay keeps its own per-plan week start.

The value is validated like the other regional settings: VALID_VALUES for
admin defaults, the display-settings MCP tool, and the managed key list.

Addresses discussion #2029.
2026-09-24 21:56:12 +02:00

234 lines
11 KiB
TypeScript

// FE-MOB-SET-001 onwards
import { describe, it, expect, vi, beforeEach } from 'vitest';
import userEvent from '@testing-library/user-event';
import { render, screen } from '../../../helpers/render';
import { resetAllStores, seedStore } from '../../../helpers/store';
import { buildSettings } from '../../../helpers/factories';
import { useSettingsStore } from '../../../../src/store/settingsStore';
import { ToastContainer } from '../../../../src/components/shared/Toast';
import MSettingsGeneral from '../../../../src/mobile/screens/settings/MSettingsGeneral';
describe('MSettingsGeneral', () => {
beforeEach(() => {
resetAllStores();
seedStore(useSettingsStore, {
settings: buildSettings({ language: 'en', temperature_unit: 'fahrenheit', default_currency: '' }),
});
});
it('FE-MOB-SET-001: renders the Language & region and Travel & map cards with the current values', () => {
render(<MSettingsGeneral />);
expect(screen.getByText('Language & region')).toBeInTheDocument();
expect(screen.getByText('Travel & map')).toBeInTheDocument();
expect(screen.getByText('English')).toBeInTheDocument();
expect(screen.getByText('Trip currency')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '°F Fahrenheit' })).toHaveAttribute('aria-pressed', 'true');
});
it('FE-MOB-SET-002: unit segments persist the preference via updateSetting', async () => {
const user = userEvent.setup();
const updateSetting = vi.fn().mockResolvedValue(undefined);
seedStore(useSettingsStore, {
settings: buildSettings({ language: 'en', temperature_unit: 'fahrenheit' }),
updateSetting,
});
render(<MSettingsGeneral />);
await user.click(screen.getByRole('button', { name: '°C Celsius' }));
expect(updateSetting).toHaveBeenCalledWith('temperature_unit', 'celsius');
});
it('FE-MOB-SET-003: the language picker sheet lists the real locales and saves the choice', async () => {
const user = userEvent.setup();
const updateSetting = vi.fn().mockResolvedValue(undefined);
seedStore(useSettingsStore, {
settings: buildSettings({ language: 'en' }),
updateSetting,
});
render(<MSettingsGeneral />);
await user.click(screen.getByRole('button', { name: /English/ }));
await user.click(await screen.findByRole('button', { name: 'Deutsch' }));
expect(updateSetting).toHaveBeenCalledWith('language', 'de');
});
it('FE-MOB-SET-004: a chosen display currency is shown with its symbol', () => {
seedStore(useSettingsStore, { settings: buildSettings({ language: 'en', default_currency: 'EUR' }) });
render(<MSettingsGeneral />);
expect(screen.getByRole('button', { name: 'EUR — €' })).toBeInTheDocument();
});
it('FE-MOB-SET-005: the currency sheet saves a pick and can fall back to the trip currency', async () => {
const user = userEvent.setup();
const updateSetting = vi.fn().mockResolvedValue(undefined);
seedStore(useSettingsStore, {
settings: buildSettings({ language: 'en', default_currency: '' }),
updateSetting,
});
render(<MSettingsGeneral />);
await user.click(screen.getByRole('button', { name: 'Trip currency' }));
await user.click(await screen.findByRole('button', { name: 'CHF — CHF' }));
expect(updateSetting).toHaveBeenCalledWith('default_currency', 'CHF');
});
it('FE-MOB-SET-006: distance and time-format segments persist their preference', async () => {
const user = userEvent.setup();
const updateSetting = vi.fn().mockResolvedValue(undefined);
seedStore(useSettingsStore, {
settings: buildSettings({ language: 'en', distance_unit: 'metric', time_format: '24h' }),
updateSetting,
});
render(<MSettingsGeneral />);
expect(screen.getByRole('button', { name: 'km Metric' })).toHaveAttribute('aria-pressed', 'true');
await user.click(screen.getByRole('button', { name: 'mi Imperial' }));
expect(updateSetting).toHaveBeenCalledWith('distance_unit', 'imperial');
await user.click(screen.getByRole('button', { name: '12h' }));
expect(updateSetting).toHaveBeenCalledWith('time_format', '12h');
});
it('FE-MOB-SET-007: unset unit preferences fall back to the metric/24h defaults', () => {
seedStore(useSettingsStore, {
settings: buildSettings({ language: 'en', temperature_unit: undefined, distance_unit: undefined, time_format: undefined }),
});
render(<MSettingsGeneral />);
expect(screen.getByRole('button', { name: '°C Celsius' })).toHaveAttribute('aria-pressed', 'true');
expect(screen.getByRole('button', { name: 'km Metric' })).toHaveAttribute('aria-pressed', 'true');
expect(screen.getByRole('button', { name: '24h' })).toHaveAttribute('aria-pressed', 'true');
});
it('FE-MOB-SET-008: the travel toggles reflect their opt-in / opt-out defaults', () => {
render(<MSettingsGeneral />);
expect(screen.getByRole('switch', { name: 'Booking route labels' })).toHaveAttribute('aria-checked', 'false');
expect(screen.getByRole('switch', { name: 'Explore places on the map' })).toHaveAttribute('aria-checked', 'true');
expect(screen.getByRole('switch', { name: 'Blur Booking Codes' })).toHaveAttribute('aria-checked', 'false');
expect(screen.getByRole('switch', { name: 'Optimize route from accommodation' })).toHaveAttribute('aria-checked', 'true');
});
it('FE-MOB-SET-009: flipping a travel toggle persists the new boolean', async () => {
const user = userEvent.setup();
const updateSetting = vi.fn().mockResolvedValue(undefined);
seedStore(useSettingsStore, {
settings: buildSettings({ language: 'en', map_booking_labels: false, map_poi_pill_enabled: true }),
updateSetting,
});
render(<MSettingsGeneral />);
await user.click(screen.getByRole('switch', { name: 'Booking route labels' }));
expect(updateSetting).toHaveBeenCalledWith('map_booking_labels', true);
await user.click(screen.getByRole('switch', { name: 'Explore places on the map' }));
expect(updateSetting).toHaveBeenCalledWith('map_poi_pill_enabled', false);
await user.click(screen.getByRole('switch', { name: 'Blur Booking Codes' }));
expect(updateSetting).toHaveBeenCalledWith('blur_booking_codes', true);
await user.click(screen.getByRole('switch', { name: 'Optimize route from accommodation' }));
expect(updateSetting).toHaveBeenCalledWith('optimize_from_accommodation', false);
});
it('FE-MOB-SET-010: a rejected save shows the server message as a toast', async () => {
const user = userEvent.setup();
seedStore(useSettingsStore, {
settings: buildSettings({ language: 'en' }),
updateSetting: vi.fn().mockRejectedValue(new Error('Error saving setting')),
});
render(<><ToastContainer /><MSettingsGeneral /></>);
await user.click(screen.getByRole('button', { name: '°F Fahrenheit' }));
expect(await screen.findByText('Error saving setting')).toBeInTheDocument();
});
it('FE-MOB-SET-011: a non-Error rejection falls back to the generic message', async () => {
const user = userEvent.setup();
seedStore(useSettingsStore, {
settings: buildSettings({ language: 'en' }),
updateSetting: vi.fn().mockRejectedValue('boom'),
});
render(<><ToastContainer /><MSettingsGeneral /></>);
await user.click(screen.getByRole('button', { name: '°F Fahrenheit' }));
expect(await screen.findByText('Error')).toBeInTheDocument();
});
it('FE-MOB-SET-012: the startup card defaults to the dashboard and hides the tab row', () => {
render(<MSettingsGeneral />);
expect(screen.getByText('Startup')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Dashboard' })).toHaveAttribute('aria-pressed', 'true');
expect(screen.queryByText('Start tab')).not.toBeInTheDocument();
});
it('FE-MOB-SET-013: picking the active trip persists it and reveals the tab row', async () => {
const user = userEvent.setup();
const updateSetting = vi.fn().mockResolvedValue(undefined);
seedStore(useSettingsStore, { settings: buildSettings({ language: 'en' }), updateSetting });
render(<MSettingsGeneral />);
await user.click(screen.getByRole('button', { name: 'Active trip' }));
expect(updateSetting).toHaveBeenCalledWith('start_page', 'active_trip');
});
it('FE-MOB-SET-014: the start-tab sheet saves the planner tab id behind the label', async () => {
const user = userEvent.setup();
const updateSetting = vi.fn().mockResolvedValue(undefined);
seedStore(useSettingsStore, {
settings: buildSettings({ language: 'en', start_page: 'active_trip', start_trip_tab: 'plan' }),
updateSetting,
});
render(<MSettingsGeneral />);
expect(screen.getByText('Start tab')).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: 'Plan' }));
await user.click(await screen.findByText('Costs'));
expect(updateSetting).toHaveBeenCalledWith('start_trip_tab', 'finanzplan');
});
// #2456: the account-wide default behind every trip's booking routes. Desktop has it
// under Settings -> Display; a phone had no way to reach it, so a booking route on the
// phone map only ever came from switching each booking on by hand.
it('FE-MOB-SET-015: the travel card carries the always-show-booking-routes default, off until set', async () => {
const user = userEvent.setup();
const updateSetting = vi.fn().mockResolvedValue(undefined);
seedStore(useSettingsStore, { settings: buildSettings({ language: 'en' }), updateSetting });
render(<MSettingsGeneral />);
const toggle = screen.getByRole('switch', { name: 'Always show booking routes' });
expect(toggle).toHaveAttribute('aria-checked', 'false');
await user.click(toggle);
expect(updateSetting).toHaveBeenCalledWith('map_always_show_routes', true);
});
it('FE-MOB-SET-016: the always-show-booking-routes toggle reflects a stored true and switches it off', async () => {
const user = userEvent.setup();
const updateSetting = vi.fn().mockResolvedValue(undefined);
seedStore(useSettingsStore, { settings: buildSettings({ language: 'en', map_always_show_routes: true }), updateSetting });
render(<MSettingsGeneral />);
const toggle = screen.getByRole('switch', { name: 'Always show booking routes' });
expect(toggle).toHaveAttribute('aria-checked', 'true');
await user.click(toggle);
expect(updateSetting).toHaveBeenCalledWith('map_always_show_routes', false);
});
});
describe('MSettingsGeneral week start (#2029)', () => {
it('FE-MOB-SET-017: the week-start segments show Monday when unset and save the day tapped', async () => {
const user = userEvent.setup();
const updateSetting = vi.fn().mockResolvedValue(undefined);
seedStore(useSettingsStore, { settings: buildSettings({ language: 'en', week_start: undefined }), updateSetting });
render(<MSettingsGeneral />);
expect(screen.getByRole('button', { name: 'Monday' })).toHaveAttribute('aria-pressed', 'true');
await user.click(screen.getByRole('button', { name: 'Saturday' }));
expect(updateSetting).toHaveBeenCalledWith('week_start', 'saturday');
});
});