전수 audit 후 핵심 root fix 3 + edge cases 5: ROOT - inbox:set-status IPC 가 pushNoteUpdated emit (이전엔 stale → 호출처별 refreshMeta 필요) - upsertNote 가 current view status 인식 (이전엔 잘못된 status 노트 잔류) - store async 함수 try/catch (이전엔 IPC fail 시 무한 loading) EDGE - restoreNote 가 status='active' 도 갱신 - upsertNote trash 판정 deletedAt → status='trashed' - Modal Escape dismiss 통일 (5개 modal) - OnboardingWizard IPC fail fallback (try/catch + skip) - MoveStatusModal overlay 클릭 close Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
192 lines
5.9 KiB
TypeScript
192 lines
5.9 KiB
TypeScript
// @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=""
|
|
currentStatus="active"
|
|
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=""
|
|
currentStatus="active"
|
|
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=""
|
|
currentStatus="active"
|
|
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('currentStatus=completed → Inbox/보관/휴지통 노출, 완료 미노출', () => {
|
|
render(
|
|
<MoveStatusModal
|
|
noteId="n1"
|
|
rawText="t"
|
|
summary=""
|
|
currentStatus="completed"
|
|
onClose={vi.fn()}
|
|
onMoved={vi.fn()}
|
|
/>
|
|
);
|
|
expect(screen.getByRole('button', { name: 'Inbox' })).toBeInTheDocument();
|
|
expect(screen.getByRole('button', { name: '보관' })).toBeInTheDocument();
|
|
expect(screen.getByRole('button', { name: '휴지통' })).toBeInTheDocument();
|
|
expect(screen.queryByRole('button', { name: '완료' })).toBeNull();
|
|
});
|
|
|
|
it('currentStatus=archived → Inbox 버튼 클릭 시 setStatus("active") 호출', async () => {
|
|
const onMoved = vi.fn();
|
|
render(
|
|
<MoveStatusModal
|
|
noteId="n1"
|
|
rawText="t"
|
|
summary=""
|
|
currentStatus="archived"
|
|
onClose={vi.fn()}
|
|
onMoved={onMoved}
|
|
/>
|
|
);
|
|
fireEvent.click(screen.getByRole('button', { name: 'Inbox' }));
|
|
await waitFor(() => {
|
|
expect(mockSetStatus).toHaveBeenCalledWith('n1', 'active', null);
|
|
expect(onMoved).toHaveBeenCalledWith('active', null);
|
|
});
|
|
});
|
|
|
|
it('currentStatus=trashed → Inbox/완료/보관 노출, 휴지통 미노출', () => {
|
|
render(
|
|
<MoveStatusModal
|
|
noteId="n1"
|
|
rawText="t"
|
|
summary=""
|
|
currentStatus="trashed"
|
|
onClose={vi.fn()}
|
|
onMoved={vi.fn()}
|
|
/>
|
|
);
|
|
expect(screen.getByRole('button', { name: 'Inbox' })).toBeInTheDocument();
|
|
expect(screen.getByRole('button', { name: '완료' })).toBeInTheDocument();
|
|
expect(screen.getByRole('button', { name: '보관' })).toBeInTheDocument();
|
|
expect(screen.queryByRole('button', { name: '휴지통' })).toBeNull();
|
|
});
|
|
|
|
it('Escape key → onClose 호출', () => {
|
|
const onClose = vi.fn();
|
|
render(
|
|
<MoveStatusModal
|
|
noteId="n1"
|
|
rawText="t"
|
|
summary=""
|
|
currentStatus="active"
|
|
onClose={onClose}
|
|
onMoved={vi.fn()}
|
|
/>
|
|
);
|
|
fireEvent.keyDown(document, { key: 'Escape' });
|
|
expect(onClose).toHaveBeenCalled();
|
|
});
|
|
|
|
it('overlay 클릭 → onClose, modal body 클릭 → 무반응', () => {
|
|
const onClose = vi.fn();
|
|
render(
|
|
<MoveStatusModal
|
|
noteId="n1"
|
|
rawText="t"
|
|
summary=""
|
|
currentStatus="active"
|
|
onClose={onClose}
|
|
onMoved={vi.fn()}
|
|
/>
|
|
);
|
|
// 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(
|
|
<MoveStatusModal
|
|
noteId="n1"
|
|
rawText="t"
|
|
summary=""
|
|
currentStatus="active"
|
|
onClose={vi.fn()}
|
|
onMoved={onMoved}
|
|
/>
|
|
);
|
|
fireEvent.click(screen.getByRole('button', { name: '보관' }));
|
|
await waitFor(() => expect(mockSetStatus).toHaveBeenCalledWith('n1', 'archived', null));
|
|
});
|
|
});
|