feat: add form input components with tests

- Add TagInput component for equipment tags
- Add ExerciseList component for workout exercises
- Both components include comprehensive test suites
- Add data-testid attributes for testability
This commit is contained in:
Millian Lamiaux
2026-03-14 20:42:36 +01:00
parent 71e9a9bdb5
commit 3df7dd4a47
4 changed files with 748 additions and 0 deletions

View File

@@ -0,0 +1,248 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { TagInput } from './tag-input'
describe('TagInput', () => {
const mockOnChange = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
})
it('should render empty input with placeholder', () => {
render(
<TagInput
value={[]}
onChange={mockOnChange}
placeholder="Add equipment..."
/>
)
expect(screen.getByPlaceholderText('Add equipment...')).toBeInTheDocument()
})
it('should add tag on Enter key', async () => {
const user = userEvent.setup()
render(
<TagInput
value={[]}
onChange={mockOnChange}
/>
)
const input = screen.getByRole('textbox')
await user.type(input, 'Dumbbells')
await user.keyboard('{Enter}')
expect(mockOnChange).toHaveBeenCalledWith(['Dumbbells'])
})
it('should add tag on blur', async () => {
const user = userEvent.setup()
render(
<TagInput
value={[]}
onChange={mockOnChange}
/>
)
const input = screen.getByRole('textbox')
await user.type(input, 'Yoga Mat')
await user.tab() // blur
expect(mockOnChange).toHaveBeenCalledWith(['Yoga Mat'])
})
it('should not add empty tag', async () => {
const user = userEvent.setup()
render(
<TagInput
value={[]}
onChange={mockOnChange}
/>
)
const input = screen.getByRole('textbox')
await user.type(input, ' ')
await user.keyboard('{Enter}')
expect(mockOnChange).not.toHaveBeenCalled()
})
it('should not add duplicate tags', async () => {
const user = userEvent.setup()
render(
<TagInput
value={['Dumbbells']}
onChange={mockOnChange}
/>
)
const input = screen.getByRole('textbox')
await user.type(input, 'Dumbbells')
await user.keyboard('{Enter}')
expect(mockOnChange).not.toHaveBeenCalled()
})
it('should remove tag on X click', async () => {
const user = userEvent.setup()
render(
<TagInput
value={['Dumbbells', 'Yoga Mat']}
onChange={mockOnChange}
/>
)
// Find and click the remove button on first tag
const removeButtons = screen.getAllByRole('button', { name: '' })
await user.click(removeButtons[0])
expect(mockOnChange).toHaveBeenCalledWith(['Yoga Mat'])
})
it('should remove last tag on Backspace when input empty', async () => {
const user = userEvent.setup()
render(
<TagInput
value={['Dumbbells', 'Yoga Mat']}
onChange={mockOnChange}
/>
)
const input = screen.getByRole('textbox')
await user.click(input)
await user.keyboard('{Backspace}')
expect(mockOnChange).toHaveBeenCalledWith(['Dumbbells'])
})
it('should not remove tag on Backspace when input has text', async () => {
const user = userEvent.setup()
render(
<TagInput
value={['Dumbbells']}
onChange={mockOnChange}
/>
)
const input = screen.getByRole('textbox')
await user.type(input, 'Yoga')
await user.keyboard('{Backspace}')
expect(mockOnChange).not.toHaveBeenCalled()
})
it('should display all tags', () => {
render(
<TagInput
value={['Dumbbells', 'Yoga Mat', 'Resistance Band']}
onChange={mockOnChange}
/>
)
expect(screen.getByText('Dumbbells')).toBeInTheDocument()
expect(screen.getByText('Yoga Mat')).toBeInTheDocument()
expect(screen.getByText('Resistance Band')).toBeInTheDocument()
})
it('should trim whitespace from tags', async () => {
const user = userEvent.setup()
render(
<TagInput
value={[]}
onChange={mockOnChange}
/>
)
const input = screen.getByRole('textbox')
await user.type(input, ' Dumbbells ')
await user.keyboard('{Enter}')
expect(mockOnChange).toHaveBeenCalledWith(['Dumbbells'])
})
it('should respect disabled prop', () => {
render(
<TagInput
value={['Dumbbells']}
onChange={mockOnChange}
disabled
/>
)
expect(screen.getByRole('textbox')).toBeDisabled()
// Remove button on tag should not be present when disabled
const removeButtons = screen.queryAllByRole('button', { name: '' })
expect(removeButtons).toHaveLength(0)
})
it('should clear input after adding tag', async () => {
const user = userEvent.setup()
render(
<TagInput
value={[]}
onChange={mockOnChange}
/>
)
const input = screen.getByRole('textbox') as HTMLInputElement
await user.type(input, 'Dumbbells')
await user.keyboard('{Enter}')
expect(input.value).toBe('')
})
it('should handle multiple tags addition', async () => {
const user = userEvent.setup()
const { rerender } = render(
<TagInput
value={[]}
onChange={mockOnChange}
/>
)
const input = screen.getByRole('textbox')
// Add first tag
await user.type(input, 'Dumbbells')
await user.keyboard('{Enter}')
expect(mockOnChange).toHaveBeenCalledWith(['Dumbbells'])
// Rerender with updated value
rerender(
<TagInput
value={['Dumbbells']}
onChange={mockOnChange}
/>
)
// Add second tag
await user.type(input, 'Yoga Mat')
await user.keyboard('{Enter}')
expect(mockOnChange).toHaveBeenCalledWith(['Dumbbells', 'Yoga Mat'])
// Rerender with updated value
rerender(
<TagInput
value={['Dumbbells', 'Yoga Mat']}
onChange={mockOnChange}
/>
)
// Add third tag
await user.type(input, 'Resistance Band')
await user.keyboard('{Enter}')
expect(mockOnChange).toHaveBeenCalledWith(['Dumbbells', 'Yoga Mat', 'Resistance Band'])
})
})