feat(tiles): let users arrange the dashboard and profile tiles
Build, Test & Push Frontend / quality-check (push) Successful in 18s
Build, Test & Push Frontend / docker (push) Successful in 28s

This commit is contained in:
lobo committed 2026-09-28 15:03:16 +02:00
1 parent b55fcfec92
commit aa9e83e0cb
24 files changed
+792 -426

No files matched your search

+7 -66
View File
@@ -57,80 +57,27 @@ describe('SettingsPage public profile', () => {
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']}));
renderPage(styled({accent: 'blue', corners: 'sharp', bannerAppid: 620, backgroundAppid: null}));
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']}]));
backgroundAppid: null}]));
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']}));
it('points to the profile page for the tiles', async () => {
renderPage(profile());
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']);
expect(await screen.findByText('SETTINGS.PROFILE_TILES_HINT')).toBeTruthy();
});
it('removes the profile banner', async () => {
@@ -145,17 +92,11 @@ describe('SettingsPage public profile', () => {
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);
await screen.findByText('SETTINGS.PROFILE_STYLE');
expect(screen.queryByText('SETTINGS.PROFILE_TILES_HINT')).toBeNull();
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}}));