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:
248
admin-web/components/tag-input.test.tsx
Normal file
248
admin-web/components/tag-input.test.tsx
Normal 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'])
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user