import { act, render, screen, waitFor } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' import ChannelPosts from './ChannelPosts' import type { Org, PostView } from '../api/client' const mocks = vi.hoisted(() => ({ getPosts: vi.fn(), })) vi.mock('../api/client', async (importOriginal) => ({ ...(await importOriginal()), getPosts: mocks.getPosts, })) function org(overrides: Partial = {}): Org { return { id: 1, orgNo: '001', orgName: '국제방송교류재단', status: 'ACTIVE', deptName: null, managerName: null, managerTitle: null, managerPhone: null, managerEmail: null, channelIdMj: 'chan-mj', channelIdLaw: 'chan-law', ...overrides, } } function post(overrides: Partial = {}): PostView { return { id: 'p1', user: '송민지', message: '안녕하세요', createAt: Date.now(), system: false, files: [], ...overrides, } } beforeEach(() => { mocks.getPosts.mockReset() vi.useRealTimers() }) describe('ChannelPosts', () => { it('메시지를 작성자와 본문과 함께 보여준다', async () => { mocks.getPosts.mockResolvedValue([post()]) render() await waitFor(() => { expect(screen.getByText('송민지')).toBeTruthy() expect(screen.getByText('안녕하세요')).toBeTruthy() }) }) it('시스템 메시지는 회색 중앙 정렬 한 줄로 보여준다', async () => { mocks.getPosts.mockResolvedValue([post({ system: true, message: '님이 입장했습니다.' })]) render() await waitFor(() => { const line = screen.getByText('님이 입장했습니다.') expect(line.className).toContain('text-gray-400') expect(line.className).toContain('text-center') }) }) it('3초마다 폴링해서 게시글을 다시 조회한다', async () => { vi.useFakeTimers() mocks.getPosts.mockResolvedValue([]) render() await act(async () => { await vi.advanceTimersByTimeAsync(0) }) expect(mocks.getPosts).toHaveBeenCalledTimes(1) await act(async () => { await vi.advanceTimersByTimeAsync(3000) }) expect(mocks.getPosts).toHaveBeenCalledTimes(2) vi.useRealTimers() }) it('양쪽 채널이 모두 없으면 안내 문구를 보여주고 API를 호출하지 않는다', () => { render() expect(screen.getByText('채널이 아직 생성되지 않았습니다.')).toBeTruthy() expect(mocks.getPosts).not.toHaveBeenCalled() }) it('법률검토 채널이 없으면 법률검토 칩이 비활성화된다', async () => { mocks.getPosts.mockResolvedValue([]) render() expect(screen.getByRole('button', { name: '법률검토' })).toBeDisabled() await waitFor(() => expect(mocks.getPosts).toHaveBeenCalled()) }) it('같은 사람이 5분 안에 연달아 쓴 메시지는 이름과 아바타를 한 번만 보여준다', async () => { const now = Date.now() mocks.getPosts.mockResolvedValue([ post({ id: 'p1', createAt: now }), post({ id: 'p2', message: '이어서 씁니다', createAt: now + 1000 }), ]) render() await waitFor(() => expect(screen.getByText('이어서 씁니다')).toBeTruthy()) expect(screen.getAllByText('송민지')).toHaveLength(1) }) it('첨부파일 칩의 링크는 다운로드 엔드포인트를 가리킨다', async () => { mocks.getPosts.mockResolvedValue([ post({ files: [{ id: 'f1', name: '보고서.pdf', size: 1200 }] }), ]) render() await waitFor(() => { const link = screen.getByText('보고서.pdf').closest('a') expect(link).toHaveAttribute('href', '/api/files/f1') }) }) })