feat(v028): NoteCard 이미지 <img> 렌더링 + onClick (openMedia 시그니처는 Task 3)
- 회색 placeholder div → <img src=inkling-media://...> 로 교체 - onClick 으로 inboxApi.openMedia(relPath) 호출 (현재는 InboxApi 인터페이스에 부재 → unknown cast 사용; Task 3 에서 정식 시그니처 추가 후 cast 제거 예정) - alt='' 로 decorative 처리 (role=presentation), title 에 relPath 유지 - flex-wrap 추가 — 다수 이미지 시 줄바꿈 Tests: tests/unit/NoteCard.test.tsx 신규 2건 (img src 검증, click → openMedia 호출) 회귀: 468 → 470 pass
This commit is contained in:
@@ -332,9 +332,23 @@ export function NoteCard({ note, onDeleted, onUpdated, mode = 'inbox', onRestore
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{local.media.length > 0 && (
|
{local.media.length > 0 && (
|
||||||
<div style={{ marginTop: 10, display: 'flex', gap: 6 }}>
|
<div style={{ marginTop: 10, display: 'flex', flexWrap: 'wrap', gap: 6 }}>
|
||||||
{local.media.map((m) => (
|
{local.media.map((m) => (
|
||||||
<div key={m.id} style={{ width: 48, height: 48, background: '#eee', borderRadius: 4 }} title={m.relPath} />
|
<img
|
||||||
|
key={m.id}
|
||||||
|
src={`inkling-media://${m.relPath}`}
|
||||||
|
alt=""
|
||||||
|
title={m.relPath}
|
||||||
|
onClick={() => { void (inboxApi as unknown as { openMedia: (rel: string) => Promise<unknown> }).openMedia(m.relPath); }}
|
||||||
|
style={{
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
objectFit: 'cover',
|
||||||
|
borderRadius: 4,
|
||||||
|
cursor: 'pointer',
|
||||||
|
border: '1px solid #e0e0e0'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
81
tests/unit/NoteCard.test.tsx
Normal file
81
tests/unit/NoteCard.test.tsx
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
// @vitest-environment jsdom
|
||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
import '@testing-library/jest-dom/vitest';
|
||||||
|
import { render, screen, fireEvent, cleanup } from '@testing-library/react';
|
||||||
|
import type { Note } from '@shared/types';
|
||||||
|
|
||||||
|
const { mockOpenMedia } = vi.hoisted(() => ({
|
||||||
|
mockOpenMedia: vi.fn(async () => ({ ok: true }))
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../src/renderer/inbox/api.js', () => ({
|
||||||
|
inboxApi: {
|
||||||
|
openMedia: mockOpenMedia,
|
||||||
|
deleteNote: vi.fn(),
|
||||||
|
restoreNote: vi.fn(),
|
||||||
|
permanentDeleteNote: vi.fn(),
|
||||||
|
updateAiFields: vi.fn(),
|
||||||
|
setDueDate: vi.fn(),
|
||||||
|
setIntent: vi.fn(),
|
||||||
|
dismissIntent: vi.fn()
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../src/renderer/inbox/store.js', () => ({
|
||||||
|
useInbox: Object.assign(
|
||||||
|
() => ({}),
|
||||||
|
{ getState: () => ({ setTagFilter: vi.fn() }) }
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { NoteCard } from '../../src/renderer/inbox/components/NoteCard';
|
||||||
|
|
||||||
|
const baseNote: Note = {
|
||||||
|
id: 'n1',
|
||||||
|
rawText: 'test',
|
||||||
|
aiTitle: 'T',
|
||||||
|
aiSummary: 'S',
|
||||||
|
aiStatus: 'done',
|
||||||
|
aiError: null,
|
||||||
|
aiProvider: null,
|
||||||
|
aiGeneratedAt: '2026-05-09T00:00:00Z',
|
||||||
|
titleEditedByUser: false,
|
||||||
|
summaryEditedByUser: false,
|
||||||
|
userIntent: null,
|
||||||
|
intentPromptedAt: '2026-05-09T00:00:00Z',
|
||||||
|
dueDate: null,
|
||||||
|
dueDateEditedByUser: false,
|
||||||
|
deletedAt: null,
|
||||||
|
lastRecalledAt: null,
|
||||||
|
recallDismissedAt: null,
|
||||||
|
createdAt: '2026-05-09T00:00:00Z',
|
||||||
|
updatedAt: '2026-05-09T00:00:00Z',
|
||||||
|
tags: [],
|
||||||
|
media: [
|
||||||
|
{ id: 'm1', kind: 'image', relPath: 'media/n1/img1.png', mime: 'image/png', bytes: 100 },
|
||||||
|
{ id: 'm2', kind: 'image', relPath: 'media/n1/img2.jpg', mime: 'image/jpeg', bytes: 200 }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('NoteCard — image rendering', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders <img> for each media item', () => {
|
||||||
|
render(<NoteCard note={baseNote} onUpdated={() => {}} mode="inbox" />);
|
||||||
|
const imgs = screen.getAllByRole('presentation');
|
||||||
|
expect(imgs).toHaveLength(2);
|
||||||
|
expect(imgs[0]?.getAttribute('src')).toBe('inkling-media://media/n1/img1.png');
|
||||||
|
expect(imgs[1]?.getAttribute('src')).toBe('inkling-media://media/n1/img2.jpg');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clicking <img> calls inboxApi.openMedia', () => {
|
||||||
|
render(<NoteCard note={baseNote} onUpdated={() => {}} mode="inbox" />);
|
||||||
|
const first = screen.getAllByRole('presentation')[0];
|
||||||
|
if (first === undefined) throw new Error('expected at least one img');
|
||||||
|
fireEvent.click(first);
|
||||||
|
expect(mockOpenMedia).toHaveBeenCalledWith('media/n1/img1.png');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user