Merged in feat/SW-3655-input-component (pull request #3296)
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
This commit is contained in:
197
packages/design-system/lib/components/InputNew/Input.test.tsx
Normal file
197
packages/design-system/lib/components/InputNew/Input.test.tsx
Normal file
@@ -0,0 +1,197 @@
|
||||
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)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user