// @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'; import React from 'react'; const { mockMoveNote, mockBatchClassifyDefault } = vi.hoisted(() => ({ mockMoveNote: vi.fn(async () => ({ ok: true as const })), mockBatchClassifyDefault: vi.fn(async () => ({ assignments: [] })) })); vi.mock('../../src/renderer/inbox/api.js', () => ({ inboxApi: { batchClassifyDefault: mockBatchClassifyDefault, getContinuity: vi.fn(async () => ({ weekStart: '', weekCount: 0, weekTarget: 7, consecutiveCompleteWeeks: 0, showRecoveryToast: false, lastNoteAt: null })), countsByStatus: vi.fn(async () => ({ active: 0, completed: 0, trashed: 0 })), getSettings: vi.fn(async () => ({ ai_enabled: true })), getPendingCount: vi.fn(async () => 0), getOllamaStatus: vi.fn(async () => ({ ok: true })), getTodayCount: vi.fn(async () => 0), getTrashCount: vi.fn(async () => 0), listExpired: vi.fn(async () => []), getFailedCount: vi.fn(async () => 0), listRecallCandidate: vi.fn(async () => null), listByStatus: vi.fn(async () => []) }, notebookApi: { moveNote: mockMoveNote, list: vi.fn(async () => []) } })); import { BatchMoveModal } from '../../src/renderer/inbox/components/BatchMoveModal'; import { useInbox } from '../../src/renderer/inbox/store'; const baseNote = { id: 'n1', rawText: '테스트 노트 내용', aiTitle: 'AI 제목', aiSummary: null, aiStatus: 'done' as const, aiError: null, aiProvider: null, aiGeneratedAt: null, titleEditedByUser: false, summaryEditedByUser: false, userIntent: null, intentPromptedAt: null, dueDate: null, dueDateEditedByUser: false, deletedAt: null, lastRecalledAt: null, recallDismissedAt: null, status: 'active' as const, statusChangedAt: null, moveReason: null, createdAt: '2026-01-01T00:00:00Z', updatedAt: '2026-01-01T00:00:00Z', tags: [], media: [], notebookId: 'nb-default' }; describe('BatchMoveModal', () => { beforeEach(() => { vi.clearAllMocks(); cleanup(); useInbox.setState({ batchClassifyResult: null, batchClassifying: false, notes: [], notebooks: [] } as Partial>); }); it('batchClassifyResult null 시 null 반환', () => { useInbox.setState({ batchClassifyResult: null } as Partial>); const { container } = render(); expect(container).toBeEmptyDOMElement(); }); it('actionable 0건 시 "찾지 못했어요" 메시지 표시', () => { useInbox.setState({ batchClassifyResult: { assignments: [{ noteId: 'n1', notebookId: null, notebookName: null }] } } as Partial>); render(); expect(screen.getByText(/찾지 못했어요/)).toBeInTheDocument(); }); it('actionable 노트 표시 + checkbox 기본 체크', () => { useInbox.setState({ batchClassifyResult: { assignments: [ { noteId: 'n1', notebookId: 'nb1', notebookName: '업무' } ] }, notes: [{ ...baseNote, id: 'n1', aiTitle: 'AI 제목' }] } as Partial>); render(); expect(screen.getByText('AI 제목')).toBeInTheDocument(); expect(screen.getByText(/→ 업무/)).toBeInTheDocument(); const checkbox = screen.getByRole('checkbox') as HTMLInputElement; expect(checkbox.checked).toBe(true); }); it('checkbox toggle 시 selectedIds 변경 (체크 해제 후 확인 버튼 비활성)', () => { useInbox.setState({ batchClassifyResult: { assignments: [ { noteId: 'n1', notebookId: 'nb1', notebookName: '업무' } ] }, notes: [{ ...baseNote, id: 'n1' }] } as Partial>); render(); const checkbox = screen.getByRole('checkbox') as HTMLInputElement; expect(checkbox.checked).toBe(true); fireEvent.click(checkbox); expect(checkbox.checked).toBe(false); const confirmBtn = screen.getByRole('button', { name: /건 이동/ }); expect(confirmBtn).toBeDisabled(); }); it('확인 클릭 → acceptBatchAssignments 호출 후 modal 닫힘', async () => { const mockAccept = vi.fn(async () => {}); useInbox.setState({ batchClassifyResult: { assignments: [ { noteId: 'n1', notebookId: 'nb1', notebookName: '업무' } ] }, notes: [{ ...baseNote, id: 'n1' }], acceptBatchAssignments: mockAccept } as Partial>); render(); const confirmBtn = screen.getByRole('button', { name: /건 이동/ }); fireEvent.click(confirmBtn); await waitFor(() => { expect(mockAccept).toHaveBeenCalledWith([{ noteId: 'n1', notebookId: 'nb1' }]); }); }); });