598 lines
16 KiB
TypeScript
598 lines
16 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 { ChoiceOption, ChoiceGrid, TextInput, ConfidenceLevel, RecordingControl } from '../../shared/AnswerActions'
|
|
|
|
// Mock BluePlayButton
|
|
vi.mock('@/components/shared/BluePlayButton', () => ({
|
|
BluePlayButton: ({ onPlayStart, disabled, title }: any) => (
|
|
<button
|
|
onClick={onPlayStart}
|
|
disabled={disabled}
|
|
title={title}
|
|
data-testid="blue-play-button"
|
|
>
|
|
播放
|
|
</button>
|
|
)
|
|
}))
|
|
|
|
describe('ChoiceOption', () => {
|
|
const mockOnSelect = vi.fn()
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
describe('基礎渲染', () => {
|
|
it('應該顯示選項文字', () => {
|
|
render(
|
|
<ChoiceOption
|
|
option="hello"
|
|
index={0}
|
|
onSelect={mockOnSelect}
|
|
/>
|
|
)
|
|
|
|
expect(screen.getByText('hello')).toBeInTheDocument()
|
|
expect(screen.getByLabelText('選項 1: hello')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('選擇狀態樣式', () => {
|
|
it('應該在選中時應用選中樣式', () => {
|
|
render(
|
|
<ChoiceOption
|
|
option="test"
|
|
index={0}
|
|
isSelected={true}
|
|
onSelect={mockOnSelect}
|
|
/>
|
|
)
|
|
|
|
const button = screen.getByRole('button')
|
|
expect(button).toHaveClass('border-blue-500', 'bg-blue-50', 'text-blue-700')
|
|
})
|
|
|
|
it('應該在顯示結果且正確時應用正確樣式', () => {
|
|
render(
|
|
<ChoiceOption
|
|
option="correct"
|
|
index={0}
|
|
isCorrect={true}
|
|
showResult={true}
|
|
onSelect={mockOnSelect}
|
|
/>
|
|
)
|
|
|
|
const button = screen.getByRole('button')
|
|
expect(button).toHaveClass('border-green-500', 'bg-green-50', 'text-green-700')
|
|
})
|
|
|
|
it('應該在顯示結果且錯誤時應用錯誤樣式', () => {
|
|
render(
|
|
<ChoiceOption
|
|
option="wrong"
|
|
index={0}
|
|
isSelected={true}
|
|
isIncorrect={true}
|
|
showResult={true}
|
|
onSelect={mockOnSelect}
|
|
/>
|
|
)
|
|
|
|
const button = screen.getByRole('button')
|
|
expect(button).toHaveClass('border-red-500', 'bg-red-50', 'text-red-700')
|
|
})
|
|
})
|
|
|
|
describe('交互功能', () => {
|
|
it('應該在點擊時調用 onSelect', async () => {
|
|
const user = userEvent.setup()
|
|
|
|
render(
|
|
<ChoiceOption
|
|
option="clickable"
|
|
index={0}
|
|
onSelect={mockOnSelect}
|
|
/>
|
|
)
|
|
|
|
await user.click(screen.getByRole('button'))
|
|
expect(mockOnSelect).toHaveBeenCalledWith('clickable')
|
|
})
|
|
|
|
it('應該在 disabled 時不調用 onSelect', async () => {
|
|
const user = userEvent.setup()
|
|
|
|
render(
|
|
<ChoiceOption
|
|
option="disabled"
|
|
index={0}
|
|
disabled={true}
|
|
onSelect={mockOnSelect}
|
|
/>
|
|
)
|
|
|
|
await user.click(screen.getByRole('button'))
|
|
expect(mockOnSelect).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('應該在 showResult 時不調用 onSelect', async () => {
|
|
const user = userEvent.setup()
|
|
|
|
render(
|
|
<ChoiceOption
|
|
option="result"
|
|
index={0}
|
|
showResult={true}
|
|
onSelect={mockOnSelect}
|
|
/>
|
|
)
|
|
|
|
await user.click(screen.getByRole('button'))
|
|
expect(mockOnSelect).not.toHaveBeenCalled()
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('ChoiceGrid', () => {
|
|
const mockOptions = ['option1', 'option2', 'option3', 'option4']
|
|
const mockOnSelect = vi.fn()
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
describe('基礎渲染', () => {
|
|
it('應該渲染所有選項', () => {
|
|
render(
|
|
<ChoiceGrid
|
|
options={mockOptions}
|
|
onSelect={mockOnSelect}
|
|
/>
|
|
)
|
|
|
|
mockOptions.forEach(option => {
|
|
expect(screen.getByText(option)).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('應該使用響應式網格布局', () => {
|
|
const { container } = render(
|
|
<ChoiceGrid
|
|
options={mockOptions}
|
|
onSelect={mockOnSelect}
|
|
/>
|
|
)
|
|
|
|
expect(container.firstChild).toHaveClass('grid', 'grid-cols-1', 'sm:grid-cols-2')
|
|
})
|
|
})
|
|
|
|
describe('選擇狀態管理', () => {
|
|
it('應該正確顯示選中狀態', () => {
|
|
render(
|
|
<ChoiceGrid
|
|
options={mockOptions}
|
|
selectedOption="option2"
|
|
onSelect={mockOnSelect}
|
|
/>
|
|
)
|
|
|
|
const selectedButton = screen.getByLabelText('選項 2: option2')
|
|
expect(selectedButton).toHaveClass('border-blue-500')
|
|
})
|
|
|
|
it('應該在顯示結果時正確標記正確答案', () => {
|
|
render(
|
|
<ChoiceGrid
|
|
options={mockOptions}
|
|
selectedOption="option1"
|
|
correctAnswer="option3"
|
|
showResult={true}
|
|
onSelect={mockOnSelect}
|
|
/>
|
|
)
|
|
|
|
const correctButton = screen.getByLabelText('選項 3: option3')
|
|
const wrongButton = screen.getByLabelText('選項 1: option1')
|
|
|
|
expect(correctButton).toHaveClass('border-green-500')
|
|
expect(wrongButton).toHaveClass('border-red-500')
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('TextInput', () => {
|
|
const mockOnChange = vi.fn()
|
|
const mockOnSubmit = vi.fn()
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
describe('基礎功能', () => {
|
|
it('應該處理文字輸入', async () => {
|
|
const user = userEvent.setup()
|
|
|
|
render(
|
|
<TextInput
|
|
value=""
|
|
onChange={mockOnChange}
|
|
onSubmit={mockOnSubmit}
|
|
/>
|
|
)
|
|
|
|
const input = screen.getByRole('textbox')
|
|
await user.type(input, 'hello')
|
|
|
|
expect(mockOnChange).toHaveBeenCalledTimes(5) // 每個字符一次
|
|
})
|
|
|
|
it('應該在按Enter時提交', async () => {
|
|
const user = userEvent.setup()
|
|
|
|
render(
|
|
<TextInput
|
|
value="test answer"
|
|
onChange={mockOnChange}
|
|
onSubmit={mockOnSubmit}
|
|
/>
|
|
)
|
|
|
|
const input = screen.getByRole('textbox')
|
|
await user.type(input, '{enter}')
|
|
|
|
expect(mockOnSubmit).toHaveBeenCalledWith('test answer')
|
|
})
|
|
|
|
it('應該在點擊提交按鈕時提交', async () => {
|
|
const user = userEvent.setup()
|
|
|
|
render(
|
|
<TextInput
|
|
value="test answer"
|
|
onChange={mockOnChange}
|
|
onSubmit={mockOnSubmit}
|
|
/>
|
|
)
|
|
|
|
const submitButton = screen.getByText('提交')
|
|
await user.click(submitButton)
|
|
|
|
expect(mockOnSubmit).toHaveBeenCalledWith('test answer')
|
|
})
|
|
})
|
|
|
|
describe('提交按鈕狀態', () => {
|
|
it('應該在輸入為空時禁用提交按鈕', () => {
|
|
render(
|
|
<TextInput
|
|
value=""
|
|
onChange={mockOnChange}
|
|
onSubmit={mockOnSubmit}
|
|
/>
|
|
)
|
|
|
|
const submitButton = screen.getByText('提交')
|
|
expect(submitButton).toBeDisabled()
|
|
})
|
|
|
|
it('應該在有輸入時啟用提交按鈕', () => {
|
|
render(
|
|
<TextInput
|
|
value="some text"
|
|
onChange={mockOnChange}
|
|
onSubmit={mockOnSubmit}
|
|
/>
|
|
)
|
|
|
|
const submitButton = screen.getByText('提交')
|
|
expect(submitButton).not.toBeDisabled()
|
|
})
|
|
|
|
it('應該在顯示結果時隱藏提交按鈕', () => {
|
|
render(
|
|
<TextInput
|
|
value="answer"
|
|
showResult={true}
|
|
onChange={mockOnChange}
|
|
onSubmit={mockOnSubmit}
|
|
/>
|
|
)
|
|
|
|
expect(screen.queryByText('提交')).not.toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('結果顯示', () => {
|
|
it('應該在答錯時顯示正確答案', () => {
|
|
render(
|
|
<TextInput
|
|
value="wrong"
|
|
showResult={true}
|
|
isCorrect={false}
|
|
correctAnswer="correct"
|
|
onChange={mockOnChange}
|
|
onSubmit={mockOnSubmit}
|
|
/>
|
|
)
|
|
|
|
expect(screen.getByText('正確答案:')).toBeInTheDocument()
|
|
expect(screen.getByText('correct')).toBeInTheDocument()
|
|
})
|
|
|
|
it('應該在答對時不顯示正確答案', () => {
|
|
render(
|
|
<TextInput
|
|
value="correct"
|
|
showResult={true}
|
|
isCorrect={true}
|
|
correctAnswer="correct"
|
|
onChange={mockOnChange}
|
|
onSubmit={mockOnSubmit}
|
|
/>
|
|
)
|
|
|
|
expect(screen.queryByText('正確答案:')).not.toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('輸入樣式', () => {
|
|
it('應該在正確時應用綠色樣式', () => {
|
|
render(
|
|
<TextInput
|
|
value="correct"
|
|
showResult={true}
|
|
isCorrect={true}
|
|
onChange={mockOnChange}
|
|
onSubmit={mockOnSubmit}
|
|
/>
|
|
)
|
|
|
|
const input = screen.getByRole('textbox')
|
|
expect(input).toHaveClass('border-green-500', 'bg-green-50')
|
|
})
|
|
|
|
it('應該在錯誤時應用紅色樣式', () => {
|
|
render(
|
|
<TextInput
|
|
value="wrong"
|
|
showResult={true}
|
|
isCorrect={false}
|
|
onChange={mockOnChange}
|
|
onSubmit={mockOnSubmit}
|
|
/>
|
|
)
|
|
|
|
const input = screen.getByRole('textbox')
|
|
expect(input).toHaveClass('border-red-500', 'bg-red-50')
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('ConfidenceLevel', () => {
|
|
const mockOnSelect = vi.fn()
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
describe('基礎渲染', () => {
|
|
it('應該渲染所有信心等級按鈕', () => {
|
|
render(
|
|
<ConfidenceLevel
|
|
onSelect={mockOnSelect}
|
|
/>
|
|
)
|
|
|
|
expect(screen.getByText('完全不熟')).toBeInTheDocument()
|
|
expect(screen.getByText('完全掌握')).toBeInTheDocument()
|
|
|
|
// 檢查所有等級數字
|
|
for (let i = 1; i <= 5; i++) {
|
|
expect(screen.getByText(i.toString())).toBeInTheDocument()
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('信心等級選擇', () => {
|
|
it('應該在點擊時調用 onSelect', async () => {
|
|
const user = userEvent.setup()
|
|
|
|
render(
|
|
<ConfidenceLevel
|
|
onSelect={mockOnSelect}
|
|
/>
|
|
)
|
|
|
|
const level3Button = screen.getByText('還算熟悉').closest('button')
|
|
await user.click(level3Button!)
|
|
|
|
expect(mockOnSelect).toHaveBeenCalledWith(3)
|
|
})
|
|
|
|
it('應該正確顯示選中狀態', () => {
|
|
render(
|
|
<ConfidenceLevel
|
|
selectedLevel={4}
|
|
onSelect={mockOnSelect}
|
|
/>
|
|
)
|
|
|
|
const selectedButton = screen.getByText('很熟悉').closest('button')
|
|
expect(selectedButton).toHaveClass('ring-4', 'ring-opacity-50')
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('RecordingControl', () => {
|
|
const mockOnStartRecording = vi.fn()
|
|
const mockOnStopRecording = vi.fn()
|
|
const mockOnPlayback = vi.fn()
|
|
const mockOnSubmit = vi.fn()
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
describe('錄音狀態管理', () => {
|
|
it('應該在非錄音狀態顯示開始按鈕', () => {
|
|
render(
|
|
<RecordingControl
|
|
isRecording={false}
|
|
hasRecording={false}
|
|
onStartRecording={mockOnStartRecording}
|
|
onStopRecording={mockOnStopRecording}
|
|
onPlayback={mockOnPlayback}
|
|
onSubmit={mockOnSubmit}
|
|
/>
|
|
)
|
|
|
|
expect(screen.getByText('🎤')).toBeInTheDocument()
|
|
expect(screen.getByText('點擊開始錄音')).toBeInTheDocument()
|
|
})
|
|
|
|
it('應該在錄音狀態顯示停止按鈕', () => {
|
|
render(
|
|
<RecordingControl
|
|
isRecording={true}
|
|
hasRecording={false}
|
|
onStartRecording={mockOnStartRecording}
|
|
onStopRecording={mockOnStopRecording}
|
|
onPlayback={mockOnPlayback}
|
|
onSubmit={mockOnSubmit}
|
|
/>
|
|
)
|
|
|
|
expect(screen.getByText('⏹️')).toBeInTheDocument()
|
|
expect(screen.getByText('錄音中... 點擊停止')).toBeInTheDocument()
|
|
})
|
|
|
|
it('應該在有錄音時顯示播放和提交按鈕', () => {
|
|
render(
|
|
<RecordingControl
|
|
isRecording={false}
|
|
hasRecording={true}
|
|
onStartRecording={mockOnStartRecording}
|
|
onStopRecording={mockOnStopRecording}
|
|
onPlayback={mockOnPlayback}
|
|
onSubmit={mockOnSubmit}
|
|
/>
|
|
)
|
|
|
|
expect(screen.getByText('錄音完成')).toBeInTheDocument()
|
|
expect(screen.getByTestId('blue-play-button')).toBeInTheDocument()
|
|
expect(screen.getByText('提交錄音')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('錄音操作', () => {
|
|
it('應該在點擊時開始錄音', async () => {
|
|
const user = userEvent.setup()
|
|
|
|
render(
|
|
<RecordingControl
|
|
isRecording={false}
|
|
hasRecording={false}
|
|
onStartRecording={mockOnStartRecording}
|
|
onStopRecording={mockOnStopRecording}
|
|
onPlayback={mockOnPlayback}
|
|
onSubmit={mockOnSubmit}
|
|
/>
|
|
)
|
|
|
|
const recordButton = screen.getByRole('button')
|
|
await user.click(recordButton)
|
|
|
|
expect(mockOnStartRecording).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('應該在錄音時點擊停止錄音', async () => {
|
|
const user = userEvent.setup()
|
|
|
|
render(
|
|
<RecordingControl
|
|
isRecording={true}
|
|
hasRecording={false}
|
|
onStartRecording={mockOnStartRecording}
|
|
onStopRecording={mockOnStopRecording}
|
|
onPlayback={mockOnPlayback}
|
|
onSubmit={mockOnSubmit}
|
|
/>
|
|
)
|
|
|
|
const recordButton = screen.getByRole('button')
|
|
await user.click(recordButton)
|
|
|
|
expect(mockOnStopRecording).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('應該在有錄音時能播放', async () => {
|
|
const user = userEvent.setup()
|
|
|
|
render(
|
|
<RecordingControl
|
|
isRecording={false}
|
|
hasRecording={true}
|
|
onStartRecording={mockOnStartRecording}
|
|
onStopRecording={mockOnStopRecording}
|
|
onPlayback={mockOnPlayback}
|
|
onSubmit={mockOnSubmit}
|
|
/>
|
|
)
|
|
|
|
const playButton = screen.getByTestId('blue-play-button')
|
|
await user.click(playButton)
|
|
|
|
expect(mockOnPlayback).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('應該在有錄音時能提交', async () => {
|
|
const user = userEvent.setup()
|
|
|
|
render(
|
|
<RecordingControl
|
|
isRecording={false}
|
|
hasRecording={true}
|
|
onStartRecording={mockOnStartRecording}
|
|
onStopRecording={mockOnStopRecording}
|
|
onPlayback={mockOnPlayback}
|
|
onSubmit={mockOnSubmit}
|
|
/>
|
|
)
|
|
|
|
const submitButton = screen.getByText('提交錄音')
|
|
await user.click(submitButton)
|
|
|
|
expect(mockOnSubmit).toHaveBeenCalledTimes(1)
|
|
})
|
|
})
|
|
|
|
describe('禁用狀態', () => {
|
|
it('應該在 disabled 時禁用所有按鈕', () => {
|
|
render(
|
|
<RecordingControl
|
|
isRecording={false}
|
|
hasRecording={true}
|
|
disabled={true}
|
|
onStartRecording={mockOnStartRecording}
|
|
onStopRecording={mockOnStopRecording}
|
|
onPlayback={mockOnPlayback}
|
|
onSubmit={mockOnSubmit}
|
|
/>
|
|
)
|
|
|
|
// 錄音按鈕應該禁用
|
|
const recordButton = screen.getByText('🎤')
|
|
expect(recordButton).toBeDisabled()
|
|
|
|
// 播放和提交按鈕應該禁用
|
|
const playButton = screen.getByTestId('blue-play-button')
|
|
const submitButton = screen.getByText('提交錄音')
|
|
expect(playButton).toBeDisabled()
|
|
expect(submitButton).toBeDisabled()
|
|
})
|
|
})
|
|
}) |