File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import OrgDetail from './OrgDetail'
import type { Org } from '../api/client'
const mocks = vi.hoisted(() => ({
updateContact: vi.fn(),
provisionChannels: vi.fn(),
}))
vi.mock('../api/client', async (importOriginal) => ({
...(await importOriginal<typeof import('../api/client')>()),
updateContact: mocks.updateContact,
provisionChannels: mocks.provisionChannels,
}))
function org(overrides: Partial<Org> = {}): Org {
return {
id: 1,
orgNo: '001',
orgName: '국제방송교류재단',
status: 'INFO_PENDING',
deptName: null,
managerName: null,
managerTitle: null,
managerPhone: null,
managerEmail: null,
mjDeptName: null,
mjManagerName: null,
mjManagerPhone: null,
mjManagerEmail: null,
lawyerName: null,
lawyerPhone: null,
lawyerEmail: null,
lawyerAssignedDate: null,
channelIdMj: null,
channelIdLaw: null,
...overrides,
}
}
/** 세 담당자 그룹은 부서명/연락처/이메일 같은 라벨을 공유하므로, 그룹 제목으로 찾은
* 컨테이너 안에서만 찾도록 스코프를 좁힌다 - 그렇지 않으면 getByLabelText가
* "여러 개 일치"로 실패한다. */
function group(title: string) {
const heading = screen.getByText(title)
return within(heading.closest('div') as HTMLElement)
}
beforeEach(() => {
mocks.updateContact.mockReset()
mocks.provisionChannels.mockReset()
})
describe('OrgDetail', () => {
it('정보 대기 상태면 채널 생성 버튼이 비활성이다', () => {
render(<OrgDetail org={org()} onChanged={() => {}} />)
expect(screen.getByRole('button', { name: '채널 생성' })).toBeDisabled()
})
it('생성 가능 상태면 채널 생성 버튼이 활성이다', () => {
render(<OrgDetail org={org({ status: 'READY' })} onChanged={() => {}} />)
expect(screen.getByRole('button', { name: '채널 생성' })).toBeEnabled()
})
it('담당자 정보를 저장하면 API를 호출한다', async () => {
mocks.updateContact.mockResolvedValue(org({ status: 'READY' }))
const onChanged = vi.fn()
render(<OrgDetail org={org()} onChanged={onChanged} />)
const applicant = group('신청기관 담당자')
fireEvent.change(applicant.getByLabelText('부서명'), { target: { value: '데이터정보화팀' } })
fireEvent.change(applicant.getByLabelText('담당자명'), { target: { value: '송민지' } })
fireEvent.change(applicant.getByLabelText('직급/직함'), { target: { value: '과장' } })
fireEvent.change(applicant.getByLabelText('연락처'), { target: { value: '02-3475-5434' } })
fireEvent.change(applicant.getByLabelText('이메일'), { target: { value: 'ming@arirang.com' } })
fireEvent.click(screen.getByRole('button', { name: '정보 저장' }))
await waitFor(() => {
expect(mocks.updateContact).toHaveBeenCalledWith(1, {
deptName: '데이터정보화팀',
managerName: '송민지',
managerTitle: '과장',
managerPhone: '02-3475-5434',
managerEmail: 'ming@arirang.com',
mjDeptName: '',
mjManagerName: '',
mjManagerPhone: '',
mjManagerEmail: '',
lawyerName: '',
lawyerPhone: '',
lawyerEmail: '',
lawyerAssignedDate: '',
})
expect(onChanged).toHaveBeenCalled()
})
})
it('문정원 담당자와 담당 변호사 정보를 입력하면 한 번의 저장으로 함께 전송된다', async () => {
mocks.updateContact.mockResolvedValue(org({ status: 'READY' }))
const onChanged = vi.fn()
render(<OrgDetail org={org()} onChanged={onChanged} />)
const applicant = group('신청기관 담당자')
fireEvent.change(applicant.getByLabelText('부서명'), { target: { value: '데이터정보화팀' } })
fireEvent.change(applicant.getByLabelText('담당자명'), { target: { value: '송민지' } })
fireEvent.change(applicant.getByLabelText('연락처'), { target: { value: '02-3475-5434' } })
fireEvent.change(applicant.getByLabelText('이메일'), { target: { value: 'ming@arirang.com' } })
const mj = group('문정원 담당자')
fireEvent.change(mj.getByLabelText('부서명'), { target: { value: '문화체육관광부 저작권정책과' } })
fireEvent.change(mj.getByLabelText('담당자명'), { target: { value: '김문정' } })
fireEvent.change(mj.getByLabelText('연락처'), { target: { value: '02-1234-5678' } })
fireEvent.change(mj.getByLabelText('이메일'), { target: { value: 'mj@mcst.go.kr' } })
const lawyer = group('담당 변호사')
fireEvent.change(lawyer.getByLabelText('성명'), { target: { value: '이변호' } })
fireEvent.change(lawyer.getByLabelText('연락처'), { target: { value: '02-9876-5432' } })
fireEvent.change(lawyer.getByLabelText('이메일'), { target: { value: 'lawyer@lawfirm.kr' } })
fireEvent.change(lawyer.getByLabelText('배정일'), { target: { value: '2026-07-21' } })
fireEvent.click(screen.getByRole('button', { name: '정보 저장' }))
await waitFor(() => {
expect(mocks.updateContact).toHaveBeenCalledWith(1, {
deptName: '데이터정보화팀',
managerName: '송민지',
managerTitle: '',
managerPhone: '02-3475-5434',
managerEmail: 'ming@arirang.com',
mjDeptName: '문화체육관광부 저작권정책과',
mjManagerName: '김문정',
mjManagerPhone: '02-1234-5678',
mjManagerEmail: 'mj@mcst.go.kr',
lawyerName: '이변호',
lawyerPhone: '02-9876-5432',
lawyerEmail: 'lawyer@lawfirm.kr',
lawyerAssignedDate: '2026-07-21',
})
expect(onChanged).toHaveBeenCalled()
})
})
it('채널 생성은 확인 다이얼로그에 만들어질 채널 2개를 보여준다', () => {
render(<OrgDetail org={org({ status: 'READY' })} onChanged={() => {}} />)
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(<OrgDetail org={org({ status: 'READY' })} onChanged={() => {}} />)
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(<OrgDetail org={org({ status: 'READY' })} onChanged={() => {}} />)
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(<OrgDetail org={org({ status: 'READY' })} onChanged={() => {}} />)
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(<OrgDetail org={org({ status: 'READY' })} onChanged={() => {}} />)
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(<OrgDetail org={org({ status: 'READY' })} onChanged={() => {}} />)
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('필수 항목이 비어있으면 정보 저장 버튼이 비활성화된다', () => {
render(<OrgDetail org={org()} onChanged={() => {}} />)
expect(screen.getByRole('button', { name: '정보 저장' })).toBeDisabled()
})
it('직급/직함이 비어있어도 나머지 필수 항목만 채우면 정보 저장 버튼이 활성화된다', () => {
render(<OrgDetail org={org()} onChanged={() => {}} />)
const applicant = group('신청기관 담당자')
fireEvent.change(applicant.getByLabelText('부서명'), { target: { value: '데이터정보화팀' } })
fireEvent.change(applicant.getByLabelText('담당자명'), { target: { value: '송민지' } })
fireEvent.change(applicant.getByLabelText('연락처'), { target: { value: '02-3475-5434' } })
fireEvent.change(applicant.getByLabelText('이메일'), { target: { value: 'ming@arirang.com' } })
expect(screen.getByRole('button', { name: '정보 저장' })).toBeEnabled()
})
it('문정원 담당자와 담당 변호사 항목을 채워도 신청기관 필수 항목이 비어있으면 저장 버튼은 비활성이다', () => {
render(<OrgDetail org={org()} onChanged={() => {}} />)
const mj = group('문정원 담당자')
fireEvent.change(mj.getByLabelText('담당자명'), { target: { value: '김문정' } })
const lawyer = group('담당 변호사')
fireEvent.change(lawyer.getByLabelText('성명'), { target: { value: '이변호' } })
expect(screen.getByRole('button', { name: '정보 저장' })).toBeDisabled()
})
it('reload로 org 객체가 새로 만들어져도 같은 기관이면 결과가 지워지지 않는다', async () => {
mocks.provisionChannels.mockResolvedValue({
mj: 'CREATED',
law: 'FAILED',
message: '[법률검토] 서버 오류',
})
const { rerender } = render(<OrgDetail org={org({ status: 'READY' })} onChanged={() => {}} />)
fireEvent.click(screen.getByRole('button', { name: '채널 생성' }))
fireEvent.click(screen.getByRole('button', { name: '생성' }))
await waitFor(() => {
expect(screen.getByText(/법률검토 채널 생성에 실패/)).toBeTruthy()
})
// App.reload()가 만드는, id는 같지만 identity가 다른 새 객체를 흉내낸다.
rerender(
<OrgDetail
org={org({ status: 'PARTIAL', channelIdMj: 'chan-mj' })}
onChanged={() => {}}
/>,
)
expect(screen.getByText(/법률검토 채널 생성에 실패/)).toBeTruthy()
})
it('다른 기관으로 전환하면 이전 결과와 입력값이 초기화된다', async () => {
mocks.provisionChannels.mockResolvedValue({
mj: 'CREATED',
law: 'FAILED',
message: '[법률검토] 서버 오류',
})
const { rerender } = render(<OrgDetail org={org({ status: 'READY' })} onChanged={() => {}} />)
fireEvent.click(screen.getByRole('button', { name: '채널 생성' }))
fireEvent.click(screen.getByRole('button', { name: '생성' }))
await waitFor(() => {
expect(screen.getByText(/법률검토 채널 생성에 실패/)).toBeTruthy()
})
rerender(<OrgDetail org={org({ id: 2, status: 'READY' })} onChanged={() => {}} />)
expect(screen.queryByText(/법률검토 채널 생성에 실패/)).toBeNull()
})
})