// 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(<>); 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(<>); 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(); 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(); 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(); 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(); 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(); 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(); expect(screen.getByRole('button', { name: 'Monday' })).toHaveAttribute('aria-pressed', 'true'); await user.click(screen.getByRole('button', { name: 'Saturday' })); expect(updateSetting).toHaveBeenCalledWith('week_start', 'saturday'); }); });