Public Access
230 lines
12 KiB
React
230 lines
12 KiB
React
// @vitest-environment jsdom
|
|
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
|
import {cleanup, fireEvent, render, screen, waitFor, within} from '@testing-library/react';
|
|
import {MemoryRouter} from 'react-router-dom';
|
|
import SettingsPage from './SettingsPage.jsx';
|
|
import {PreferencesProvider} from '../context/PreferencesContext.jsx';
|
|
|
|
vi.mock('react-i18next', async () => {
|
|
const mock = await import('../test/i18n-mock.js');
|
|
const i18n = {language: 'de', changeLanguage: vi.fn()};
|
|
return {...mock, useTranslation: () => ({t: mock.useTranslation().t, i18n})};
|
|
});
|
|
vi.mock('sonner', () => ({toast: {success: vi.fn(), error: vi.fn()}}));
|
|
|
|
const profile = (overrides = {}) => ({
|
|
id: 1, email: 'a@b.de', role: 'USER', displayName: 'Lobo', steamId: '76561198005679788',
|
|
settings: {useDarkTheme: true, language: 'de', publicProfile: true}, ...overrides,
|
|
});
|
|
|
|
const appearanceCard = () => within(screen.getByText('SETTINGS.APPEARANCE').closest('[data-slot="card"]'));
|
|
|
|
const puts = () => fetch.mock.calls.filter(([, init]) => init?.method === 'PUT').map(([, init]) => JSON.parse(init.body));
|
|
|
|
function renderPage(me) {
|
|
globalThis.fetch = vi.fn(async (url, init) => (init?.method === 'PUT'
|
|
// Both PUTs answer 204 without a body, like Auth does.
|
|
? {ok: true, status: 204, json: async () => { throw new SyntaxError('no body'); }}
|
|
: {ok: true, status: 200, json: async () => me}));
|
|
render(<MemoryRouter><PreferencesProvider><SettingsPage/></PreferencesProvider></MemoryRouter>);
|
|
}
|
|
|
|
describe('SettingsPage public profile', () => {
|
|
beforeEach(() => localStorage.setItem('tbp_session', '1'));
|
|
|
|
afterEach(() => {
|
|
cleanup();
|
|
localStorage.clear();
|
|
});
|
|
|
|
it('keeps the public profile when another setting is saved', async () => {
|
|
renderPage(profile());
|
|
|
|
fireEvent.click(await screen.findByRole('switch', {name: 'SETTINGS.DARK_THEME'}));
|
|
|
|
await waitFor(() => expect(puts()).toHaveLength(1));
|
|
expect(puts()[0].publicProfile).toBe(true);
|
|
});
|
|
|
|
it('turns the public profile off and shows the link while it is on', async () => {
|
|
renderPage(profile());
|
|
|
|
expect(await screen.findByText(/\/u\/Lobo$/)).toBeTruthy();
|
|
fireEvent.click(screen.getByRole('switch', {name: 'SETTINGS.PUBLIC_PROFILE'}));
|
|
|
|
await waitFor(() => expect(puts()).toHaveLength(1));
|
|
expect(puts()[0]).toMatchObject({publicProfile: false, displayName: 'Lobo'});
|
|
await waitFor(() => expect(screen.queryByText(/\/u\/Lobo$/)).toBeNull());
|
|
});
|
|
|
|
it('saves the stat groups the public profile shows, in a fixed order', async () => {
|
|
renderPage(profile({settings: {useDarkTheme: true, language: 'de', publicProfile: true, profileStats: ['TREND']}}));
|
|
|
|
fireEvent.click(await screen.findByRole('switch', {name: 'STATS.PILE.TITLE'}));
|
|
|
|
await waitFor(() => expect(puts()).toHaveLength(1));
|
|
expect(puts()[0]).toEqual({groups: ['PILE', 'TREND']});
|
|
const [url] = fetch.mock.calls.find(([, init]) => init?.method === 'PUT');
|
|
expect(url).toMatch(/\/api\/users\/settings\/profile-stats$/);
|
|
await waitFor(() => expect(screen.getByRole('switch', {name: 'STATS.PILE.TITLE'}).getAttribute('aria-checked'))
|
|
.toBe('true'));
|
|
});
|
|
|
|
it('waits for a save before the next switch click', async () => {
|
|
const answers = [];
|
|
const me = profile({settings: {useDarkTheme: true, language: 'de', publicProfile: true, profileStats: []}});
|
|
globalThis.fetch = vi.fn((url, init) => (init?.method === 'PUT'
|
|
? new Promise((resolve) => answers.push(() => resolve({ok: true, status: 204, json: async () => ({})})))
|
|
: Promise.resolve({ok: true, status: 200, json: async () => me})));
|
|
render(<MemoryRouter><PreferencesProvider><SettingsPage/></PreferencesProvider></MemoryRouter>);
|
|
|
|
fireEvent.click(await screen.findByRole('switch', {name: 'STATS.PILE.TITLE'}));
|
|
await waitFor(() => expect(screen.getByRole('switch', {name: 'STATS.TASTE.TITLE'}).disabled).toBe(true));
|
|
answers.forEach((answer) => answer());
|
|
await waitFor(() => expect(screen.getByRole('switch', {name: 'STATS.TASTE.TITLE'}).disabled).toBe(false));
|
|
fireEvent.click(screen.getByRole('switch', {name: 'STATS.TASTE.TITLE'}));
|
|
|
|
await waitFor(() => expect(puts()).toHaveLength(2));
|
|
expect(puts()[1]).toEqual({groups: ['PILE', 'TASTE']});
|
|
});
|
|
|
|
it('switches the group back when the save fails', async () => {
|
|
const me = profile({settings: {useDarkTheme: true, language: 'de', publicProfile: true, profileStats: []}});
|
|
globalThis.fetch = vi.fn(async (url, init) => (init?.method === 'PUT'
|
|
? {ok: false, status: 500, json: async () => ({})}
|
|
: {ok: true, status: 200, json: async () => me}));
|
|
render(<MemoryRouter><PreferencesProvider><SettingsPage/></PreferencesProvider></MemoryRouter>);
|
|
|
|
fireEvent.click(await screen.findByRole('switch', {name: 'STATS.PILE.TITLE'}));
|
|
|
|
await waitFor(() => expect(screen.getByRole('switch', {name: 'STATS.PILE.TITLE'}).getAttribute('aria-checked'))
|
|
.toBe('false'));
|
|
});
|
|
|
|
const styled = (profileStyle) => profile({settings: {useDarkTheme: true, language: 'de', publicProfile: true, profileStyle}});
|
|
const styleGroup = (label) => within(screen.getAllByRole('group', {name: label})[1]);
|
|
const stylePuts = () => fetch.mock.calls.filter(([url, init]) => init?.method === 'PUT' && url.endsWith('/profile-style'))
|
|
.map(([, init]) => JSON.parse(init.body));
|
|
|
|
it('saves the profile accent apart from the app accent', async () => {
|
|
renderPage(styled({accent: 'blue', corners: 'sharp', bannerAppid: 620, backgroundAppid: null,
|
|
sections: ['PLAYING', 'SUMMARY']}));
|
|
|
|
await screen.findByText('SETTINGS.PROFILE_STYLE');
|
|
fireEvent.click(styleGroup('SETTINGS.ACCENT').getByRole('button', {name: 'SETTINGS.ACCENT_OPTIONS.RED'}));
|
|
|
|
await waitFor(() => expect(stylePuts()).toEqual([{accent: 'red', corners: 'sharp', bannerAppid: 620,
|
|
backgroundAppid: null, sections: ['PLAYING', 'SUMMARY']}]));
|
|
expect(styleGroup('SETTINGS.ACCENT').getByRole('button', {name: 'SETTINGS.ACCENT_OPTIONS.RED'})
|
|
.getAttribute('aria-pressed')).toBe('true');
|
|
});
|
|
|
|
it('moves, hides and shows profile sections', async () => {
|
|
renderPage(styled({sections: ['PLAYING', 'SUMMARY']}));
|
|
|
|
await screen.findByText('SETTINGS.PROFILE_STYLE');
|
|
// The first row is the first shown section, PLAYING.
|
|
fireEvent.click(screen.getAllByRole('button', {name: 'SETTINGS.PROFILE_SECTION_DOWN'})[0]);
|
|
await waitFor(() => expect(stylePuts()).toHaveLength(1));
|
|
expect(stylePuts()[0].sections).toEqual(['SUMMARY', 'PLAYING']);
|
|
|
|
fireEvent.click(screen.getByRole('switch', {name: 'SETTINGS.PROFILE_SECTION.STATS'}));
|
|
await waitFor(() => expect(stylePuts()).toHaveLength(2));
|
|
expect(stylePuts()[1].sections).toEqual(['SUMMARY', 'PLAYING', 'STATS']);
|
|
});
|
|
|
|
it('removes the profile banner', async () => {
|
|
renderPage(styled({bannerAppid: 620}));
|
|
|
|
fireEvent.click(await screen.findByRole('button', {name: 'SETTINGS.PROFILE_ART_REMOVE'}));
|
|
|
|
await waitFor(() => expect(stylePuts()).toHaveLength(1));
|
|
expect(stylePuts()[0].bannerAppid).toBeNull();
|
|
});
|
|
|
|
it('offers the profile look only while the profile is public', async () => {
|
|
renderPage(profile({settings: {useDarkTheme: true, language: 'de', publicProfile: false}}));
|
|
|
|
expect((await screen.findByRole('switch', {name: 'SETTINGS.PROFILE_SECTION.STATS'})).disabled).toBe(true);
|
|
expect(styleGroup('SETTINGS.ACCENT').getByRole('button', {name: 'SETTINGS.ACCENT_OPTIONS.RED'}).disabled).toBe(true);
|
|
});
|
|
|
|
it('offers the stat groups only while the profile is public', async () => {
|
|
renderPage(profile({settings: {useDarkTheme: true, language: 'de', publicProfile: false}}));
|
|
|
|
expect((await screen.findByRole('switch', {name: 'STATS.PILE.TITLE'})).disabled).toBe(true);
|
|
expect(screen.getByText('SETTINGS.PROFILE_STATS_NEEDS_PUBLIC')).toBeTruthy();
|
|
});
|
|
|
|
it('needs a display name first', async () => {
|
|
renderPage(profile({displayName: null, settings: {useDarkTheme: true, language: 'de', publicProfile: false}}));
|
|
|
|
const toggle = await screen.findByRole('switch', {name: 'SETTINGS.PUBLIC_PROFILE'});
|
|
expect(toggle.disabled).toBe(true);
|
|
expect(screen.getByText('SETTINGS.PUBLIC_PROFILE_NEEDS_NAME')).toBeTruthy();
|
|
});
|
|
|
|
it('turns the public profile off when the display name is removed', async () => {
|
|
renderPage(profile());
|
|
|
|
const name = await screen.findByLabelText('SETTINGS.DISPLAY_NAME');
|
|
fireEvent.change(name, {target: {value: ''}});
|
|
fireEvent.click(screen.getByRole('button', {name: 'SETTINGS.SAVE_PROFILE'}));
|
|
|
|
await waitFor(() => expect(puts()).toHaveLength(1));
|
|
expect(puts()[0]).toMatchObject({displayName: null, publicProfile: false});
|
|
await waitFor(() => expect(screen.getByRole('switch', {name: 'SETTINGS.PUBLIC_PROFILE'}).getAttribute('aria-checked')).toBe('false'));
|
|
});
|
|
});
|
|
|
|
describe('SettingsPage appearance', () => {
|
|
beforeEach(() => localStorage.setItem('tbp_session', '1'));
|
|
|
|
afterEach(() => {
|
|
cleanup();
|
|
localStorage.clear();
|
|
});
|
|
|
|
const appearancePuts = () => fetch.mock.calls
|
|
.filter(([url, init]) => init?.method === 'PUT' && url.endsWith('/api/users/settings/appearance'))
|
|
.map(([, init]) => JSON.parse(init.body));
|
|
|
|
it('saves a picked accent and corner style and marks them as picked', async () => {
|
|
renderPage(profile());
|
|
|
|
await screen.findByText('SETTINGS.APPEARANCE');
|
|
fireEvent.click(appearanceCard().getByRole('button', {name: 'SETTINGS.ACCENT_OPTIONS.GREEN'}));
|
|
fireEvent.click(appearanceCard().getByRole('button', {name: 'SETTINGS.CORNER_OPTIONS.ROUND'}));
|
|
|
|
await waitFor(() => expect(appearancePuts()).toHaveLength(2));
|
|
expect(appearancePuts()[1]).toEqual({accent: 'green', corners: 'round', wideLayout: false, backgroundAppid: null, bannerAppid: null, showBanner: true});
|
|
expect(appearanceCard().getByRole('button', {name: 'SETTINGS.ACCENT_OPTIONS.GREEN'}).getAttribute('aria-pressed')).toBe('true');
|
|
expect(appearanceCard().getByRole('button', {name: 'SETTINGS.ACCENT_OPTIONS.VIOLET'}).getAttribute('aria-pressed')).toBe('false');
|
|
});
|
|
|
|
it('removes the background', async () => {
|
|
localStorage.setItem('tbp_appearance', JSON.stringify({backgroundAppid: 620}));
|
|
renderPage(profile());
|
|
|
|
fireEvent.click(await screen.findByRole('button', {name: 'SETTINGS.BACKGROUND_REMOVE'}));
|
|
|
|
await waitFor(() => expect(appearancePuts()).toHaveLength(1));
|
|
expect(appearancePuts()[0].backgroundAppid).toBeNull();
|
|
expect(screen.getByText('SETTINGS.BACKGROUND_HINT')).toBeTruthy();
|
|
});
|
|
|
|
it('turns the banner off and goes back to the automatic banner', async () => {
|
|
localStorage.setItem('tbp_appearance', JSON.stringify({bannerAppid: 620}));
|
|
renderPage(profile());
|
|
|
|
fireEvent.click(await screen.findByRole('button', {name: 'SETTINGS.BANNER_AUTO'}));
|
|
await waitFor(() => expect(appearancePuts()).toHaveLength(1));
|
|
expect(appearancePuts()[0]).toMatchObject({bannerAppid: null, showBanner: true});
|
|
expect(screen.getByText('SETTINGS.BANNER_HINT')).toBeTruthy();
|
|
|
|
fireEvent.click(screen.getByRole('switch', {name: 'SETTINGS.BANNER'}));
|
|
await waitFor(() => expect(appearancePuts()).toHaveLength(2));
|
|
expect(appearancePuts()[1].showBanner).toBe(false);
|
|
});
|
|
});
|