// @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 + 3 buttons + AI classify button (v0.4 — 보관 제거)', () => { render( ); expect(screen.getByRole('textbox')).toBeInTheDocument(); expect(screen.getByRole('button', { name: '완료' })).toBeInTheDocument(); expect(screen.queryByRole('button', { name: '보관' })).toBeNull(); 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( ); 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( ); 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('currentStatus=completed → Inbox/휴지통 노출, 완료/보관 미노출 (v0.4 — 보관 제거)', () => { render( ); expect(screen.getByRole('button', { name: 'Inbox' })).toBeInTheDocument(); expect(screen.queryByRole('button', { name: '보관' })).toBeNull(); expect(screen.getByRole('button', { name: '휴지통' })).toBeInTheDocument(); expect(screen.queryByRole('button', { name: '완료' })).toBeNull(); }); // v0.4 Task 16 — currentStatus=archived 는 NoteStatus 에서 제거됨. 테스트 제거. it('currentStatus=trashed → Inbox/완료 노출, 휴지통/보관 미노출 (v0.4 — 보관 제거)', () => { render( ); expect(screen.getByRole('button', { name: 'Inbox' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '완료' })).toBeInTheDocument(); expect(screen.queryByRole('button', { name: '보관' })).toBeNull(); expect(screen.queryByRole('button', { name: '휴지통' })).toBeNull(); }); it('Escape key → onClose 호출', () => { const onClose = vi.fn(); render( ); fireEvent.keyDown(document, { key: 'Escape' }); expect(onClose).toHaveBeenCalled(); }); it('overlay 클릭 → onClose, modal body 클릭 → 무반응', () => { const onClose = vi.fn(); render( ); // body 클릭 (textarea) → onClose 호출 안 됨 fireEvent.click(screen.getByRole('textbox')); expect(onClose).not.toHaveBeenCalled(); // overlay (dialog) 클릭 → onClose fireEvent.click(screen.getByRole('dialog', { name: '이동' })); expect(onClose).toHaveBeenCalled(); }); it('빈 사유 → null reason 전달', async () => { const onMoved = vi.fn(); render( ); fireEvent.click(screen.getByRole('button', { name: '완료' })); await waitFor(() => expect(mockSetStatus).toHaveBeenCalledWith('n1', 'completed', null)); }); });