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 } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import ContactsPage from './ContactsPage'
import { withCodes } from '../codes/fixtures'
import type { Contact } from '../api/client'
const mocks = vi.hoisted(() => ({
getContacts: vi.fn(),
createContact: vi.fn(),
updateContactInfo: vi.fn(),
deleteContact: vi.fn(),
importMembers: vi.fn(),
}))
vi.mock('../api/client', async (importOriginal) => ({
...(await importOriginal<typeof import('../api/client')>()),
getContacts: mocks.getContacts,
createContact: mocks.createContact,
updateContactInfo: mocks.updateContactInfo,
deleteContact: mocks.deleteContact,
importMembers: mocks.importMembers,
}))
function contact(overrides: Partial<Contact> = {}): 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()
mocks.importMembers.mockReset()
})
describe('ContactsPage', () => {
it('담당자 목록을 구분·성명 등과 함께 보여준다', async () => {
mocks.getContacts.mockResolvedValue([contact()])
render(withCodes(<ContactsPage />))
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(withCodes(<ContactsPage />))
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(withCodes(<ContactsPage />))
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([
contact({ id: 1, name: '송민지', category: 'APPLICANT' }),
contact({ id: 2, name: '홍수행', category: 'OPERATOR', affiliation: '수행사', deptName: '운영팀' }),
])
render(withCodes(<ContactsPage />))
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(withCodes(<ContactsPage />))
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(withCodes(<ContactsPage />))
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(withCodes(<ContactsPage />))
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(withCodes(<ContactsPage />))
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(withCodes(<ContactsPage />))
await waitFor(() => expect(screen.getByText('송민지')).toBeTruthy())
fireEvent.click(screen.getByRole('button', { name: '삭제' }))
expect(confirmSpy).toHaveBeenCalled()
expect(mocks.deleteContact).not.toHaveBeenCalled()
confirmSpy.mockRestore()
})
it('회원명단 업로드 버튼으로 파일을 고르면 importMembers가 호출되고 결과가 표시된다', async () => {
mocks.getContacts.mockResolvedValue([])
mocks.importMembers.mockResolvedValue({ created: 2, updated: 1, skipped: 1, assigned: 1 })
render(withCodes(<ContactsPage />))
await waitFor(() => expect(mocks.getContacts).toHaveBeenCalled())
const file = new File(['dummy'], 'members.xlsx', {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
})
const input = document.querySelector('input[type="file"]') as HTMLInputElement
fireEvent.change(input, { target: { files: [file] } })
await waitFor(() => expect(mocks.importMembers).toHaveBeenCalledWith(file))
expect(
screen.getByText('등록 2 · 갱신 1 · 기관지정 1 · 건너뜀 1'),
).toBeTruthy()
})
it('회원명단 업로드가 실패하면 오류 메시지를 보여준다', async () => {
mocks.getContacts.mockResolvedValue([])
mocks.importMembers.mockRejectedValue(new Error('파일 형식이 올바르지 않습니다.'))
render(withCodes(<ContactsPage />))
await waitFor(() => expect(mocks.getContacts).toHaveBeenCalled())
const file = new File(['dummy'], 'members.xlsx', {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
})
const input = document.querySelector('input[type="file"]') as HTMLInputElement
fireEvent.change(input, { target: { files: [file] } })
await waitFor(() => {
expect(screen.getByText('파일 형식이 올바르지 않습니다.')).toBeTruthy()
})
})
})