// @vitest-environment jsdom import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'; import {act, cleanup, render} from '@testing-library/react'; import {PreferencesProvider, usePreferences} from './PreferencesContext.jsx'; import {fetchRoutes, json} from '../test/fetchRoutes.js'; const i18n = vi.hoisted(() => ({language: 'de'})); vi.mock('react-i18next', async () => { const mock = await import('../test/i18n-mock.js'); return {...mock, useTranslation: () => ({t: mock.useTranslation().t, i18n})}; }); let preferences; function Spy() { preferences = usePreferences(); return null; } const themeColor = () => document.querySelector('meta[name="theme-color"]').content; beforeEach(() => { document.head.innerHTML = ''; }); beforeEach(() => { i18n.language = 'de'; i18n.changeLanguage = vi.fn((lng) => { i18n.language = lng; }); }); afterEach(() => { cleanup(); localStorage.clear(); }); const answerMe = (settings) => fetchRoutes({ 'GET /api/users/me': json(200, {displayName: 'Lobo', settings}), 'PUT /api/users/settings': json(204), 'GET /api/rewards/wallet': json(200, {balance: 0, catalog: [], owned: [], equipped: {}, history: []}), }); describe('PreferencesProvider storage', () => { it('renders in dark mode when localStorage throws', () => { ['getItem', 'setItem'].forEach((method) => vi.spyOn(Storage.prototype, method).mockImplementation(() => { throw new DOMException('blocked', 'SecurityError'); })); try { render(); expect(preferences.isDarkMode).toBe(true); } finally { vi.restoreAllMocks(); } }); }); describe('PreferencesProvider language', () => { it('takes the language saved in Auth', async () => { answerMe({language: 'en', useDarkTheme: true, publicProfile: false}); render(); await act(() => preferences.syncPreferences()); expect(i18n.changeLanguage).toHaveBeenCalledWith('en'); }); it('keeps its own language while Auth has none', async () => { answerMe({language: '', useDarkTheme: true, publicProfile: false}); render(); await act(() => preferences.syncPreferences()); expect(i18n.changeLanguage).not.toHaveBeenCalled(); }); it('keeps a language picked while Auth was still answering', async () => { answerMe({language: 'de', useDarkTheme: true, publicProfile: false}); render(); await act(async () => { const sync = preferences.syncPreferences(); preferences.setLanguage('en'); await sync; }); expect(i18n.changeLanguage.mock.calls).toEqual([['en']]); }); it('saves a picked language to Auth with the other settings unchanged', async () => { localStorage.setItem('tbp_session', '1'); answerMe({language: 'de', useDarkTheme: true, publicProfile: true}); render(); await act(() => preferences.setLanguage('en')); const put = fetch.mock.calls.find(([, init]) => init?.method === 'PUT'); expect(put[0]).toMatch(/\/api\/users\/settings$/); expect(JSON.parse(put[1].body)).toEqual({useDarkTheme: true, language: 'en', displayName: 'Lobo', publicProfile: true}); }); }); describe('PreferencesProvider theme color', () => { it('gives the installed app a light title bar in light mode and a dark one again in dark mode', () => { localStorage.setItem('tbp_dark_mode', 'false'); render(); expect(themeColor()).toBe('#fafafa'); act(() => preferences.setDarkMode(true)); expect(themeColor()).toBe('#09090b'); }); });