Public Access
feat(tiles): let users arrange the dashboard and profile tiles
This commit is contained in:
1 parent
b55fcfec92
commit
aa9e83e0cb
24 files changed
+792
-426
No files matched your search
@@ -1,6 +1,6 @@
|
||||
// @vitest-environment jsdom
|
||||
import {afterEach, describe, expect, it, vi} from 'vitest';
|
||||
import {cleanup, render, screen, waitFor} from '@testing-library/react';
|
||||
import {cleanup, fireEvent, render, screen, waitFor} from '@testing-library/react';
|
||||
import {MemoryRouter, Route, Routes} from 'react-router-dom';
|
||||
import PublicProfilePage from './PublicProfilePage.jsx';
|
||||
import {PreferencesProvider} from '../context/PreferencesContext.jsx';
|
||||
@@ -14,8 +14,10 @@ const library = {
|
||||
finished: [{appid: 620, name: 'Portal 2', playtimeMinutes: 900, status: 'COMPLETED', rating: 5}],
|
||||
};
|
||||
|
||||
function mockBackends({profileStatus = 200, libraryStatus = 200, style, answer = library} = {}) {
|
||||
globalThis.fetch = vi.fn(async (url) => {
|
||||
function mockBackends({profileStatus = 200, libraryStatus = 200, style, answer = library, me} = {}) {
|
||||
globalThis.fetch = vi.fn(async (url, init) => {
|
||||
if (init?.method === 'PUT') return {ok: true, status: 204, json: async () => ({})};
|
||||
if (url.includes('/api/users/me')) return {ok: !!me, status: me ? 200 : 401, json: async () => me};
|
||||
if (url.includes('/api/public/profiles/library')) {
|
||||
return {ok: libraryStatus === 200, status: libraryStatus, json: async () => answer};
|
||||
}
|
||||
@@ -65,7 +67,7 @@ describe('PublicProfilePage', () => {
|
||||
it('shows only the stat groups the user shares, without links', async () => {
|
||||
library.stats = {pile: null, trend: null, topGames: null, taste: null, finished: null,
|
||||
habits: {moonTrips: 2.5, longestDay: null, underOneHourGames: 0, underOneHourShare: null}};
|
||||
mockBackends();
|
||||
mockBackends({style: {tiles: [{id: 'STATS_HABITS', size: 'M'}, {id: 'STATS_PILE', size: 'M'}]}});
|
||||
renderPage();
|
||||
|
||||
expect(await screen.findByText('STATS.HABITS.TITLE')).toBeTruthy();
|
||||
@@ -75,7 +77,7 @@ describe('PublicProfilePage', () => {
|
||||
});
|
||||
|
||||
it('applies the owner\'s accent and corners to the page only, with banner and background', async () => {
|
||||
mockBackends({style: {accent: 'red', corners: 'round', bannerAppid: 620, backgroundAppid: 220, sections: ['PLAYING']}});
|
||||
mockBackends({style: {accent: 'red', corners: 'round', bannerAppid: 620, backgroundAppid: 220, tiles: [{id: 'PLAYING', size: 'M'}]}});
|
||||
const {container} = renderPage();
|
||||
|
||||
await screen.findByText('Half-Life 2');
|
||||
@@ -88,13 +90,13 @@ describe('PublicProfilePage', () => {
|
||||
|
||||
it('shows the sections in the owner\'s order and leaves out hidden ones', async () => {
|
||||
mockBackends({
|
||||
style: {sections: ['FINISHED', 'PLAYING', 'SUMMARY']},
|
||||
style: {tiles: [{id: 'FINISHED', size: 'M'}, {id: 'PLAYING', size: 'S'}, {id: 'SUMMARY', size: 'M'}]},
|
||||
answer: {...library, byStatus: null, finishedGames: 60},
|
||||
});
|
||||
const {container} = renderPage();
|
||||
|
||||
await screen.findByText('Portal 2');
|
||||
expect([...container.querySelectorAll('[data-section]')].map((el) => el.dataset.section))
|
||||
expect([...container.querySelectorAll('[data-tile]')].map((el) => el.dataset.tile))
|
||||
.toEqual(['FINISHED', 'PLAYING', 'SUMMARY']);
|
||||
expect(screen.getByText('60')).toBeTruthy();
|
||||
expect(screen.queryByText('BACKLOG.STATUS.BACKLOG')).toBeNull();
|
||||
@@ -105,7 +107,7 @@ describe('PublicProfilePage', () => {
|
||||
const {container} = renderPage();
|
||||
|
||||
await screen.findByText('PROFILE.GAMES');
|
||||
expect([...container.querySelectorAll('[data-section]')].map((el) => el.dataset.section)).toEqual(['SUMMARY']);
|
||||
expect([...container.querySelectorAll('[data-tile]')].map((el) => el.dataset.tile)).toEqual(['SUMMARY']);
|
||||
});
|
||||
|
||||
it('shows every section for an Auth without a style', async () => {
|
||||
@@ -113,11 +115,84 @@ describe('PublicProfilePage', () => {
|
||||
const {container} = renderPage();
|
||||
|
||||
await screen.findByText('Portal 2');
|
||||
expect([...container.querySelectorAll('[data-section]')].map((el) => el.dataset.section))
|
||||
expect([...container.querySelectorAll('[data-tile]')].map((el) => el.dataset.tile))
|
||||
.toEqual(['SUMMARY', 'STATUS', 'PLAYING', 'FINISHED']);
|
||||
expect(container.querySelector('[data-accent="violet"]')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('shows the small playing tile with half the games and at half width', async () => {
|
||||
const games = Array.from({length: 8}, (_, i) => ({appid: 100 + i, name: `Game ${i}`, playtimeMinutes: 60, status: 'PLAYING'}));
|
||||
mockBackends({style: {tiles: [{id: 'PLAYING', size: 'S'}]}, answer: {...library, playing: games}});
|
||||
const {container} = renderPage();
|
||||
|
||||
await screen.findByText('Game 0');
|
||||
expect(screen.queryByText('Game 4')).toBeNull();
|
||||
expect(container.querySelector('[data-tile="PLAYING"]').className).toContain('md:col-span-6');
|
||||
});
|
||||
|
||||
const libraryLoads = () => fetch.mock.calls.filter(([url]) => url.includes('/api/public/profiles/library')).length;
|
||||
const putBody = () => JSON.parse(fetch.mock.calls.find(([, init]) => init?.method === 'PUT')[1].body);
|
||||
|
||||
it('offers "Customize" to the owner and hides a tile without a new load', async () => {
|
||||
localStorage.setItem('tbp_session', '1');
|
||||
try {
|
||||
mockBackends({me: {displayName: 'lobo', role: 'USER'}});
|
||||
const {container} = renderPage();
|
||||
|
||||
fireEvent.click(await screen.findByRole('button', {name: 'TILES.CUSTOMIZE'}));
|
||||
// The test texts carry no tile name; the first tile is SUMMARY.
|
||||
fireEvent.click(screen.getAllByRole('button', {name: 'TILES.HIDE'})[0]);
|
||||
fireEvent.click(screen.getByRole('button', {name: 'TILES.DONE'}));
|
||||
|
||||
expect(await screen.findByRole('button', {name: 'TILES.CUSTOMIZE'})).toBeTruthy();
|
||||
expect(fetch.mock.calls.find(([, init]) => init?.method === 'PUT')[0]).toMatch(/\/api\/users\/settings\/profile-tiles$/);
|
||||
expect(putBody().tiles.map((tile) => tile.id)).toEqual(['STATUS', 'PLAYING', 'FINISHED']);
|
||||
expect([...container.querySelectorAll('[data-tile]')].map((el) => el.dataset.tile)).not.toContain('SUMMARY');
|
||||
expect(libraryLoads()).toBe(1);
|
||||
} finally {
|
||||
localStorage.clear();
|
||||
}
|
||||
});
|
||||
|
||||
it('loads again after adding a tile, because Core sent no data for it', async () => {
|
||||
localStorage.setItem('tbp_session', '1');
|
||||
try {
|
||||
mockBackends({me: {displayName: 'Lobo', role: 'USER'}});
|
||||
renderPage();
|
||||
|
||||
fireEvent.click(await screen.findByRole('button', {name: 'TILES.CUSTOMIZE'}));
|
||||
fireEvent.click(screen.getByRole('button', {name: 'STATS.PILE.TITLE'}));
|
||||
fireEvent.click(screen.getByRole('button', {name: 'TILES.DONE'}));
|
||||
|
||||
await waitFor(() => expect(libraryLoads()).toBe(2));
|
||||
expect(putBody().tiles.at(-1)).toEqual({id: 'STATS_PILE', size: 'M'});
|
||||
} finally {
|
||||
localStorage.clear();
|
||||
}
|
||||
});
|
||||
|
||||
it('shows no "Customize" to a visitor', async () => {
|
||||
localStorage.setItem('tbp_session', '1');
|
||||
try {
|
||||
mockBackends({me: {displayName: 'Someone', role: 'USER'}});
|
||||
renderPage();
|
||||
|
||||
await screen.findByText('Portal 2');
|
||||
await waitFor(() => expect(fetch).toHaveBeenCalledWith(expect.stringContaining('/api/users/me'), expect.anything()));
|
||||
expect(screen.queryByRole('button', {name: 'TILES.CUSTOMIZE'})).toBeNull();
|
||||
} finally {
|
||||
localStorage.clear();
|
||||
}
|
||||
});
|
||||
it('keeps three numbers in a row on a phone', async () => {
|
||||
mockBackends();
|
||||
const {container} = renderPage();
|
||||
|
||||
await screen.findByText('PROFILE.GAMES');
|
||||
const summary = container.querySelector('[data-tile="SUMMARY"] [data-slot="card"]');
|
||||
expect(summary.className.split(' ')).toContain('grid-cols-3');
|
||||
});
|
||||
|
||||
it('says the profile is unknown or private on a 404', async () => {
|
||||
mockBackends({profileStatus: 404});
|
||||
renderPage();
|
||||
|
||||
Reference in new issue
Block a user