// @vitest-environment jsdom
/**
* Field-control behavior: when a draft becomes a write, what a bad draft does
* instead, and how an overridden field offers its reset.
*/
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { NumberField, SecretField, TextField } from '../src/client/fields.tsx'
afterEach(cleanup)
const frame = {
id: 'field',
label: 'Command timeout',
hint: 'How long one command may run.',
overriddenLabel: 'Overridden',
resetLabel: 'Reset to default',
disabled: false,
}
describe('NumberField', () => {
it('commits a changed draft on blur', () => {
const onCommit = vi.fn()
render(
,
)
const input = screen.getByLabelText('Command timeout')
fireEvent.change(input, { target: { value: '9000' } })
fireEvent.blur(input)
expect(onCommit).toHaveBeenCalledWith(9_000)
})
it('commits on Enter through the blur the key triggers', () => {
const onCommit = vi.fn()
render(
,
)
const input = screen.getByLabelText('Command timeout')
fireEvent.change(input, { target: { value: '1234' } })
fireEvent.keyDown(input, { key: 'Enter' })
fireEvent.blur(input)
expect(onCommit).toHaveBeenCalledWith(1_234)
})
it('restores the last good value instead of committing a draft that is not a number', () => {
const onCommit = vi.fn()
render(
,
)
const input = screen.getByLabelText('Command timeout')
fireEvent.change(input, { target: { value: 'soon' } })
fireEvent.blur(input)
expect(onCommit).not.toHaveBeenCalled()
expect(input).toHaveProperty('value', '60000')
})
it('writes nothing when the draft settles on the value already shown', () => {
const onCommit = vi.fn()
render(
,
)
const input = screen.getByLabelText('Command timeout')
fireEvent.change(input, { target: { value: '60000' } })
fireEvent.blur(input)
expect(onCommit).not.toHaveBeenCalled()
})
it('offers the reset only while the field is overridden', () => {
const onReset = vi.fn()
const { rerender } = render(
,
)
expect(screen.queryByRole('button', { name: 'Reset to default' })).toBeNull()
rerender(
,
)
fireEvent.click(screen.getByRole('button', { name: 'Reset to default' }))
expect(screen.getByText('Overridden')).toBeTruthy()
expect(onReset).toHaveBeenCalledOnce()
})
it('re-seeds the draft when the authoritative value changes underneath', () => {
const { rerender } = render(
,
)
expect(screen.getByLabelText('Command timeout')).toHaveProperty('value', '9000')
rerender(
,
)
expect(screen.getByLabelText('Command timeout')).toHaveProperty('value', '60000')
})
it('ignores a keystroke that is not Enter', () => {
const onCommit = vi.fn()
render(
,
)
const input = screen.getByLabelText('Command timeout')
fireEvent.change(input, { target: { value: '9000' } })
fireEvent.keyDown(input, { key: 'Escape' })
expect(onCommit).not.toHaveBeenCalled()
})
it('renders an absent value as empty rather than as a number nobody chose', () => {
const onCommit = vi.fn()
render(
,
)
const input = screen.getByLabelText('Command timeout')
expect(input).toHaveProperty('value', '')
// A draft typed and then cleared restores the same emptiness, not a zero.
fireEvent.change(input, { target: { value: 'abc' } })
fireEvent.blur(input)
expect(input).toHaveProperty('value', '')
expect(onCommit).not.toHaveBeenCalled()
})
it('suppresses every interaction while disabled', () => {
const onCommit = vi.fn()
const onReset = vi.fn()
render(
,
)
const input = screen.getByLabelText('Command timeout')
expect(input).toHaveProperty('disabled', true)
expect(screen.getByRole('button', { name: 'Reset to default' })).toHaveProperty('disabled', true)
expect(onCommit).not.toHaveBeenCalled()
expect(onReset).not.toHaveBeenCalled()
})
})
describe('TextField', () => {
it('commits the trimmed draft', () => {
const onCommit = vi.fn()
render(
,
)
const input = screen.getByLabelText('Endpoint')
fireEvent.change(input, { target: { value: ' https://search.test/v1 ' } })
fireEvent.blur(input)
expect(onCommit).toHaveBeenCalledWith('https://search.test/v1')
})
it('commits an emptied draft, which clears the field', () => {
const onCommit = vi.fn()
render(
,
)
const input = screen.getByLabelText('Endpoint')
fireEvent.change(input, { target: { value: '' } })
fireEvent.blur(input)
expect(onCommit).toHaveBeenCalledWith('')
})
it('renders its placeholder and commits on Enter', () => {
const onCommit = vi.fn()
render(
,
)
const input = screen.getByLabelText('Endpoint')
expect(input).toHaveProperty('placeholder', 'https://api.deepseek.com')
fireEvent.change(input, { target: { value: 'https://other.test' } })
fireEvent.keyDown(input, { key: 'Enter' })
fireEvent.blur(input)
expect(onCommit).toHaveBeenCalledWith('https://other.test')
})
it('ignores a keystroke that is not Enter and writes nothing unchanged', () => {
const onCommit = vi.fn()
render(
,
)
const input = screen.getByLabelText('Endpoint')
fireEvent.keyDown(input, { key: 'a' })
fireEvent.blur(input)
expect(onCommit).not.toHaveBeenCalled()
})
})
describe('SecretField', () => {
it('commits a non-empty draft and clears the control after writing', () => {
const onCommit = vi.fn()
render(
,
)
const input = screen.getByLabelText('API key')
fireEvent.change(input, { target: { value: ' ds-secret ' } })
fireEvent.blur(input)
expect(onCommit).toHaveBeenCalledWith('ds-secret')
expect(input).toHaveProperty('value', '')
})
it('keeps the stored key when the draft is left blank', () => {
const onCommit = vi.fn()
render(
,
)
const input = screen.getByLabelText('API key')
fireEvent.change(input, { target: { value: ' ' } })
fireEvent.blur(input)
expect(onCommit).not.toHaveBeenCalled()
expect(screen.getByText('A key is configured.')).toBeTruthy()
})
it('ignores a keystroke that is not Enter', () => {
const onCommit = vi.fn()
render(
,
)
const input = screen.getByLabelText('API key')
fireEvent.change(input, { target: { value: 'ds-secret' } })
fireEvent.keyDown(input, { key: 'Tab' })
expect(onCommit).not.toHaveBeenCalled()
})
it('never renders the value it writes', () => {
render(
,
)
expect(screen.getByLabelText('API key')).toHaveProperty('type', 'password')
})
it('commits on Enter and stays disabled when the document is read-only', () => {
const onCommit = vi.fn()
const { rerender } = render(
,
)
const input = screen.getByLabelText('API key')
fireEvent.change(input, { target: { value: 'ds-secret' } })
fireEvent.keyDown(input, { key: 'Enter' })
fireEvent.blur(input)
expect(onCommit).toHaveBeenCalledWith('ds-secret')
rerender(
,
)
expect(screen.getByLabelText('API key')).toHaveProperty('disabled', true)
})
})