Files
web/packages/design-system/lib/components/InputNew/Input.test.tsx
Rasmus Langvad edca33c49f 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
2025-12-08 08:51:03 +00:00

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)
})
})
})