Files
TREK/client/tests/unit/mobile/settings/MSettingsGeneral.test.tsx
T
Maurice 44c6fbf372 fix(settings): let every date follow a date format of the user's choice
Every date in TREK was written in the interface language's format, and
English is en-US, so an English interface always showed and read
mm/dd/yyyy with no way to change it. A few native date inputs followed
the browser instead, so one screen could mix two formats.

date_format is now a user setting with an admin default, next to the
time format: automatic (the language's format, as before), YYYY-MM-DD,
YYYY/MM/DD, DD.MM.YYYY, DD/MM/YYYY or MM/DD/YYYY. The date picker shows,
pre-fills, hints and reads typed dates in it, and every date written in
numbers across the app (lists, cards, admin panels, CSV and PDF exports)
goes through one formatter. Dates written with a month name keep the
language's wording. The remaining native date inputs are the shared
picker now. The display settings MCP tool reads and writes the key.

Closes #2971
2026-11-03 23:38:37 +00:00

255 lines
12 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');
});
});
describe('MSettingsGeneral date format (#2971)', () => {
it('FE-MOB-SET-018: the date-format row shows the language format when unset and the sheet saves a pick', async () => {
const user = userEvent.setup();
const updateSetting = vi.fn().mockResolvedValue(undefined);
seedStore(useSettingsStore, { settings: buildSettings({ language: 'en', date_format: undefined }), updateSetting });
render(<MSettingsGeneral />);
// The choices are labelled by how they write 31 December; auto adds the language's own way.
await user.click(screen.getByRole('button', { name: /\(12\/31\/2026\)/ }));
await user.click(await screen.findByRole('button', { name: '31.12.2026' }));
expect(updateSetting).toHaveBeenCalledWith('date_format', 'DD.MM.YYYY');
});
it('FE-MOB-SET-019: a stored format is shown on its row', () => {
seedStore(useSettingsStore, { settings: buildSettings({ language: 'en', date_format: 'YYYY/MM/DD' }) });
render(<MSettingsGeneral />);
expect(screen.getByRole('button', { name: '2026/12/31' })).toBeInTheDocument();
});
});