import { fireEvent, render, screen, waitFor } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' import Timeline from './Timeline' import type { Org, TimelineEvent } from '../api/client' const mocks = vi.hoisted(() => ({ getTimeline: vi.fn(), })) vi.mock('../api/client', async (importOriginal) => ({ ...(await importOriginal()), getTimeline: mocks.getTimeline, })) function org(overrides: Partial = {}): Org { return { id: 1, orgNo: '001', orgName: '국제방송교류재단', status: 'ACTIVE', applicant: null, mj: null, itn: null, lawyer: null, lawyerAssignedDate: null, channelIdMj: 'chan-mj', channelIdLaw: 'chan-law', stage: 5, ...overrides, } } function event(overrides: Partial = {}): TimelineEvent { return { type: 'STAGE', at: Date.now(), title: '1', detail: null, ...overrides, } } beforeEach(() => { mocks.getTimeline.mockReset() }) describe('Timeline', () => { it('불러오는 동안 로딩 문구를 보여준다', () => { mocks.getTimeline.mockReturnValue(new Promise(() => {})) render() expect(screen.getByText('불러오는 중…')).toBeTruthy() }) it('기록이 없으면 빈 상태 문구를 보여준다', async () => { mocks.getTimeline.mockResolvedValue([]) render() await waitFor(() => expect( screen.getByText('아직 기록이 없습니다. 채널을 만들고 업무를 진행하면 여기에 쌓입니다.'), ).toBeTruthy(), ) }) it('세 종류 이벤트를 종류별 라벨과 함께 날짜로 묶어서 보여준다', async () => { const day1 = new Date('2026-07-20T09:00:00').getTime() const day2Morning = new Date('2026-07-21T08:30:00').getTime() const day2Afternoon = new Date('2026-07-21T15:00:00').getTime() mocks.getTimeline.mockResolvedValue([ event({ type: 'FILE', at: day2Afternoon, title: '보고서.pdf', detail: '문정원 · 박아이티' }), event({ type: 'MEMO', at: day2Morning, title: '송민지', detail: '전화 통화 내용' }), event({ type: 'STAGE', at: day1, title: '4', detail: null }), ]) render() await waitFor(() => expect(screen.getByText('보고서.pdf')).toBeTruthy()) expect(screen.getAllByText('자료 등록')).toHaveLength(1) expect(screen.getByText('문정원 · 박아이티')).toBeTruthy() expect(screen.getByText('업무메모')).toBeTruthy() expect(screen.getByText('송민지')).toBeTruthy() expect(screen.getByText('전화 통화 내용')).toBeTruthy() expect(screen.getByText('단계 변경')).toBeTruthy() expect(screen.getByText('4. 변호사 배당 단계로 진행')).toBeTruthy() // 날짜 그룹 헤더 2개(2026-07-21, 2026-07-20)가 최신순으로 보여야 한다. const headers = screen.getAllByText(/2026-07-2[01]/) expect(headers[0].textContent).toContain('2026-07-21') expect(headers[1].textContent).toContain('2026-07-20') }) it('새로고침 버튼을 누르면 다시 조회한다', async () => { mocks.getTimeline.mockResolvedValue([]) render() await waitFor(() => expect(mocks.getTimeline).toHaveBeenCalledTimes(1)) fireEvent.click(screen.getByRole('button', { name: '새로고침' })) await waitFor(() => expect(mocks.getTimeline).toHaveBeenCalledTimes(2)) }) it('조회가 실패하면 오류 메시지를 보여준다', async () => { mocks.getTimeline.mockRejectedValue(new Error('타임라인을 불러오지 못했습니다.')) render() await waitFor(() => expect(screen.getByText('타임라인을 불러오지 못했습니다.')).toBeTruthy()) }) })