// @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');
});
});