import { fireEvent, render, screen, waitFor } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' import OrgDetail from './OrgDetail' import type { Contact, Org } from '../api/client' const mocks = vi.hoisted(() => ({ updateAssignments: vi.fn(), provisionChannels: vi.fn(), getContacts: vi.fn(), })) vi.mock('../api/client', async (importOriginal) => ({ ...(await importOriginal()), updateAssignments: mocks.updateAssignments, provisionChannels: mocks.provisionChannels, getContacts: mocks.getContacts, })) function contact(overrides: Partial = {}): Contact { return { id: 1, category: 'APPLICANT', name: '이름없음', affiliation: null, deptName: null, title: null, phone: null, email: null, ...overrides, } } function org(overrides: Partial = {}): Org { return { id: 1, orgNo: '001', orgName: '국제방송교류재단', status: 'INFO_PENDING', applicant: null, mj: null, itn: null, lawyer: null, lawyerAssignedDate: null, channelIdMj: null, channelIdLaw: null, ...overrides, } } beforeEach(() => { mocks.updateAssignments.mockReset() mocks.provisionChannels.mockReset() mocks.getContacts.mockReset().mockResolvedValue([]) }) describe('OrgDetail', () => { it('정보 대기 상태면 채널 생성 버튼이 비활성이다', () => { render( {}} />) expect(screen.getByRole('button', { name: '채널 생성' })).toBeDisabled() }) it('생성 가능 상태면 채널 생성 버튼이 활성이다', () => { render( {}} />) expect(screen.getByRole('button', { name: '채널 생성' })).toBeEnabled() }) it('배정되지 않은 역할은 미지정으로 표시된다', () => { render( {}} />) expect(screen.getAllByText('미지정')).toHaveLength(4) }) it('배정된 담당자는 요약(이름·부서·연락처)으로 표시된다', () => { render( {}} />, ) expect(screen.getByText('송민지 · 데이터정보화팀 · 02-3475-5434')).toBeTruthy() }) it('선택 버튼을 누르면 담당자 선택 모달이 뜬다', async () => { mocks.getContacts.mockResolvedValue([contact({ id: 9, name: '송민지' })]) render( {}} />) fireEvent.click(screen.getByRole('button', { name: '신청기관 담당자 선택' })) expect(screen.getByText('신청기관 담당자 선택')).toBeTruthy() await waitFor(() => expect(screen.getByText('송민지')).toBeTruthy()) expect(mocks.getContacts).toHaveBeenCalledWith('APPLICANT') }) it('모달에서 담당자를 고르면 updateAssignments가 그 담당자ID로 호출된다', async () => { mocks.getContacts.mockResolvedValue([contact({ id: 9, name: '송민지' })]) mocks.updateAssignments.mockResolvedValue(org({ status: 'READY' })) const onChanged = vi.fn() render() fireEvent.click(screen.getByRole('button', { name: '신청기관 담당자 선택' })) await waitFor(() => expect(screen.getByText('송민지')).toBeTruthy()) fireEvent.click(screen.getByRole('button', { name: '선택' })) await waitFor(() => { expect(mocks.updateAssignments).toHaveBeenCalledWith(1, { applicantContactId: 9, mjContactId: null, itnContactId: null, lawyerContactId: null, lawyerAssignedDate: null, }) expect(onChanged).toHaveBeenCalled() }) }) it('아이티앤 담당자 선택 모달에서 고르면 updateAssignments가 itnContactId로 호출된다', async () => { mocks.getContacts.mockResolvedValue([contact({ id: 11, category: 'ITN', name: '박아이티' })]) mocks.updateAssignments.mockResolvedValue(org()) const onChanged = vi.fn() render() fireEvent.click(screen.getByRole('button', { name: '아이티앤 담당자 선택' })) expect(screen.getByText('아이티앤 담당자 선택')).toBeTruthy() await waitFor(() => expect(screen.getByText('박아이티')).toBeTruthy()) expect(mocks.getContacts).toHaveBeenCalledWith('ITN') fireEvent.click(screen.getByRole('button', { name: '선택' })) await waitFor(() => { expect(mocks.updateAssignments).toHaveBeenCalledWith(1, { applicantContactId: null, mjContactId: null, itnContactId: 11, lawyerContactId: null, lawyerAssignedDate: null, }) expect(onChanged).toHaveBeenCalled() }) }) it('해제 버튼을 누르면 해당 역할만 null로 배정을 해제한다', async () => { mocks.updateAssignments.mockResolvedValue(org()) const onChanged = vi.fn() render( , ) fireEvent.click(screen.getByRole('button', { name: '신청기관 담당자 해제' })) await waitFor(() => { expect(mocks.updateAssignments).toHaveBeenCalledWith(1, { applicantContactId: null, mjContactId: 6, itnContactId: null, lawyerContactId: null, lawyerAssignedDate: null, }) expect(onChanged).toHaveBeenCalled() }) }) it('배정이 없는 역할에는 해제 버튼이 없다', () => { render( {}} />) expect(screen.queryByRole('button', { name: '신청기관 담당자 해제' })).toBeNull() }) it('담당 변호사 배정일을 바꾸면 updateAssignments가 호출된다', async () => { mocks.updateAssignments.mockResolvedValue(org()) const onChanged = vi.fn() render( , ) fireEvent.change(screen.getByLabelText('배정일'), { target: { value: '2026-07-21' } }) await waitFor(() => { expect(mocks.updateAssignments).toHaveBeenCalledWith(1, { applicantContactId: null, mjContactId: null, itnContactId: null, lawyerContactId: 7, lawyerAssignedDate: '2026-07-21', }) expect(onChanged).toHaveBeenCalled() }) }) it('채널 생성은 확인 다이얼로그에 만들어질 채널 2개를 보여준다', () => { render( {}} />) fireEvent.click(screen.getByRole('button', { name: '채널 생성' })) expect(screen.getByText('001_국제방송교류재단 (문정원)')).toBeTruthy() expect(screen.getByText('001_국제방송교류재단 (법률검토)')).toBeTruthy() expect(mocks.provisionChannels).not.toHaveBeenCalled() }) it('확인을 눌러야 채널 생성 API가 호출된다', async () => { mocks.provisionChannels.mockResolvedValue({ mj: 'CREATED', law: 'CREATED', message: '' }) render( {}} />) fireEvent.click(screen.getByRole('button', { name: '채널 생성' })) fireEvent.click(screen.getByRole('button', { name: '생성' })) await waitFor(() => expect(mocks.provisionChannels).toHaveBeenCalledWith(1)) }) it('부분 실패하면 재시도 안내를 보여준다', async () => { mocks.provisionChannels.mockResolvedValue({ mj: 'CREATED', law: 'FAILED', message: '[법률검토] 서버 오류', }) render( {}} />) fireEvent.click(screen.getByRole('button', { name: '채널 생성' })) fireEvent.click(screen.getByRole('button', { name: '생성' })) await waitFor(() => { expect(screen.getByText(/법률검토 채널 생성에 실패/)).toBeTruthy() expect(screen.getByText(/다시 눌러도 안전/)).toBeTruthy() }) }) it('양쪽 다 실패하면 문정원 채널이 살아있다는 잘못된 안내를 보여주지 않는다', async () => { mocks.provisionChannels.mockResolvedValue({ mj: 'FAILED', law: 'FAILED', message: '[문정원] 서버 오류\n[법률검토] 서버 오류', }) render( {}} />) fireEvent.click(screen.getByRole('button', { name: '채널 생성' })) fireEvent.click(screen.getByRole('button', { name: '생성' })) await waitFor(() => { expect(screen.getByText(/문정원 채널.*법률검토 채널.*모두 생성에 실패/)).toBeTruthy() }) expect(screen.queryByText(/문정원 채널은 그대로 남아 있으니/)).toBeNull() }) it('문정원만 실패하면 법률검토 채널이 살아있다는 안내를 보여준다', async () => { mocks.provisionChannels.mockResolvedValue({ mj: 'FAILED', law: 'CREATED', message: '[문정원] 서버 오류', }) render( {}} />) fireEvent.click(screen.getByRole('button', { name: '채널 생성' })) fireEvent.click(screen.getByRole('button', { name: '생성' })) await waitFor(() => { expect(screen.getByText(/문정원 채널 생성에 실패/)).toBeTruthy() expect(screen.getByText(/법률검토 채널은 그대로 남아 있으니/)).toBeTruthy() }) }) it('채널 생성이 진행 중이면 생성 버튼과 채널 생성 버튼이 비활성화된다', async () => { let resolvePromise: (value: { mj: string; law: string; message: string }) => void = () => {} mocks.provisionChannels.mockReturnValue( new Promise((resolve) => { resolvePromise = resolve }), ) render( {}} />) fireEvent.click(screen.getByRole('button', { name: '채널 생성' })) fireEvent.click(screen.getByRole('button', { name: '생성' })) await waitFor(() => { expect(screen.getByRole('button', { name: '생성' })).toBeDisabled() }) expect(screen.getByRole('button', { name: '채널 생성' })).toBeDisabled() resolvePromise({ mj: 'CREATED', law: 'CREATED', message: '' }) await waitFor(() => { expect(screen.getByRole('button', { name: '채널 생성' })).toBeEnabled() }) }) it('reload로 org 객체가 새로 만들어져도 같은 기관이면 결과가 지워지지 않는다', async () => { mocks.provisionChannels.mockResolvedValue({ mj: 'CREATED', law: 'FAILED', message: '[법률검토] 서버 오류', }) const { rerender } = render( {}} />) fireEvent.click(screen.getByRole('button', { name: '채널 생성' })) fireEvent.click(screen.getByRole('button', { name: '생성' })) await waitFor(() => { expect(screen.getByText(/법률검토 채널 생성에 실패/)).toBeTruthy() }) // App.reload()가 만드는, id는 같지만 identity가 다른 새 객체를 흉내낸다. rerender( {}} />, ) expect(screen.getByText(/법률검토 채널 생성에 실패/)).toBeTruthy() }) it('다른 기관으로 전환하면 이전 결과가 초기화된다', async () => { mocks.provisionChannels.mockResolvedValue({ mj: 'CREATED', law: 'FAILED', message: '[법률검토] 서버 오류', }) const { rerender } = render( {}} />) fireEvent.click(screen.getByRole('button', { name: '채널 생성' })) fireEvent.click(screen.getByRole('button', { name: '생성' })) await waitFor(() => { expect(screen.getByText(/법률검토 채널 생성에 실패/)).toBeTruthy() }) rerender( {}} />) expect(screen.queryByText(/법률검토 채널 생성에 실패/)).toBeNull() }) })