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 Dashboard from './Dashboard'
import { withCodes } from '../codes/fixtures'
import type { DashboardData, DashboardOrgRow } from '../api/client'
const mocks = vi.hoisted(() => ({
getDashboard: vi.fn(),
updateStage: vi.fn(),
}))
vi.mock('../api/client', async (importOriginal) => ({
...(await importOriginal<typeof import('../api/client')>()),
getDashboard: mocks.getDashboard,
updateStage: mocks.updateStage,
}))
const DAY = 24 * 60 * 60 * 1000
function row(overrides: Partial<DashboardOrgRow> = {}): DashboardOrgRow {
return {
id: 1,
orgNo: '001',
orgName: '한국출판문화산업진흥원',
stage: 5,
hasChannel: true,
lawyerName: '전세준',
mjName: '이연경',
lawyerAssignedDate: '2026-06-19',
reviewTotal: 392,
reviewDone: 78,
processTotal: 0,
processDone: 0,
stageChangedAt: Date.now() - 3 * DAY,
lastChangedAt: Date.now() - 3 * DAY,
reCount: 0,
...overrides,
}
}
function data(rows: DashboardOrgRow[], summary: Partial<DashboardData['summary']> = {}): DashboardData {
return {
summary: {
applied: rows.length,
docSubmitted: 0,
reviewTotal: 0,
reviewDone: 0,
processTotal: 0,
processDone: 0,
unresolvedRe: 0,
reportWriting: 0,
completed: 0,
...summary,
},
orgs: rows,
}
}
function renderDashboard(onOpenOrg = vi.fn(), onOpenOrgList = vi.fn()) {
render(withCodes(<Dashboard onOpenOrg={onOpenOrg} onOpenOrgList={onOpenOrgList} />))
return { onOpenOrg, onOpenOrgList }
}
/** 기관명은 칸반 카드와 표에 동시에 나오므로, 표를 볼 때는 항상 표 안에서만 찾는다. */
function table() {
return within(screen.getByTestId('recent-table'))
}
describe('Dashboard', () => {
beforeEach(() => {
vi.clearAllMocks()
mocks.getDashboard.mockResolvedValue(data([row()]))
mocks.updateStage.mockResolvedValue({})
})
it('KPI 타일에 서버가 준 집계를 그대로 보여준다', async () => {
mocks.getDashboard.mockResolvedValue(
data([row()], {
applied: 50,
docSubmitted: 9,
reviewTotal: 2012,
reviewDone: 503,
processTotal: 197,
processDone: 0,
completed: 1,
}),
)
renderDashboard()
expect(await screen.findByText('503 / 2,012')).toBeTruthy()
expect(screen.getByText('0 / 197')).toBeTruthy()
// 권리확인 진척률
expect(screen.getByText('25%')).toBeTruthy()
})
it('채널이 없는 기관은 단계 미지정 컬럼에 담긴다', async () => {
mocks.getDashboard.mockResolvedValue(
data([row({ id: 1, orgName: '채널없는기관', hasChannel: false, stage: null })]),
)
renderDashboard()
const column = await screen.findByTestId('column-unassigned')
expect(column.textContent).toContain('채널없는기관')
expect(column.textContent).toContain('단계 미지정')
})
it('단계 미지정 카드를 끌면 이동하지 않고 채널 생성 경고창이 뜬다', async () => {
mocks.getDashboard.mockResolvedValue(
data([row({ orgName: '채널없는기관', hasChannel: false, stage: null })]),
)
renderDashboard()
const column = await screen.findByTestId('column-unassigned')
const card = within(column).getByText('채널없는기관')
fireEvent.dragStart(card.parentElement!, {
dataTransfer: { setData: vi.fn(), getData: vi.fn() },
})
expect(await screen.findByText('채널을 먼저 생성해 주세요')).toBeTruthy()
expect(mocks.updateStage).not.toHaveBeenCalled()
})
it('채널이 있는 카드를 다른 컬럼에 떨어뜨리면 단계가 바뀐다', async () => {
renderDashboard()
await screen.findByTestId('recent-table')
const target = screen.getByTestId('column-6')
fireEvent.drop(target, {
dataTransfer: { getData: () => '1', setData: vi.fn() },
})
await waitFor(() => expect(mocks.updateStage).toHaveBeenCalledWith(1, 6))
})
it('같은 컬럼에 떨어뜨리면 아무 것도 하지 않는다', async () => {
renderDashboard()
await screen.findByTestId('recent-table')
fireEvent.drop(screen.getByTestId('column-5'), {
dataTransfer: { getData: () => '1', setData: vi.fn() },
})
await waitFor(() => expect(mocks.getDashboard).toHaveBeenCalledTimes(1))
expect(mocks.updateStage).not.toHaveBeenCalled()
})
it('단계 미지정 컬럼에는 떨어뜨려도 단계가 바뀌지 않는다', async () => {
renderDashboard()
await screen.findByTestId('recent-table')
fireEvent.drop(screen.getByTestId('column-unassigned'), {
dataTransfer: { getData: () => '1', setData: vi.fn() },
})
await waitFor(() => expect(mocks.updateStage).not.toHaveBeenCalled())
})
it('카드에 현재 단계 진입 후 경과일을 D+n으로 보여준다', async () => {
mocks.getDashboard.mockResolvedValue(
data([row({ stageChangedAt: Date.now() - 30 * DAY })]),
)
renderDashboard()
expect(await screen.findByText('D+30')).toBeTruthy()
})
it('최근 진행 기관 표는 최근 변경순으로 정렬한다', async () => {
mocks.getDashboard.mockResolvedValue(
data([
row({ id: 1, orgNo: '001', orgName: '오래된기관', lastChangedAt: Date.now() - 10 * DAY }),
row({ id: 2, orgNo: '002', orgName: '최근기관', lastChangedAt: Date.now() }),
]),
)
renderDashboard()
await screen.findByTestId('recent-table')
const names = table().getAllByRole('row').slice(1).map((tr) => tr.children[1].textContent)
expect(names).toEqual(['최근기관', '오래된기관'])
})
it('표는 8개까지만 보여주고 더보기로 늘린다', async () => {
const rows = Array.from({ length: 10 }, (_, i) =>
row({ id: i + 1, orgNo: String(i + 1).padStart(3, '0'), orgName: `기관${i + 1}` }),
)
mocks.getDashboard.mockResolvedValue(data(rows))
renderDashboard()
await screen.findByTestId('recent-table')
expect(table().getAllByRole('row')).toHaveLength(9) // 헤더 1 + 8
fireEvent.click(screen.getByText('더보기 (2개 남음)'))
expect(table().getAllByRole('row')).toHaveLength(11)
})
it('기관명 검색으로 표를 거른다', async () => {
mocks.getDashboard.mockResolvedValue(
data([
row({ id: 1, orgNo: '001', orgName: '한국출판문화산업진흥원' }),
row({ id: 2, orgNo: '002', orgName: '세종학당재단' }),
]),
)
renderDashboard()
await screen.findByTestId('recent-table')
fireEvent.change(screen.getByLabelText('기관명/기관코드 검색'), { target: { value: '세종' } })
expect(table().queryByText('한국출판문화산업진흥원')).toBeNull()
expect(table().getByText('세종학당재단')).toBeTruthy()
})
it('표에서 기관을 클릭하면 기관관리로 넘긴다', async () => {
const onOpenOrg = vi.fn()
renderDashboard(onOpenOrg)
await screen.findByTestId('recent-table')
fireEvent.click(table().getByText('한국출판문화산업진흥원'))
expect(onOpenOrg).toHaveBeenCalledWith(1)
})
it('하단 전체 보기는 사업관리로 넘긴다', async () => {
const onOpenOrgList = vi.fn()
renderDashboard(vi.fn(), onOpenOrgList)
fireEvent.click(await screen.findByText('사업관리에서 전체 보기 →'))
expect(onOpenOrgList).toHaveBeenCalled()
})
})