Public Access
feat(images): report broken covers and achievement icons
This commit is contained in:
1 parent
ca75e6a530
commit
42ed0afe51
9 files changed
+173
-9
No files matched your search
@@ -21,6 +21,7 @@ import useSaveAppearance from '../hooks/useSaveAppearance.js';
|
||||
import {usePreferences} from '../context/PreferencesContext.jsx';
|
||||
import PageContainer from '../components/PageContainer.jsx';
|
||||
import {apiFetch} from '../utils/api.js';
|
||||
import {ACHIEVEMENT_ICONS, reportBrokenImage} from '../utils/brokenImages.js';
|
||||
import {hasSession} from '../utils/auth.js';
|
||||
import {isSteamKey} from '../utils/manualGames.js';
|
||||
|
||||
@@ -35,6 +36,18 @@ const formatUnlockedAt = (instant, locale) => {
|
||||
}
|
||||
};
|
||||
|
||||
function AchievementIcon({appid, src, unlocked}) {
|
||||
const [failed, setFailed] = useState(false);
|
||||
if (!src || failed) return <div className="size-12 rounded-md bg-muted"/>;
|
||||
return (
|
||||
<img src={src} alt="" className={cn('size-12 rounded-md', !unlocked && 'grayscale')}
|
||||
onError={() => {
|
||||
reportBrokenImage(appid, ACHIEVEMENT_ICONS);
|
||||
setFailed(true);
|
||||
}}/>
|
||||
);
|
||||
}
|
||||
|
||||
export default function GameDetail() {
|
||||
const {t, i18n} = useTranslation();
|
||||
const {appid} = useParams();
|
||||
@@ -261,9 +274,7 @@ export default function GameDetail() {
|
||||
a.unlocked ? 'border-primary/40' : 'opacity-60',
|
||||
)}
|
||||
>
|
||||
{icon
|
||||
? <img src={icon} alt="" className={cn('size-12 rounded-md', !a.unlocked && 'grayscale')}/>
|
||||
: <div className="size-12 rounded-md bg-muted"/>}
|
||||
<AchievementIcon key={icon} appid={data.appid} src={icon} unlocked={a.unlocked}/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate font-medium">{a.displayName ?? a.apiName}</div>
|
||||
{a.description && (
|
||||
|
||||
@@ -4,6 +4,7 @@ import {cleanup, fireEvent, render, screen, waitFor} from '@testing-library/reac
|
||||
import {MemoryRouter, Route, Routes} from 'react-router-dom';
|
||||
import GameDetail from './GameDetail.jsx';
|
||||
import {PreferencesProvider} from '../context/PreferencesContext.jsx';
|
||||
import {resetBrokenImageReports} from '../utils/brokenImages.js';
|
||||
|
||||
vi.mock('react-i18next', () => import('../test/i18n-mock.js'));
|
||||
vi.mock('sonner', () => ({toast: {success: vi.fn(), error: vi.fn()}}));
|
||||
@@ -32,9 +33,30 @@ beforeEach(() => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
localStorage.clear();
|
||||
resetBrokenImageReports();
|
||||
});
|
||||
|
||||
describe('GameDetail', () => {
|
||||
it('reports a broken achievement icon once and shows the empty tile', async () => {
|
||||
const achievement = (apiName) => ({apiName, displayName: apiName, unlocked: false, iconUrl: `https://x/${apiName}.jpg`,
|
||||
iconGrayUrl: `https://x/${apiName}_gray.jpg`});
|
||||
globalThis.fetch = vi.fn(async (url, init) => init?.method === 'POST'
|
||||
? {ok: true, status: 204}
|
||||
: {ok: true, status: 200, json: async () => ({...game, hasAchievements: true, statsVisible: true,
|
||||
achievementsTotal: 2, achievementsUnlocked: 0, completionPercent: 0,
|
||||
achievements: [achievement('A'), achievement('B')]})});
|
||||
const {container} = renderDetail();
|
||||
|
||||
await screen.findByText('A');
|
||||
container.querySelectorAll('img[src^="https://x/"]').forEach((img) => fireEvent.error(img));
|
||||
|
||||
const reports = fetch.mock.calls.filter(([, init]) => init?.method === 'POST');
|
||||
expect(reports).toHaveLength(1);
|
||||
expect(reports[0][0]).toContain('/api/library/broken-images');
|
||||
expect(JSON.parse(reports[0][1].body)).toEqual({appid: 220, kind: 'ACHIEVEMENT_ICONS'});
|
||||
expect(container.querySelector('img[src^="https://x/"]')).toBeNull();
|
||||
});
|
||||
|
||||
it('links the speedrun.com page when Core found one', async () => {
|
||||
globalThis.fetch = vi.fn(async () => ({ok: true, status: 200,
|
||||
json: async () => ({...game, speedrunUrl: 'https://www.speedrun.com/hl2'})}));
|
||||
|
||||
Reference in new issue
Block a user