Public Access
feat(psn): replace PSN import button with sync review
Remove the PSN import tab; linking now starts a sync. Show the PSN review link, last played date and PSN expiry notes. PSN rows hide the hours field; unchecked titles you saw are dismissed, unseen ones stay open.
This commit is contained in:
1 parent
599b083591
commit
27c402fa34
30 files changed
+338
-286
No files matched your search
@@ -61,15 +61,17 @@ const Dashboard = () => {
|
||||
};
|
||||
|
||||
const hasSteamId = !!userData?.steamId;
|
||||
const showLibrary = hasSteamId || steamData.count > 0;
|
||||
// A PlayStation link syncs too, also without a Steam ID.
|
||||
const canSync = hasSteamId || !!steamData.canSync;
|
||||
const showLibrary = canSync || steamData.count > 0;
|
||||
const {appearance, equipped} = usePreferences();
|
||||
const steamProfile = useSteamProfile(hasSteamId);
|
||||
const bannerAppid = appearance.showBanner
|
||||
? appearance.bannerAppid ?? activity?.recentGames?.[0]?.appid ?? null
|
||||
: null;
|
||||
const sync = useLibrarySync({
|
||||
enabled: hasSteamId,
|
||||
expectInitialSync: hasSteamId && steamData.count === 0,
|
||||
enabled: canSync,
|
||||
expectInitialSync: canSync && steamData.count === 0,
|
||||
onComplete: () => {
|
||||
reloadLibrary();
|
||||
stats.reload();
|
||||
@@ -126,7 +128,7 @@ const Dashboard = () => {
|
||||
</Button>
|
||||
</Card>
|
||||
);
|
||||
case 'ACTIVITY': return hasSteamId && activity && <ActivityCard activity={activity} charts={charts} onChart={change}/>;
|
||||
case 'ACTIVITY': return canSync && activity && <ActivityCard activity={activity} charts={charts} onChart={change}/>;
|
||||
case 'ACHIEVEMENTS': return hasSteamId && achievements && <AchievementsCard summary={achievements}/>;
|
||||
default: return statGroupTile(id, stats.stats, true, charts, change);
|
||||
}
|
||||
@@ -137,14 +139,14 @@ const Dashboard = () => {
|
||||
<PageContainer className="p-4 md:p-8 space-y-6">
|
||||
<DashboardWelcomeCard bannerAppid={bannerAppid} avatarUrl={steamProfile?.avatarUrl}
|
||||
equipped={equipped}
|
||||
sync={sync} forceSync={forceSync} editing={editing} onCustomize={() => setEditing(true)}/>
|
||||
{hasSteamId && <SyncChangesBanner lastSyncedAt={sync.lastSyncedAt}/>}
|
||||
sync={sync} forceSync={forceSync} canSync={canSync} editing={editing} onCustomize={() => setEditing(true)}/>
|
||||
{canSync && <SyncChangesBanner lastSyncedAt={sync.lastSyncedAt}/>}
|
||||
|
||||
<TileGrid tiles={DASHBOARD_TILES} titles={DASHBOARD_TITLES} layout={layout} renderTile={renderTile} editing={editing}
|
||||
defaultLayout={DEFAULT_DASHBOARD} onSave={saveLayout} onCancel={() => setEditing(false)}/>
|
||||
<WhatsNewDialog/>
|
||||
{/* Last child: space-y gives every other child a bottom margin, which would move the fixed card. */}
|
||||
<DashboardHint user={userData} summary={summaryFailed ? null : summary ?? undefined}
|
||||
<DashboardHint user={userData} canSync={canSync} summary={summaryFailed ? null : summary ?? undefined}
|
||||
trend={stats.failed ? null : stats.stats ? stats.stats.trend ?? null : undefined}/>
|
||||
</PageContainer>
|
||||
</StatDrillProvider>
|
||||
|
||||
@@ -234,6 +234,43 @@ describe('Dashboard', () => {
|
||||
await waitFor(() => expect(watchSync).toHaveBeenCalledTimes(1));
|
||||
});
|
||||
|
||||
describe('of a user with a PlayStation link and no Steam ID', () => {
|
||||
const psnOnlyRoutes = (canSync) => ({
|
||||
...dashboardRoutes(),
|
||||
'GET /api/users/me': json(200, {email: 'a@b.de', displayName: 'Lobo', settings: {}}),
|
||||
'GET /api/library/games-count': json(200, {count: 1, hasSteamId: false, canSync}),
|
||||
});
|
||||
const asked = (path) => fetch.mock.calls.some(([url]) => url.includes(path));
|
||||
|
||||
it('loads the activity and the sync result, but not the achievements', async () => {
|
||||
fetchRoutes(psnOnlyRoutes(true));
|
||||
renderDashboard();
|
||||
|
||||
expect(await screen.findByText(/DASHBOARD.HELLO/)).toBeTruthy();
|
||||
await waitFor(() => expect(asked('/api/library/activity')).toBe(true));
|
||||
await waitFor(() => expect(asked('/api/library/sync-changes')).toBe(true));
|
||||
expect(asked('/api/library/achievements/summary')).toBe(false);
|
||||
expect(asked('/api/library/steam-profile')).toBe(false);
|
||||
});
|
||||
|
||||
it('shows the sync button of the welcome card', async () => {
|
||||
fetchRoutes(psnOnlyRoutes(true));
|
||||
renderDashboard();
|
||||
|
||||
expect(await screen.findByText('LIBRARY.RESYNC')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('asks for no activity and no sync result without a PlayStation link', async () => {
|
||||
fetchRoutes(psnOnlyRoutes(false));
|
||||
renderDashboard();
|
||||
|
||||
expect(await screen.findByText(/DASHBOARD.HELLO/)).toBeTruthy();
|
||||
expect(asked('/api/library/activity')).toBe(false);
|
||||
expect(asked('/api/library/sync-changes')).toBe(false);
|
||||
expect(screen.queryByText('LIBRARY.RESYNC')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('does not watch a sync when the server started none', async () => {
|
||||
fetchRoutes(dashboardRoutes());
|
||||
renderDashboard();
|
||||
|
||||
@@ -24,9 +24,10 @@ import useGame from '../hooks/useGame.js';
|
||||
import PageContainer from '../components/PageContainer.jsx';
|
||||
import {canCorrect} from '../utils/corrections.js';
|
||||
import {celebrationFor, celebrationLevel} from '../utils/confetti.js';
|
||||
import {formatDate} from '../utils/format.js';
|
||||
|
||||
export default function GameDetail() {
|
||||
const {t} = useTranslation();
|
||||
const {t, i18n} = useTranslation();
|
||||
const {appid} = useParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -87,6 +88,12 @@ export default function GameDetail() {
|
||||
onHiddenChanged={(hidden) => setData((d) => ({...d, hidden}))}
|
||||
onProfileStyleSaved={setSavedProfileStyle}/>
|
||||
|
||||
{data.lastPlayedAt && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('GAME_DETAIL.LAST_PLAYED', {date: formatDate(data.lastPlayedAt, i18n.language, {dateStyle: 'medium'})})}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* The finished question also answers "are you playing it?". */}
|
||||
{!(data.finishedHint && ['BACKLOG', 'PLAYING'].includes(data.status)) && (
|
||||
<PlayingHint game={data} onChanged={(entry) => setData((d) => ({...d, ...entry}))}/>
|
||||
|
||||
@@ -249,6 +249,29 @@ describe('GameDetail of a game added by hand', () => {
|
||||
expect(screen.getByText('MANUAL_GAME.NO_ACHIEVEMENTS')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('psnRowOffersNoHoursField', async () => {
|
||||
answer({...manualGame, source: 'PSN', platform: 'PS5'});
|
||||
renderDetail(-1025);
|
||||
|
||||
expect(await screen.findByLabelText('MANUAL_GAME.PLATFORM')).toBeTruthy();
|
||||
expect(screen.queryByLabelText('MANUAL_GAME.HOURS')).toBeNull();
|
||||
});
|
||||
|
||||
it('shows when a PlayStation game was last played, and nothing for a game without that date', async () => {
|
||||
answer({...manualGame, source: 'PSN', platform: 'PS5', lastPlayedAt: '2026-10-05T18:30:00Z'},
|
||||
{'PUT /api/library/games/-1025/manual': noBody, 'DELETE /api/library/games/-1025': noBody});
|
||||
renderDetail(-1025);
|
||||
|
||||
expect(await screen.findByText('GAME_DETAIL.LAST_PLAYED')).toBeTruthy();
|
||||
cleanup();
|
||||
|
||||
answer({...manualGame, source: 'PSN', platform: 'PS5', lastPlayedAt: null});
|
||||
renderDetail(-1025);
|
||||
|
||||
expect(await screen.findByLabelText('MANUAL_GAME.PLATFORM')).toBeTruthy();
|
||||
expect(screen.queryByText('GAME_DETAIL.LAST_PLAYED')).toBeNull();
|
||||
});
|
||||
|
||||
it('saves platform and hours', async () => {
|
||||
renderDetail(-1025);
|
||||
fireEvent.change(await screen.findByLabelText('MANUAL_GAME.HOURS'), {target: {value: '2.5'}});
|
||||
|
||||
@@ -22,7 +22,7 @@ beforeEach(() => {
|
||||
'GET /api/users/me': json(200, {settings: {}}),
|
||||
'GET /api/rewards/wallet': json(200, {balance: 0, catalog: [], owned: [], equipped: {COMPLETED_EFFECT: 'FX_GOLD_SHINE'}, history: []}),
|
||||
'GET /api/library/games': json(200, {content: games, page: 0, size: 20, totalElements: 2, totalPages: 1}),
|
||||
'GET /api/library/games-count': json(200, {count: 2, hasSteamId: true}),
|
||||
'GET /api/library/games-count': json(200, {count: 2, hasSteamId: true, canSync: true}),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -65,8 +65,9 @@ export default function GameLibrary() {
|
||||
const filtered = Boolean(listStatus || q.trim() || listShortOnly || hiddenOnly || listHideDuplicates);
|
||||
const [search, setSearch] = useState(q);
|
||||
const [adding, setAdding] = useState(false);
|
||||
// Starts true until /games-count answers, so a Steam user does not see the sync controls vanish and come back.
|
||||
const [hasSteamId, setHasSteamId] = useState(true);
|
||||
// A Steam ID or a PlayStation link. Starts true until /games-count answers, so the sync controls do not vanish
|
||||
// and come back.
|
||||
const [canSync, setCanSync] = useState(true);
|
||||
|
||||
const [data, setData] = useState({content: [], page: 0, size, totalElements: 0, totalPages: 0});
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
@@ -98,7 +99,7 @@ export default function GameLibrary() {
|
||||
apiFetch(`${import.meta.env.VITE_CORE_API_URL}/api/library/games-count`)
|
||||
.then((response) => (response.ok ? response.json() : null))
|
||||
.then((json) => {
|
||||
if (!cancelled && json) setHasSteamId(json.hasSteamId);
|
||||
if (!cancelled && json) setCanSync(json.canSync);
|
||||
})
|
||||
.catch(() => {});
|
||||
return () => {
|
||||
@@ -108,7 +109,7 @@ export default function GameLibrary() {
|
||||
|
||||
const expectInitialSync = !isLoading && !error && !filtered && data.totalElements === 0;
|
||||
const sync = useLibrarySync({
|
||||
enabled: hasSteamId,
|
||||
enabled: canSync,
|
||||
expectInitialSync,
|
||||
onComplete: load,
|
||||
});
|
||||
@@ -164,7 +165,7 @@ export default function GameLibrary() {
|
||||
<LibraryTabs/>
|
||||
<h1 className="text-3xl font-bold tracking-tight">{t('LIBRARY.TITLE')}</h1>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
{hasSteamId && (
|
||||
{canSync && (
|
||||
<>
|
||||
<SyncStatusBadge
|
||||
status={sync.status}
|
||||
|
||||
@@ -17,10 +17,10 @@ vi.mock('../hooks/useForceSync.js', () => ({default: () => ({trigger: vi.fn(), b
|
||||
const DEBOUNCE_WAIT_MS = 700;
|
||||
const page = {content: [{appid: 220, name: 'Half-Life 2', playtimeMinutes: 0, status: 'BACKLOG'}], page: 0, size: 20, totalElements: 1, totalPages: 1};
|
||||
|
||||
const answer = (games = page.content, hasSteamId = true) => fetchRoutes({
|
||||
const answer = (games = page.content, canSync = true, hasSteamId = canSync) => fetchRoutes({
|
||||
'GET /api/users/me': json(200, {settings: {}}),
|
||||
'GET /api/library/games': json(200, {...page, content: games, totalElements: games.length, totalPages: games.length ? 1 : 0}),
|
||||
'GET /api/library/games-count': json(200, {count: games.length, hasSteamId}),
|
||||
'GET /api/library/games-count': json(200, {count: games.length, hasSteamId, canSync}),
|
||||
});
|
||||
|
||||
const wait = (ms) => act(() => new Promise((resolve) => setTimeout(resolve, ms)));
|
||||
@@ -92,7 +92,7 @@ describe('GameLibrary search', () => {
|
||||
const games = (name) => json(200, {...page, content: [{...page.content[0], name}], totalElements: 1, totalPages: 1});
|
||||
fetchRoutes({
|
||||
'GET /api/users/me': json(200, {settings: {}}),
|
||||
'GET /api/library/games-count': json(200, {count: 1, hasSteamId: true}),
|
||||
'GET /api/library/games-count': json(200, {count: 1, hasSteamId: true, canSync: true}),
|
||||
'GET /api/library/games': (url, init) => {
|
||||
const q = new URL(url, 'http://x').searchParams.get('q');
|
||||
if (!q) return games('Half-Life 2');
|
||||
@@ -263,7 +263,15 @@ describe('GameLibrary with games added by hand', () => {
|
||||
expect(screen.getByRole('link', {name: 'IMPORT.OPEN'}).getAttribute('href')).toBe('/import');
|
||||
});
|
||||
|
||||
it('hides the Steam sync for a user without a Steam ID but still offers adding a game', async () => {
|
||||
it('shows the sync for a user with a PlayStation link and no Steam ID', async () => {
|
||||
answer([zelda], true, false);
|
||||
renderLibrary();
|
||||
|
||||
await screen.findByText('Zelda');
|
||||
expect(await screen.findByText('LIBRARY.RESYNC')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('hides the sync for a user who has neither a Steam ID nor a PlayStation link but still offers adding a game', async () => {
|
||||
answer([zelda], false);
|
||||
renderLibrary();
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import {decodeStoreList} from '../utils/storeList.js';
|
||||
|
||||
/**
|
||||
* Many games at once: a list or a CSV goes to Core, which matches it with IGDB in the background; the user checks
|
||||
* the matches and saves them. ?id= opens one import; ?source=PSN opens the PlayStation input; the history below can
|
||||
* the matches and saves them. ?id= opens one import (also the PlayStation review the sync made); the history below can
|
||||
* undo a saved import. #data= is the list the bookmarklet read on a store page.
|
||||
*/
|
||||
export default function ImportPage() {
|
||||
@@ -84,8 +84,7 @@ export default function ImportPage() {
|
||||
<ImportView key={shownId} id={shownId} onDone={done} onChanged={loadImports}/>
|
||||
) : (
|
||||
// A store list comes after the first answer: a new key opens its tab.
|
||||
<ImportInput key={storeRows ? 'store' : 'input'} storeRows={storeRows}
|
||||
initialSource={searchParams.get('source') === 'PSN' ? 'PSN' : null} onCreated={(created) => {
|
||||
<ImportInput key={storeRows ? 'store' : 'input'} storeRows={storeRows} onCreated={(created) => {
|
||||
setStoreRows(null);
|
||||
show(created);
|
||||
loadImports();
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
// @vitest-environment jsdom
|
||||
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
import {cleanup, fireEvent, render, screen, waitFor} from '@testing-library/react';
|
||||
import {MemoryRouter, Route, Routes} from 'react-router-dom';
|
||||
import {toast} from 'sonner';
|
||||
import ImportPage from './ImportPage.jsx';
|
||||
import {PreferencesProvider} from '../context/PreferencesContext.jsx';
|
||||
import {CurrentUserProvider} from '../context/CurrentUserContext.jsx';
|
||||
import {fetchRoutes, json} from '../test/fetchRoutes.js';
|
||||
|
||||
vi.mock('react-i18next', () => import('../test/i18n-mock.js'));
|
||||
vi.mock('sonner', () => ({toast: {success: vi.fn(), error: vi.fn(), info: vi.fn()}}));
|
||||
|
||||
const linked = {enabled: true, linked: true, linkedAt: '2026-10-01T10:00:00Z', expiresAt: new Date(Date.now() + 5 * 86_400_000).toISOString()};
|
||||
|
||||
function renderPage(routes, entry = '/import?source=PSN') {
|
||||
fetchRoutes({
|
||||
'GET /api/users/me': json(200, {settings: {favoritePlatform: null}}),
|
||||
'GET /api/library/imports': json(200, {imports: []}),
|
||||
'GET /api/library/psn-account': json(200, linked),
|
||||
...routes,
|
||||
});
|
||||
render(
|
||||
<MemoryRouter initialEntries={[entry]}>
|
||||
<Routes>
|
||||
<Route path="/import" element={<PreferencesProvider><CurrentUserProvider><ImportPage/></CurrentUserProvider></PreferencesProvider>}/>
|
||||
<Route path="/settings" element={<p>settings page</p>}/>
|
||||
</Routes>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
}
|
||||
|
||||
const startButton = () => screen.findByRole('button', {name: 'IMPORT.START'});
|
||||
|
||||
beforeEach(() => localStorage.setItem('tbp_session', '1'));
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
localStorage.clear();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('ImportPage PlayStation source', () => {
|
||||
it('opens on the PlayStation tab for ?source=PSN', async () => {
|
||||
renderPage({});
|
||||
|
||||
expect(await screen.findByText('IMPORT.PSN_HINT')).toBeTruthy();
|
||||
expect(screen.getByRole('button', {name: 'IMPORT.SOURCE_PSN'}).getAttribute('aria-pressed')).toBe('true');
|
||||
});
|
||||
|
||||
it('opens on the list without the parameter', async () => {
|
||||
renderPage({}, '/import');
|
||||
|
||||
expect(await screen.findByLabelText('IMPORT.TEXT_LABEL')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('points to the settings when no account is linked', async () => {
|
||||
renderPage({'GET /api/library/psn-account': json(200, {enabled: true, linked: false})});
|
||||
|
||||
expect(await screen.findByText(/IMPORT.PSN_NOT_LINKED/)).toBeTruthy();
|
||||
expect(screen.queryByRole('button', {name: 'IMPORT.START'})).toBeNull();
|
||||
fireEvent.click(screen.getByRole('link', {name: 'IMPORT.PSN_SETTINGS'}));
|
||||
expect(await screen.findByText('settings page')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('points to the settings when the link has ended', async () => {
|
||||
renderPage({'GET /api/library/psn-account': json(200, {...linked, expiresAt: '2020-01-01T00:00:00Z'})});
|
||||
|
||||
expect(await screen.findByText(/IMPORT.PSN_RELINK/)).toBeTruthy();
|
||||
expect(screen.queryByRole('button', {name: 'IMPORT.START'})).toBeNull();
|
||||
});
|
||||
|
||||
it('starts the import at the PlayStation endpoint without sending rows, and shows its progress', async () => {
|
||||
renderPage({
|
||||
'POST /api/library/psn-account/imports': json(202, {id: 9}),
|
||||
'GET /api/library/imports/9': json(200, {id: 9, source: 'PSN', status: 'MATCHING', total: 4, matched: 1}),
|
||||
});
|
||||
|
||||
fireEvent.click(await startButton());
|
||||
|
||||
expect(await screen.findByText('IMPORT.MATCHING')).toBeTruthy();
|
||||
const [, init] = fetch.mock.calls.find(([url, call]) => call?.method === 'POST' && url.endsWith('/psn-account/imports'));
|
||||
expect(init.body).toBeUndefined();
|
||||
});
|
||||
|
||||
it.each([
|
||||
[204, {}, 'info', 'IMPORT.PSN_NO_TITLES'],
|
||||
[404, {}, 'error', 'IMPORT.PSN_NOT_LINKED'],
|
||||
[409, {code: 'PSN_RELINK'}, 'error', 'IMPORT.PSN_RELINK'],
|
||||
[409, {code: 'IMPORT_RUNNING'}, 'error', 'IMPORT.RUNNING'],
|
||||
[429, {code: 'PSN_BUSY'}, 'error', 'IMPORT.PSN_BUSY'],
|
||||
[502, {}, 'error', 'IMPORT.PSN_UNAVAILABLE'],
|
||||
[503, {}, 'error', 'MANUAL_GAME.UNAVAILABLE'],
|
||||
])('answers status %i %j with a toast', async (status, body, kind, key) => {
|
||||
renderPage({'POST /api/library/psn-account/imports': json(status, body)});
|
||||
|
||||
fireEvent.click(await startButton());
|
||||
|
||||
await waitFor(() => expect(toast[kind]).toHaveBeenCalledWith(key));
|
||||
expect(screen.getByText('IMPORT.PSN_HINT')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -259,7 +259,7 @@ describe('ImportPage', () => {
|
||||
expect(JSON.parse(posted('POST', '/5/save')[1].body)).toEqual({rows: [
|
||||
{lineNo: 1, igdbId: 1, platform: 'Switch', status: 'BACKLOG', playtimeMinutes: 0},
|
||||
{lineNo: 3, igdbId: 1, platform: 'Switch', status: 'PLAYING', playtimeMinutes: 90},
|
||||
]});
|
||||
], shownLineNos: [1, 2, 3]});
|
||||
});
|
||||
|
||||
it('checks every line with a new match at once, and none', async () => {
|
||||
|
||||
+11
-2
@@ -1,5 +1,6 @@
|
||||
import {useEffect, useState} from 'react';
|
||||
import {useTranslation} from 'react-i18next';
|
||||
import {Link} from 'react-router-dom';
|
||||
import {Loader2} from 'lucide-react';
|
||||
|
||||
import {Card, CardContent, CardHeader, CardTitle} from '@/components/ui/card';
|
||||
@@ -8,8 +9,10 @@ import PageContainer from '../components/PageContainer.jsx';
|
||||
import {formatDate} from '../utils/format.js';
|
||||
import {digestParts, fetchInbox, markInboxRead} from '../utils/inbox.js';
|
||||
|
||||
const PSN_KINDS = ['PSN_EXPIRING', 'PSN_EXPIRED'];
|
||||
|
||||
/**
|
||||
* The newest notes about the people the user follows. Opening the page marks them read.
|
||||
* The newest notes about the people the user follows and about the PlayStation link. Opening the page marks them read.
|
||||
*/
|
||||
export default function InboxPage() {
|
||||
const {t, i18n} = useTranslation();
|
||||
@@ -32,6 +35,7 @@ export default function InboxPage() {
|
||||
}, []);
|
||||
|
||||
const text = (note) => {
|
||||
if (PSN_KINDS.includes(note.kind)) return t(`INBOX.${note.kind}`);
|
||||
const name = note.displayName ?? t('FOLLOW.PRIVATE_USER');
|
||||
if (note.kind === 'VISIBILITY') return t('INBOX.VISIBILITY', {name});
|
||||
const parts = digestParts(note).map(({key, count}) => t(`INBOX.PART_${key}`, {count})).join(', ');
|
||||
@@ -55,7 +59,12 @@ export default function InboxPage() {
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{formatDate(note.createdAt, i18n.language, {dateStyle: 'medium'})}
|
||||
</p>
|
||||
<p className={cn('break-words text-sm', !note.read && 'font-semibold')}>{text(note)}</p>
|
||||
<p className={cn('break-words text-sm', !note.read && 'font-semibold')}>
|
||||
{text(note)}
|
||||
{PSN_KINDS.includes(note.kind) && (
|
||||
<> <Link to="/settings" className="underline">{t('INBOX.OPEN_SETTINGS')}</Link></>
|
||||
)}
|
||||
</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@@ -34,6 +34,24 @@ describe('InboxPage', () => {
|
||||
expect(await screen.findByText('INBOX.VISIBILITY')).toBeTruthy();
|
||||
});
|
||||
|
||||
it.each(['PSN_EXPIRING', 'PSN_EXPIRED'])('shows the %s note with a link to the settings', async (kind) => {
|
||||
fetchRoutes({'GET /api/inbox': json(200, {results: [note(1, {kind, displayName: null})], unread: 1}),
|
||||
'PUT /api/inbox/read': json(204)});
|
||||
renderWithProviders(<InboxPage/>);
|
||||
|
||||
expect(await screen.findByText(`INBOX.${kind}`)).toBeTruthy();
|
||||
expect(screen.getByRole('link', {name: 'INBOX.OPEN_SETTINGS'}).getAttribute('href')).toBe('/settings');
|
||||
expect(screen.queryByText('INBOX.DIGEST')).toBeNull();
|
||||
});
|
||||
|
||||
it('gives a digest or a visibility note no settings link', async () => {
|
||||
fetchRoutes({'GET /api/inbox': json(200, {results: [note(1, {newGames: 2}), note(2, {kind: 'VISIBILITY'})], unread: 0})});
|
||||
renderWithProviders(<InboxPage/>);
|
||||
|
||||
expect(await screen.findByText('INBOX.VISIBILITY')).toBeTruthy();
|
||||
expect(screen.queryByRole('link', {name: 'INBOX.OPEN_SETTINGS'})).toBeNull();
|
||||
});
|
||||
|
||||
it('saysSoWhenThereAreNoNotesAndMarksNothing', async () => {
|
||||
fetchRoutes({'GET /api/inbox': json(200, {results: [], unread: 0})});
|
||||
renderWithProviders(<InboxPage/>);
|
||||
|
||||
Reference in new issue
Block a user