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 WorkMemos from './WorkMemos'
import type { Contact, Org, WorkMemo } from '../api/client'
const mocks = vi.hoisted(() => ({
getMemos: vi.fn(),
createMemo: vi.fn(),
updateMemo: vi.fn(),
deleteMemo: vi.fn(),
}))
vi.mock('../api/client', async (importOriginal) => ({
...(await importOriginal<typeof import('../api/client')>()),
getMemos: mocks.getMemos,
createMemo: mocks.createMemo,
updateMemo: mocks.updateMemo,
deleteMemo: mocks.deleteMemo,
}))
function contact(overrides: Partial<Contact> = {}): Contact {
return {
id: 1,
category: 'APPLICANT',
name: '이름없음',
affiliation: null,
deptName: null,
title: null,
phone: null,
email: null,
...overrides,
}
}
function org(overrides: Partial<Org> = {}): Org {
return {
id: 1,
orgNo: '001',
orgName: '국제방송교류재단',
status: 'ACTIVE',
applicant: contact({
id: 1,
category: 'APPLICANT',
name: '송민지',
affiliation: '국제방송교류재단',
deptName: '데이터정보화팀',
title: '과장',
phone: '02-3475-5434',
email: 'ming@arirang.com',
}),
mj: null,
itn: null,
lawyer: null,
lawyerAssignedDate: null,
channelIdMj: 'chan-mj',
channelIdLaw: 'chan-law',
stage: null,
...overrides,
}
}
function memo(overrides: Partial<WorkMemo> = {}): 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.updateMemo.mockReset()
mocks.deleteMemo.mockReset()
})
describe('WorkMemos', () => {
it('등록된 메모를 담당자와 본문과 함께 보여준다', async () => {
mocks.getMemos.mockResolvedValue([memo()])
render(<WorkMemos org={org()} />)
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(<WorkMemos org={org()} />)
await waitFor(() => {
expect(
screen.getByText('등록된 메모가 없습니다. 통화 후 첫 메모를 남겨보세요.'),
).toBeTruthy()
})
})
it('작성자를 고르는 칸은 없고, 자동 기록 안내만 보인다', async () => {
mocks.getMemos.mockResolvedValue([])
render(<WorkMemos org={org()} />)
await waitFor(() => expect(mocks.getMemos).toHaveBeenCalled())
expect(screen.queryByLabelText('담당자')).toBeNull()
expect(screen.queryByLabelText('담당자 이름')).toBeNull()
expect(screen.getByText('작성자는 로그인한 사용자로 자동 기록됩니다.')).toBeTruthy()
})
it('메모 내용이 비어 있으면 등록 버튼이 비활성이고, 입력하면 활성화된다', async () => {
mocks.getMemos.mockResolvedValue([])
render(<WorkMemos org={org()} />)
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('등록을 누르면 본문만 보내고(작성자는 서버가 기록) 본문을 비운다', async () => {
mocks.getMemos.mockResolvedValue([])
mocks.createMemo.mockResolvedValue(memo({ id: 2, body: '통화 내용', contactName: 'admin' }))
render(<WorkMemos org={org()} />)
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, { body: '통화 내용' })
})
await waitFor(() => {
expect((screen.getByLabelText('메모 내용') as HTMLTextAreaElement).value).toBe('')
})
// 서버가 기록한 작성자가 목록에 그대로 보인다
expect(screen.getByText('admin')).toBeTruthy()
})
it('수정을 누르면 본문을 그 자리에서 고치고 저장하면 updateMemo를 부른다', async () => {
mocks.getMemos.mockResolvedValue([memo({ id: 7, body: '원래 내용' })])
mocks.updateMemo.mockResolvedValue(memo({ id: 7, body: '고친 내용' }))
render(<WorkMemos org={org()} />)
await waitFor(() => expect(screen.getByText('원래 내용')).toBeTruthy())
fireEvent.click(screen.getByRole('button', { name: '수정' }))
fireEvent.change(screen.getByLabelText('메모 수정'), { target: { value: '고친 내용' } })
fireEvent.click(screen.getByRole('button', { name: '저장' }))
await waitFor(() => expect(mocks.updateMemo).toHaveBeenCalledWith(1, 7, { body: '고친 내용' }))
await waitFor(() => expect(screen.getByText('고친 내용')).toBeTruthy())
})
it('수정 중 취소를 누르면 원래 내용으로 돌아간다', async () => {
mocks.getMemos.mockResolvedValue([memo({ id: 7, body: '원래 내용' })])
render(<WorkMemos org={org()} />)
await waitFor(() => expect(screen.getByText('원래 내용')).toBeTruthy())
fireEvent.click(screen.getByRole('button', { name: '수정' }))
fireEvent.change(screen.getByLabelText('메모 수정'), { target: { value: '버릴 내용' } })
fireEvent.click(screen.getByRole('button', { name: '취소' }))
expect(screen.queryByLabelText('메모 수정')).toBeNull()
expect(screen.getByText('원래 내용')).toBeTruthy()
expect(mocks.updateMemo).not.toHaveBeenCalled()
})
it('삭제를 누르면 확인을 물어보고, 확인하면 deleteMemo를 호출한다', async () => {
mocks.getMemos.mockResolvedValue([memo()])
mocks.deleteMemo.mockResolvedValue(undefined)
const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true)
render(<WorkMemos org={org()} />)
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(<WorkMemos org={org()} />)
await waitFor(() => expect(screen.getByText('채널 생성 일정 문의')).toBeTruthy())
fireEvent.click(screen.getByRole('button', { name: '삭제' }))
expect(confirmSpy).toHaveBeenCalled()
expect(mocks.deleteMemo).not.toHaveBeenCalled()
confirmSpy.mockRestore()
})
})