feat: (SW-3655) new Input and FormInput components * First version new Input and FormInput components * Handle aria-describedby with react-aria instead of manually add it * Update breaking unit and stories tests * Merge branch 'master' into feat/SW-3655-input-component * Update example form * Merge branch 'master' into feat/SW-3655-input-component * New lock file Approved-by: Linus Flood
198 lines
6.0 KiB
TypeScript
198 lines
6.0 KiB
TypeScript
import { describe, expect, it, vi, afterEach } from 'vitest'
|
|
import { render, screen, fireEvent, cleanup } from '@testing-library/react'
|
|
import userEvent from '@testing-library/user-event'
|
|
import { Input } from './Input'
|
|
import { TextField } from 'react-aria-components'
|
|
|
|
afterEach(() => {
|
|
cleanup()
|
|
})
|
|
|
|
// Wrap Input in TextField for proper React Aria context
|
|
const renderInput = (props: React.ComponentProps<typeof Input>) => {
|
|
return render(
|
|
<TextField>
|
|
<Input {...props} />
|
|
</TextField>
|
|
)
|
|
}
|
|
|
|
// Render Input standalone (without TextField) for testing Input's own behavior
|
|
const renderInputStandalone = (props: React.ComponentProps<typeof Input>) => {
|
|
return render(<Input {...props} />)
|
|
}
|
|
|
|
describe('Input', () => {
|
|
describe('props', () => {
|
|
it('applies required attribute', () => {
|
|
renderInput({ label: 'Email', required: true })
|
|
expect(screen.getByRole('textbox')).toHaveProperty('required', true)
|
|
})
|
|
|
|
it('applies readOnly attribute', () => {
|
|
renderInput({ label: 'Email', readOnly: true })
|
|
expect(screen.getByRole('textbox')).toHaveProperty('readOnly', true)
|
|
})
|
|
|
|
it('applies placeholder for floating label when provided', () => {
|
|
renderInput({ label: 'Email', placeholder: 'Enter email' })
|
|
expect(screen.getByRole('textbox').getAttribute('placeholder')).toBe(
|
|
'Enter email'
|
|
)
|
|
})
|
|
|
|
it('applies empty placeholder for top label by default', () => {
|
|
renderInput({ label: 'Email', labelPosition: 'top' })
|
|
expect(screen.getByRole('textbox').getAttribute('placeholder')).toBe('')
|
|
})
|
|
|
|
it('applies custom id', () => {
|
|
// Use standalone render since TextField overrides id via context
|
|
renderInputStandalone({ label: 'Email', id: 'custom-id' })
|
|
expect(screen.getByRole('textbox').getAttribute('id')).toBe('custom-id')
|
|
})
|
|
|
|
it('applies aria-describedby', () => {
|
|
renderInput({ label: 'Email', 'aria-describedby': 'error-message' })
|
|
expect(screen.getByRole('textbox').getAttribute('aria-describedby')).toBe(
|
|
'error-message'
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('clear content button', () => {
|
|
it('does not show clear button when showClearContentIcon is false', () => {
|
|
renderInput({
|
|
label: 'Email',
|
|
value: 'test',
|
|
onChange: vi.fn(),
|
|
showClearContentIcon: false,
|
|
})
|
|
expect(screen.queryByLabelText('Clear content')).toBeNull()
|
|
})
|
|
|
|
it('does not show clear button when input is empty', () => {
|
|
renderInput({
|
|
label: 'Email',
|
|
value: '',
|
|
onChange: vi.fn(),
|
|
showClearContentIcon: true,
|
|
})
|
|
expect(screen.queryByLabelText('Clear content')).toBeNull()
|
|
})
|
|
|
|
it('shows clear button when input has value and showClearContentIcon is true', () => {
|
|
renderInput({
|
|
label: 'Email',
|
|
value: 'test',
|
|
onChange: vi.fn(),
|
|
showClearContentIcon: true,
|
|
})
|
|
expect(screen.getByLabelText('Clear content')).toBeTruthy()
|
|
})
|
|
})
|
|
|
|
describe('icons', () => {
|
|
it('renders left icon when provided', () => {
|
|
renderInput({
|
|
label: 'Search',
|
|
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
|
|
leftIcon: <span data-testid="left-icon">🔍</span>,
|
|
})
|
|
expect(screen.getByTestId('left-icon')).toBeTruthy()
|
|
})
|
|
|
|
it('renders right icon when provided', () => {
|
|
renderInput({
|
|
label: 'Password',
|
|
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
|
|
rightIcon: <span data-testid="right-icon">👁</span>,
|
|
})
|
|
expect(screen.getByTestId('right-icon')).toBeTruthy()
|
|
})
|
|
|
|
it('hides right icon when clear button is shown', () => {
|
|
renderInput({
|
|
label: 'Email',
|
|
value: 'test',
|
|
onChange: vi.fn(),
|
|
showClearContentIcon: true,
|
|
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
|
|
rightIcon: <span data-testid="right-icon">👁</span>,
|
|
})
|
|
expect(screen.queryByTestId('right-icon')).toBeNull()
|
|
expect(screen.getByLabelText('Clear content')).toBeTruthy()
|
|
})
|
|
|
|
it('shows right icon when clear button condition not met', () => {
|
|
renderInput({
|
|
label: 'Email',
|
|
value: '',
|
|
onChange: vi.fn(),
|
|
showClearContentIcon: true,
|
|
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
|
|
rightIcon: <span data-testid="right-icon">👁</span>,
|
|
})
|
|
expect(screen.getByTestId('right-icon')).toBeTruthy()
|
|
})
|
|
})
|
|
|
|
describe('controlled input', () => {
|
|
it('displays the controlled value', () => {
|
|
renderInput({
|
|
label: 'Email',
|
|
value: 'test@example.com',
|
|
onChange: vi.fn(),
|
|
})
|
|
expect(screen.getByRole('textbox')).toHaveProperty(
|
|
'value',
|
|
'test@example.com'
|
|
)
|
|
})
|
|
|
|
it('calls onChange when typing', async () => {
|
|
const onChange = vi.fn()
|
|
renderInput({ label: 'Email', value: '', onChange })
|
|
|
|
const input = screen.getByRole('textbox')
|
|
await userEvent.type(input, 'a')
|
|
|
|
expect(onChange).toHaveBeenCalled()
|
|
})
|
|
|
|
it('does not change value without onChange updating it', () => {
|
|
const onChange = vi.fn()
|
|
renderInput({ label: 'Email', value: 'initial', onChange })
|
|
|
|
const input = screen.getByRole('textbox')
|
|
fireEvent.change(input, { target: { value: 'changed' } })
|
|
|
|
// Value stays the same because it's controlled
|
|
expect(input).toHaveProperty('value', 'initial')
|
|
})
|
|
})
|
|
|
|
describe('ref forwarding', () => {
|
|
it('forwards ref to the input element', () => {
|
|
const ref = { current: null as HTMLInputElement | null }
|
|
render(
|
|
<TextField>
|
|
<Input label="Email" ref={ref} />
|
|
</TextField>
|
|
)
|
|
expect(ref.current).toBeInstanceOf(HTMLInputElement)
|
|
})
|
|
|
|
it('allows focusing via ref', () => {
|
|
const ref = { current: null as HTMLInputElement | null }
|
|
render(
|
|
<TextField>
|
|
<Input label="Email" ref={ref} />
|
|
</TextField>
|
|
)
|
|
ref.current?.focus()
|
|
expect(document.activeElement).toBe(ref.current)
|
|
})
|
|
})
|
|
})
|