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
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user