import { fireEvent, render, screen, waitFor, within } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' import WorkMemos from './WorkMemos' import type { Org, WorkMemo } from '../api/client' const mocks = vi.hoisted(() => ({ getMemos: vi.fn(), createMemo: vi.fn(), deleteMemo: vi.fn(), })) vi.mock('../api/client', async (importOriginal) => ({ ...(await importOriginal()), getMemos: mocks.getMemos, createMemo: mocks.createMemo, deleteMemo: mocks.deleteMemo, })) function org(overrides: Partial = {}): Org { return { id: 1, orgNo: '001', orgName: '국제방송교류재단', status: 'ACTIVE', deptName: '데이터정보화팀', managerName: '송민지', managerTitle: '과장', managerPhone: '02-3475-5434', managerEmail: 'ming@arirang.com', channelIdMj: 'chan-mj', channelIdLaw: 'chan-law', ...overrides, } } function memo(overrides: Partial = {}): WorkMemo { return { id: 1, orgId: 1, contactName: '송민지 과장', body: '채널 생성 일정 문의', createdAt: new Date('2026-07-01T10:30:00').getTime(), ...overrides, } } beforeEach(() => { mocks.getMemos.mockReset() mocks.createMemo.mockReset() mocks.deleteMemo.mockReset() }) describe('WorkMemos', () => { it('등록된 메모를 담당자와 본문과 함께 보여준다', async () => { mocks.getMemos.mockResolvedValue([memo()]) render() await waitFor(() => expect(screen.getByRole('listitem')).toBeTruthy()) const item = within(screen.getByRole('listitem')) expect(item.getByText('송민지 과장')).toBeTruthy() expect(item.getByText('채널 생성 일정 문의')).toBeTruthy() }) it('메모가 없으면 안내 문구를 보여준다', async () => { mocks.getMemos.mockResolvedValue([]) render() await waitFor(() => { expect( screen.getByText('등록된 메모가 없습니다. 통화 후 첫 메모를 남겨보세요.'), ).toBeTruthy() }) }) it('담당자 선택에 기관 담당자와 직접 입력이 나오고, 직접 입력을 고르면 이름 입력칸이 나타난다', async () => { mocks.getMemos.mockResolvedValue([]) render() await waitFor(() => expect(mocks.getMemos).toHaveBeenCalled()) expect(screen.getByRole('option', { name: '송민지 과장' })).toBeTruthy() expect(screen.getByRole('option', { name: '직접 입력' })).toBeTruthy() expect(screen.queryByLabelText('담당자 이름')).toBeNull() fireEvent.change(screen.getByLabelText('담당자'), { target: { value: '__custom__' } }) expect(screen.getByLabelText('담당자 이름')).toBeTruthy() }) it('메모 내용이 비어 있으면 등록 버튼이 비활성이고, 입력하면 활성화된다', async () => { mocks.getMemos.mockResolvedValue([]) render() await waitFor(() => expect(mocks.getMemos).toHaveBeenCalled()) const submit = screen.getByRole('button', { name: '등록' }) expect(submit).toBeDisabled() fireEvent.change(screen.getByLabelText('메모 내용'), { target: { value: '통화 내용' } }) expect(submit).not.toBeDisabled() }) it('등록을 누르면 선택된 담당자와 본문으로 createMemo를 호출하고 본문을 비운다', async () => { mocks.getMemos.mockResolvedValue([]) mocks.createMemo.mockResolvedValue(memo({ id: 2, body: '통화 내용' })) render() await waitFor(() => expect(mocks.getMemos).toHaveBeenCalled()) fireEvent.change(screen.getByLabelText('메모 내용'), { target: { value: '통화 내용' } }) fireEvent.click(screen.getByRole('button', { name: '등록' })) await waitFor(() => { expect(mocks.createMemo).toHaveBeenCalledWith(1, { contactName: '송민지 과장', body: '통화 내용', }) }) await waitFor(() => { expect((screen.getByLabelText('메모 내용') as HTMLTextAreaElement).value).toBe('') }) }) it('삭제를 누르면 확인을 물어보고, 확인하면 deleteMemo를 호출한다', async () => { mocks.getMemos.mockResolvedValue([memo()]) mocks.deleteMemo.mockResolvedValue(undefined) const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true) render() await waitFor(() => expect(screen.getByText('채널 생성 일정 문의')).toBeTruthy()) fireEvent.click(screen.getByRole('button', { name: '삭제' })) expect(confirmSpy).toHaveBeenCalledWith('이 메모를 삭제할까요?') await waitFor(() => expect(mocks.deleteMemo).toHaveBeenCalledWith(1, 1)) confirmSpy.mockRestore() }) it('삭제 확인을 취소하면 deleteMemo를 호출하지 않는다', async () => { mocks.getMemos.mockResolvedValue([memo()]) const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(false) render() await waitFor(() => expect(screen.getByText('채널 생성 일정 문의')).toBeTruthy()) fireEvent.click(screen.getByRole('button', { name: '삭제' })) expect(confirmSpy).toHaveBeenCalled() expect(mocks.deleteMemo).not.toHaveBeenCalled() confirmSpy.mockRestore() }) })