chore(release): v0.3.5 — dogfood UX hotfix 7건
v0.3.4 까지 누적된 dogfood UX 결함 hotfix. 사용자 직접 보고 3건 (inbox 재진입, 회고 탈출, 이동 modal 중복) + 동반 갭 4건 (count stale, 필터 잔류, 초기 로드 불일치, 배너 컨텍스트 누수). 데이터/마이그레이션 변경 없음 (스키마 v8). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,98 +0,0 @@
|
||||
// @vitest-environment jsdom
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import '@testing-library/jest-dom/vitest';
|
||||
import { render, screen, fireEvent, cleanup, waitFor } from '@testing-library/react';
|
||||
|
||||
const { mockSetStatus, mockClassify } = vi.hoisted(() => ({
|
||||
mockSetStatus: vi.fn(async () => ({ ok: true as const })),
|
||||
mockClassify: vi.fn(async () => ({
|
||||
recommended: 'completed' as const,
|
||||
rationale: '결재 끝'
|
||||
}))
|
||||
}));
|
||||
|
||||
vi.mock('../../src/renderer/inbox/api.js', () => ({
|
||||
inboxApi: {
|
||||
setStatus: mockSetStatus,
|
||||
classifyStatus: mockClassify
|
||||
}
|
||||
}));
|
||||
|
||||
import { MoveStatusModal } from '../../src/renderer/inbox/components/MoveStatusModal';
|
||||
|
||||
describe('MoveStatusModal', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it('renders reason textarea + 4 buttons + AI classify button', () => {
|
||||
render(
|
||||
<MoveStatusModal
|
||||
noteId="n1"
|
||||
rawText="t"
|
||||
summary=""
|
||||
onClose={vi.fn()}
|
||||
onMoved={vi.fn()}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByRole('textbox')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: '완료' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: '보관' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: '휴지통' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /AI 자동 분류/ })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('clicking 완료 calls setStatus with reason', async () => {
|
||||
const onMoved = vi.fn();
|
||||
render(
|
||||
<MoveStatusModal
|
||||
noteId="n1"
|
||||
rawText="t"
|
||||
summary=""
|
||||
onClose={vi.fn()}
|
||||
onMoved={onMoved}
|
||||
/>
|
||||
);
|
||||
fireEvent.change(screen.getByRole('textbox'), { target: { value: '결재 끝' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '완료' }));
|
||||
await waitFor(() => {
|
||||
expect(mockSetStatus).toHaveBeenCalledWith('n1', 'completed', '결재 끝');
|
||||
expect(onMoved).toHaveBeenCalledWith('completed', '결재 끝');
|
||||
});
|
||||
});
|
||||
|
||||
it('AI 자동 분류 → recommendation 표시 + 확정 → setStatus', async () => {
|
||||
const onMoved = vi.fn();
|
||||
render(
|
||||
<MoveStatusModal
|
||||
noteId="n1"
|
||||
rawText="t"
|
||||
summary=""
|
||||
onClose={vi.fn()}
|
||||
onMoved={onMoved}
|
||||
/>
|
||||
);
|
||||
fireEvent.change(screen.getByRole('textbox'), { target: { value: '결재 끝' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: /AI 자동 분류/ }));
|
||||
await screen.findByText(/AI 추천/);
|
||||
expect(screen.getByText(/이유: 결재 끝/)).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: /확정/ }));
|
||||
await waitFor(() => expect(onMoved).toHaveBeenCalledWith('completed', '결재 끝'));
|
||||
});
|
||||
|
||||
it('빈 사유 → null reason 전달', async () => {
|
||||
const onMoved = vi.fn();
|
||||
render(
|
||||
<MoveStatusModal
|
||||
noteId="n1"
|
||||
rawText="t"
|
||||
summary=""
|
||||
onClose={vi.fn()}
|
||||
onMoved={onMoved}
|
||||
/>
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: '보관' }));
|
||||
await waitFor(() => expect(mockSetStatus).toHaveBeenCalledWith('n1', 'archived', null));
|
||||
});
|
||||
});
|
||||
@@ -32,10 +32,11 @@ vi.mock('../../src/renderer/inbox/api.js', () => ({
|
||||
}
|
||||
}));
|
||||
|
||||
const mockRefreshMeta = vi.fn();
|
||||
vi.mock('../../src/renderer/inbox/store.js', () => ({
|
||||
useInbox: Object.assign(
|
||||
() => ({}),
|
||||
{ getState: () => ({ setTagFilter: vi.fn() }) }
|
||||
{ getState: () => ({ setTagFilter: vi.fn(), refreshMeta: mockRefreshMeta }) }
|
||||
)
|
||||
}));
|
||||
|
||||
@@ -143,19 +144,45 @@ describe('NoteCard — 이동 메뉴 (v0.2.9 Cut B Task 6)', () => {
|
||||
expect(screen.queryByRole('button', { name: '활성으로 이동' })).toBeNull();
|
||||
});
|
||||
|
||||
it('메뉴 항목 클릭 → MoveStatusModal 열림 + 확정 시 setStatus 호출', async () => {
|
||||
it('메뉴 항목 클릭 → 즉시 setStatus 호출 (modal 없음)', async () => {
|
||||
const onUpdated = vi.fn();
|
||||
render(<NoteCard note={baseNote} onUpdated={onUpdated} mode="inbox" />);
|
||||
const onDeleted = vi.fn();
|
||||
render(
|
||||
<NoteCard
|
||||
note={baseNote}
|
||||
onUpdated={onUpdated}
|
||||
onDeleted={onDeleted}
|
||||
mode="inbox"
|
||||
/>
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: '이동' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '완료로 이동' }));
|
||||
// Modal 의 dialog role 등장
|
||||
expect(screen.getByRole('dialog', { name: '이동' })).toBeInTheDocument();
|
||||
// Modal 내부의 "완료" 버튼 클릭 → setStatus
|
||||
fireEvent.click(screen.getByRole('button', { name: '완료' }));
|
||||
await waitFor(() => {
|
||||
expect(mockSetStatus).toHaveBeenCalledWith('n1', 'completed', null);
|
||||
expect(onUpdated).toHaveBeenCalled();
|
||||
// status 변경 → 현재 view (inbox) 에서 제거되어야 함.
|
||||
expect(onDeleted).toHaveBeenCalled();
|
||||
// 헤더 탭 count 동기화.
|
||||
expect(mockRefreshMeta).toHaveBeenCalled();
|
||||
});
|
||||
// modal 미존재 검증.
|
||||
expect(screen.queryByRole('dialog', { name: '이동' })).toBeNull();
|
||||
});
|
||||
|
||||
it('이동 메뉴 외부 클릭 시 dropdown 닫힘', () => {
|
||||
render(<NoteCard note={baseNote} onUpdated={() => {}} mode="inbox" />);
|
||||
fireEvent.click(screen.getByRole('button', { name: '이동' }));
|
||||
expect(screen.getByRole('button', { name: '완료로 이동' })).toBeInTheDocument();
|
||||
fireEvent.mouseDown(document.body);
|
||||
expect(screen.queryByRole('button', { name: '완료로 이동' })).toBeNull();
|
||||
});
|
||||
|
||||
it('이동 메뉴 열린 상태에서 Escape → dropdown 닫힘', () => {
|
||||
render(<NoteCard note={baseNote} onUpdated={() => {}} mode="inbox" />);
|
||||
fireEvent.click(screen.getByRole('button', { name: '이동' }));
|
||||
expect(screen.getByRole('button', { name: '완료로 이동' })).toBeInTheDocument();
|
||||
fireEvent.keyDown(document, { key: 'Escape' });
|
||||
expect(screen.queryByRole('button', { name: '완료로 이동' })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ import { render, screen, fireEvent, cleanup } from '@testing-library/react';
|
||||
// 빈 객체 대신 필요한 메서드를 stub 한다.
|
||||
vi.mock('../../src/renderer/inbox/api.js', () => ({
|
||||
inboxApi: {
|
||||
// setShowSettings(false) → setView('inbox') → loadByView('inbox') 가 listByStatus 호출.
|
||||
listByStatus: vi.fn(async () => []),
|
||||
loadOllamaSettings: vi.fn(async () => null),
|
||||
saveOllamaSettings: vi.fn(async () => ({ ok: true })),
|
||||
ollamaRecheck: vi.fn(async () => ({ ok: true })),
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { Note } from '@shared/types';
|
||||
|
||||
const mockApi = {
|
||||
listNotes: vi.fn(async () => [] as Note[]),
|
||||
listByStatus: vi.fn(async () => [] as Note[]),
|
||||
listTrash: vi.fn(async () => [] as Note[]),
|
||||
getTrashCount: vi.fn(async () => 0),
|
||||
getContinuity: vi.fn(async () => ({ weekStart: '', weekCount: 0, weekTarget: 7, consecutiveCompleteWeeks: 0, showRecoveryToast: false, lastNoteAt: null })),
|
||||
|
||||
Reference in New Issue
Block a user