274 lines
7.8 KiB
TypeScript
274 lines
7.8 KiB
TypeScript
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) => (
|
||
<button onClick={onClick} data-testid="error-report-button">
|
||
回報錯誤
|
||
</button>
|
||
),
|
||
TestHeader: ({ title, cefr }: any) => (
|
||
<div data-testid="test-header">
|
||
<h2>{title}</h2>
|
||
<span>CEFR: {cefr}</span>
|
||
</div>
|
||
)
|
||
}))
|
||
|
||
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(
|
||
<BaseTestComponent
|
||
cardData={mockCardData}
|
||
testTitle="測試標題"
|
||
onReportError={mockOnReportError}
|
||
>
|
||
<div data-testid="test-content">測試內容</div>
|
||
</BaseTestComponent>
|
||
)
|
||
|
||
expect(screen.getByTestId('test-header')).toBeInTheDocument()
|
||
expect(screen.getByText('測試標題')).toBeInTheDocument()
|
||
expect(screen.getByText('CEFR: A1')).toBeInTheDocument()
|
||
expect(screen.getByTestId('test-content')).toBeInTheDocument()
|
||
})
|
||
|
||
it('應該顯示錯誤回報按鈕', () => {
|
||
render(
|
||
<BaseTestComponent
|
||
cardData={mockCardData}
|
||
testTitle="測試"
|
||
onReportError={mockOnReportError}
|
||
>
|
||
<div>內容</div>
|
||
</BaseTestComponent>
|
||
)
|
||
|
||
expect(screen.getByTestId('error-report-button')).toBeInTheDocument()
|
||
})
|
||
|
||
it('應該在有說明時顯示說明文字', () => {
|
||
render(
|
||
<BaseTestComponent
|
||
cardData={mockCardData}
|
||
testTitle="測試"
|
||
instructions="這是測試說明"
|
||
onReportError={mockOnReportError}
|
||
>
|
||
<div>內容</div>
|
||
</BaseTestComponent>
|
||
)
|
||
|
||
expect(screen.getByText('這是測試說明')).toBeInTheDocument()
|
||
})
|
||
})
|
||
|
||
describe('結果顯示', () => {
|
||
it('應該在 showResult 為 true 時顯示結果內容', () => {
|
||
render(
|
||
<BaseTestComponent
|
||
cardData={mockCardData}
|
||
testTitle="測試"
|
||
showResult={true}
|
||
resultContent={<div data-testid="result">測試結果</div>}
|
||
onReportError={mockOnReportError}
|
||
>
|
||
<div>內容</div>
|
||
</BaseTestComponent>
|
||
)
|
||
|
||
expect(screen.getByTestId('result')).toBeInTheDocument()
|
||
})
|
||
|
||
it('應該在 showResult 為 false 時隱藏結果內容', () => {
|
||
render(
|
||
<BaseTestComponent
|
||
cardData={mockCardData}
|
||
testTitle="測試"
|
||
showResult={false}
|
||
resultContent={<div data-testid="result">測試結果</div>}
|
||
onReportError={mockOnReportError}
|
||
>
|
||
<div>內容</div>
|
||
</BaseTestComponent>
|
||
)
|
||
|
||
expect(screen.queryByTestId('result')).not.toBeInTheDocument()
|
||
})
|
||
})
|
||
|
||
describe('錯誤回報功能', () => {
|
||
it('應該在點擊錯誤回報按鈕時調用 onReportError', async () => {
|
||
const user = userEvent.setup()
|
||
|
||
render(
|
||
<BaseTestComponent
|
||
cardData={mockCardData}
|
||
testTitle="測試"
|
||
onReportError={mockOnReportError}
|
||
>
|
||
<div>內容</div>
|
||
</BaseTestComponent>
|
||
)
|
||
|
||
const errorButton = screen.getByTestId('error-report-button')
|
||
await user.click(errorButton)
|
||
|
||
expect(mockOnReportError).toHaveBeenCalledTimes(1)
|
||
})
|
||
})
|
||
|
||
describe('自定義樣式', () => {
|
||
it('應該應用自定義 className', () => {
|
||
const { container } = render(
|
||
<BaseTestComponent
|
||
cardData={mockCardData}
|
||
testTitle="測試"
|
||
className="custom-test-class"
|
||
onReportError={mockOnReportError}
|
||
>
|
||
<div>內容</div>
|
||
</BaseTestComponent>
|
||
)
|
||
|
||
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)
|
||
})
|
||
})
|
||
}) |