import { fireEvent, render, screen, waitFor } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' import ContactsPage from './ContactsPage' import type { Contact } from '../api/client' const mocks = vi.hoisted(() => ({ getContacts: vi.fn(), createContact: vi.fn(), updateContactInfo: vi.fn(), deleteContact: vi.fn(), })) vi.mock('../api/client', async (importOriginal) => ({ ...(await importOriginal()), getContacts: mocks.getContacts, createContact: mocks.createContact, updateContactInfo: mocks.updateContactInfo, deleteContact: mocks.deleteContact, })) function contact(overrides: Partial = {}): Contact { return { id: 1, category: 'APPLICANT', name: '송민지', affiliation: '국제방송교류재단', deptName: '데이터정보화팀', title: '과장', phone: '02-3475-5434', email: 'ming@arirang.com', ...overrides, } } beforeEach(() => { mocks.getContacts.mockReset() mocks.createContact.mockReset() mocks.updateContactInfo.mockReset() mocks.deleteContact.mockReset() }) describe('ContactsPage', () => { it('담당자 목록을 구분·성명 등과 함께 보여준다', async () => { mocks.getContacts.mockResolvedValue([contact()]) render() await waitFor(() => expect(screen.getByText('송민지')).toBeTruthy()) expect(screen.getByRole('cell', { name: '신청기관' })).toBeTruthy() expect(screen.getByText('국제방송교류재단')).toBeTruthy() expect(screen.getByText('데이터정보화팀')).toBeTruthy() expect(screen.getByText('02-3475-5434')).toBeTruthy() expect(screen.getByText('ming@arirang.com')).toBeTruthy() }) it('담당자가 없으면 안내 문구를 보여준다', async () => { mocks.getContacts.mockResolvedValue([]) render() await waitFor(() => { expect(screen.getByText('등록된 담당자가 없습니다.')).toBeTruthy() }) }) it('필터 칩을 누르면 해당 구분만 보인다', async () => { mocks.getContacts.mockResolvedValue([ contact({ id: 1, name: '송민지', category: 'APPLICANT' }), contact({ id: 2, name: '이변호', category: 'LAWYER', affiliation: null, deptName: null }), ]) render() await waitFor(() => expect(screen.getByText('송민지')).toBeTruthy()) fireEvent.click(screen.getByRole('button', { name: '변호사' })) expect(screen.queryByText('송민지')).toBeNull() expect(screen.getByText('이변호')).toBeTruthy() }) it('담당자 추가를 누르면 모달이 뜨고, 구분과 성명을 채워야 저장이 활성화된다', async () => { mocks.getContacts.mockResolvedValue([]) render() await waitFor(() => expect(mocks.getContacts).toHaveBeenCalled()) fireEvent.click(screen.getByRole('button', { name: '담당자 추가' })) expect(screen.getByRole('heading', { name: '담당자 추가' })).toBeTruthy() const save = screen.getByRole('button', { name: '저장' }) expect(save).toBeDisabled() fireEvent.change(screen.getByLabelText('성명'), { target: { value: '홍길동' } }) expect(save).toBeEnabled() }) it('저장하면 createContact가 폼 값으로 호출되고, 이후 목록을 다시 불러온다', async () => { mocks.getContacts.mockResolvedValueOnce([]).mockResolvedValueOnce([contact({ name: '홍길동' })]) mocks.createContact.mockResolvedValue(contact({ name: '홍길동' })) render() await waitFor(() => expect(mocks.getContacts).toHaveBeenCalledTimes(1)) fireEvent.click(screen.getByRole('button', { name: '담당자 추가' })) fireEvent.change(screen.getByLabelText('성명'), { target: { value: '홍길동' } }) fireEvent.change(screen.getByLabelText('구분'), { target: { value: 'MJ' } }) fireEvent.click(screen.getByRole('button', { name: '저장' })) await waitFor(() => { expect(mocks.createContact).toHaveBeenCalledWith({ category: 'MJ', name: '홍길동', affiliation: '', deptName: '', title: '', phone: '', email: '', }) }) await waitFor(() => expect(mocks.getContacts).toHaveBeenCalledTimes(2)) }) it('수정 버튼을 누르면 기존 값이 채워진 모달이 뜨고, 저장하면 updateContactInfo가 호출된다', async () => { mocks.getContacts.mockResolvedValue([contact()]) mocks.updateContactInfo.mockResolvedValue(contact({ phone: '02-0000-0000' })) render() await waitFor(() => expect(screen.getByText('송민지')).toBeTruthy()) fireEvent.click(screen.getByRole('button', { name: '수정' })) expect(screen.getByText('담당자 수정')).toBeTruthy() expect(screen.getByLabelText('성명')).toHaveValue('송민지') fireEvent.change(screen.getByLabelText('연락처'), { target: { value: '02-0000-0000' } }) fireEvent.click(screen.getByRole('button', { name: '저장' })) await waitFor(() => { expect(mocks.updateContactInfo).toHaveBeenCalledWith( 1, expect.objectContaining({ phone: '02-0000-0000' }), ) }) }) it('삭제를 누르면 확인을 물어보고, 확인하면 deleteContact를 호출한다', async () => { mocks.getContacts.mockResolvedValue([contact()]) mocks.deleteContact.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.deleteContact).toHaveBeenCalledWith(1)) confirmSpy.mockRestore() }) it('삭제 확인을 취소하면 deleteContact를 호출하지 않는다', async () => { mocks.getContacts.mockResolvedValue([contact()]) 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.deleteContact).not.toHaveBeenCalled() confirmSpy.mockRestore() }) })