import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { renderHook, act } from '@testing-library/react'
import { BaseTestComponent, useTestAnswer } from '../../shared/BaseTestComponent'
// Mock 依賴組件
vi.mock('@/components/review/shared', () => ({
ErrorReportButton: ({ onClick }: any) => (
),
TestHeader: ({ title, cefr }: any) => (
{title}
CEFR: {cefr}
)
}))
describe('BaseTestComponent', () => {
const mockCardData = {
id: 'test-1',
word: 'hello',
definition: 'a greeting',
example: 'Hello world',
translation: '你好',
pronunciation: '/həˈloʊ/',
cefr: 'A1',
synonyms: [],
exampleImage: undefined
}
const mockOnReportError = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
})
describe('基礎渲染', () => {
it('應該正確渲染測驗標題和基本結構', () => {
render(
測試內容
)
expect(screen.getByTestId('test-header')).toBeInTheDocument()
expect(screen.getByText('測試標題')).toBeInTheDocument()
expect(screen.getByText('CEFR: A1')).toBeInTheDocument()
expect(screen.getByTestId('test-content')).toBeInTheDocument()
})
it('應該顯示錯誤回報按鈕', () => {
render(
內容
)
expect(screen.getByTestId('error-report-button')).toBeInTheDocument()
})
it('應該在有說明時顯示說明文字', () => {
render(
內容
)
expect(screen.getByText('這是測試說明')).toBeInTheDocument()
})
})
describe('結果顯示', () => {
it('應該在 showResult 為 true 時顯示結果內容', () => {
render(
測試結果}
onReportError={mockOnReportError}
>
內容
)
expect(screen.getByTestId('result')).toBeInTheDocument()
})
it('應該在 showResult 為 false 時隱藏結果內容', () => {
render(
測試結果}
onReportError={mockOnReportError}
>
內容
)
expect(screen.queryByTestId('result')).not.toBeInTheDocument()
})
})
describe('錯誤回報功能', () => {
it('應該在點擊錯誤回報按鈕時調用 onReportError', async () => {
const user = userEvent.setup()
render(
內容
)
const errorButton = screen.getByTestId('error-report-button')
await user.click(errorButton)
expect(mockOnReportError).toHaveBeenCalledTimes(1)
})
})
describe('自定義樣式', () => {
it('應該應用自定義 className', () => {
const { container } = render(
內容
)
expect(container.firstChild).toHaveClass('custom-test-class')
})
})
})
describe('useTestAnswer Hook', () => {
const mockOnAnswer = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
})
describe('初始狀態', () => {
it('應該有正確的初始值', () => {
const { result } = renderHook(() => useTestAnswer(mockOnAnswer))
expect(result.current.selectedAnswer).toBeNull()
expect(result.current.showResult).toBe(false)
})
})
describe('答題功能', () => {
it('應該在 handleAnswer 時更新狀態並調用回調', () => {
const { result } = renderHook(() => useTestAnswer(mockOnAnswer))
act(() => {
result.current.handleAnswer('test answer')
})
expect(result.current.selectedAnswer).toBe('test answer')
expect(result.current.showResult).toBe(true)
expect(mockOnAnswer).toHaveBeenCalledWith('test answer')
})
it('應該防止重複提交', async () => {
const { result } = renderHook(() => useTestAnswer(mockOnAnswer))
// 第一次提交
act(() => {
result.current.handleAnswer('first answer')
})
expect(result.current.showResult).toBe(true)
expect(mockOnAnswer).toHaveBeenCalledTimes(1)
// 第二次提交應該被阻止
act(() => {
result.current.handleAnswer('second answer')
})
// onAnswer 不應該被再次調用
expect(mockOnAnswer).toHaveBeenCalledTimes(1)
expect(mockOnAnswer).toHaveBeenLastCalledWith('first answer')
})
})
describe('重置功能', () => {
it('應該正確重置所有狀態', () => {
const { result } = renderHook(() => useTestAnswer(mockOnAnswer))
// 先設置一些狀態
act(() => {
result.current.handleAnswer('test answer')
})
expect(result.current.selectedAnswer).toBe('test answer')
expect(result.current.showResult).toBe(true)
// 重置
act(() => {
result.current.resetAnswer()
})
expect(result.current.selectedAnswer).toBeNull()
expect(result.current.showResult).toBe(false)
})
})
describe('邊界情況', () => {
it('應該處理空字符串答案', () => {
const { result } = renderHook(() => useTestAnswer(mockOnAnswer))
act(() => {
result.current.handleAnswer('')
})
expect(result.current.selectedAnswer).toBe('')
expect(mockOnAnswer).toHaveBeenCalledWith('')
})
it('應該處理多次重置', () => {
const { result } = renderHook(() => useTestAnswer(mockOnAnswer))
act(() => {
result.current.resetAnswer()
result.current.resetAnswer()
result.current.resetAnswer()
})
expect(result.current.selectedAnswer).toBeNull()
expect(result.current.showResult).toBe(false)
})
})
describe('Hook 穩定性', () => {
it('應該在依賴未變時保持函數引用穩定', () => {
const { result, rerender } = renderHook(() => useTestAnswer(mockOnAnswer))
const firstHandleAnswer = result.current.handleAnswer
const firstResetAnswer = result.current.resetAnswer
rerender()
expect(result.current.handleAnswer).toBe(firstHandleAnswer)
expect(result.current.resetAnswer).toBe(firstResetAnswer)
})
})
})