feat(images): report broken covers and achievement icons
Build, Test & Push Frontend / quality-check (push) Successful in 15s
Build, Test & Push Frontend / docker (push) Successful in 22s

This commit is contained in:
lobo committed 2026-09-26 12:07:47 +02:00
1 parent ca75e6a530
commit 42ed0afe51
9 files changed
+173 -9

No files matched your search

+1 -1
View File
@@ -54,7 +54,7 @@ export default function AchievementsCard({summary}) {
to={`/library/${game.appid}`}
className="group flex items-center gap-3 rounded-lg p-1 transition-colors hover:bg-muted/50"
>
<GameCover appid={game.appid} className="w-16 shrink-0"/>
<GameCover appid={game.appid} imageUrl={game.imageUrl} className="w-16 shrink-0"/>
<div className="min-w-0 flex-1 space-y-1">
<div className="flex items-baseline justify-between gap-2">
<span className="truncate text-sm font-medium group-hover:text-primary transition-colors">
+1 -1
View File
@@ -65,7 +65,7 @@ export default function ActivityCard({activity}) {
{recentGames.map((g) => (
<li key={g.appid}>
<Link to={`/library/${g.appid}`} className="group flex items-center gap-3">
<GameCover appid={g.appid} className="w-16 shrink-0"/>
<GameCover appid={g.appid} imageUrl={g.imageUrl} className="w-16 shrink-0"/>
<span className="min-w-0 flex-1 truncate text-sm font-medium group-hover:text-primary transition-colors">
{g.name ?? `App ${g.appid}`}
</span>
+11 -4
View File
@@ -3,14 +3,18 @@ import {Gamepad2} from 'lucide-react';
import {cn} from '@/lib/utils';
import {coverUrl} from '@/utils/games.js';
import {COVER, reportBrokenImage} from '@/utils/brokenImages.js';
/**
* Steam store header (460x215). Falls back to a neutral tile when the app has no header image.
*
* @param imageUrl picture for a game that has no Steam art (added by hand from IGDB); wins over the Steam header
* @param imageUrl a game added by hand from IGDB, or a Steam header Core repaired; wins over the fixed Steam header
*/
export default function GameCover({appid, imageUrl, className}) {
const [failed, setFailed] = useState(false);
const src = imageUrl ?? coverUrl(appid);
// Per URL: a reused cover (e.g. the next suggestion) loads and reports its own picture.
const [failedSrc, setFailedSrc] = useState(null);
const failed = failedSrc === src;
return (
<div className={cn('aspect-[460/215] overflow-hidden rounded-md bg-muted', className)}>
@@ -20,11 +24,14 @@ export default function GameCover({appid, imageUrl, className}) {
</div>
) : (
<img
src={imageUrl ?? coverUrl(appid)}
src={src}
alt=""
loading="lazy"
className="size-full object-cover"
onError={() => setFailed(true)}
onError={() => {
reportBrokenImage(appid, COVER);
setFailedSrc(src);
}}
/>
)}
</div>
+41
View File
@@ -0,0 +1,41 @@
// @vitest-environment jsdom
import {afterEach, describe, expect, it, vi} from 'vitest';
import {cleanup, fireEvent, render} from '@testing-library/react';
import GameCover from './GameCover.jsx';
import {reportBrokenImage} from '../utils/brokenImages.js';
vi.mock('../utils/brokenImages.js', () => ({COVER: 'COVER', reportBrokenImage: vi.fn()}));
afterEach(() => {
cleanup();
vi.clearAllMocks();
});
describe('GameCover', () => {
it('prefers the stored picture over the Steam header', () => {
const {container} = render(<GameCover appid={620} imageUrl="https://shared.akamai.steamstatic.com/x/header.jpg"/>);
expect(container.querySelector('img').getAttribute('src')).toBe('https://shared.akamai.steamstatic.com/x/header.jpg');
});
it('reports a picture that does not load and shows the placeholder', () => {
const {container} = render(<GameCover appid={620}/>);
fireEvent.error(container.querySelector('img'));
expect(reportBrokenImage).toHaveBeenCalledWith(620, 'COVER');
expect(container.querySelector('img')).toBeNull();
expect(container.querySelector('svg')).toBeTruthy();
});
it('loads the picture of the next game after a failed one', () => {
const {container, rerender} = render(<GameCover appid={620}/>);
fireEvent.error(container.querySelector('img'));
rerender(<GameCover appid={220}/>);
expect(container.querySelector('img').getAttribute('src')).toContain('/220/header.jpg');
fireEvent.error(container.querySelector('img'));
expect(reportBrokenImage).toHaveBeenCalledWith(220, 'COVER');
});
});