diff --git a/frontend/src/components/modals/NodeModal.tsx b/frontend/src/components/modals/NodeModal.tsx index 4e5e776..746fd01 100644 --- a/frontend/src/components/modals/NodeModal.tsx +++ b/frontend/src/components/modals/NodeModal.tsx @@ -43,13 +43,18 @@ export function NodeModal({ open, onClose, onSubmit, initial, title = 'Add Node' const [form, setForm] = useState>({ ...DEFAULT_DATA, ...initial }) const [iconSearch, setIconSearch] = useState('') const [iconPickerOpen, setIconPickerOpen] = useState(false) + const [labelError, setLabelError] = useState(false) const set = (key: keyof NodeData, value: unknown) => setForm((f) => ({ ...f, [key]: value })) const handleSubmit = (e: React.FormEvent) => { e.preventDefault() - if (!form.label?.trim()) return + if (!form.label?.trim()) { + setLabelError(true) + return + } + setLabelError(false) onSubmit(form) onClose() } @@ -167,11 +172,11 @@ export function NodeModal({ open, onClose, onSubmit, initial, title = 'Add Node' set('label', e.target.value)} + onChange={(e) => { set('label', e.target.value); if (labelError) setLabelError(false) }} placeholder="My Server" - className="bg-[#21262d] border-[#30363d] text-sm h-8" - required + className={`bg-[#21262d] text-sm h-8 ${labelError ? 'border-[#f85149] focus-visible:ring-[#f85149]' : 'border-[#30363d]'}`} /> + {labelError &&

Label is required

} {/* Hostname */} diff --git a/frontend/src/components/modals/__tests__/NodeModal.test.tsx b/frontend/src/components/modals/__tests__/NodeModal.test.tsx new file mode 100644 index 0000000..c45a498 --- /dev/null +++ b/frontend/src/components/modals/__tests__/NodeModal.test.tsx @@ -0,0 +1,65 @@ +import { describe, it, expect, vi } from 'vitest' +import { render, screen, fireEvent } from '@testing-library/react' +import { NodeModal } from '../NodeModal' + +describe('NodeModal', () => { + it('renders nothing when closed', () => { + const { container } = render( + + ) + expect(container.querySelector('[role="dialog"]')).toBeNull() + }) + + it('renders form fields when open', () => { + render() + expect(screen.getByPlaceholderText('My Server')).toBeDefined() + expect(screen.getByText('Add Node')).toBeDefined() + }) + + it('does not call onSubmit when label is empty and shows error', () => { + const onSubmit = vi.fn() + render() + fireEvent.click(screen.getByText('Add')) + expect(onSubmit).not.toHaveBeenCalled() + expect(screen.getByText('Label is required')).toBeDefined() + }) + + it('calls onSubmit with form data when label is filled', () => { + const onSubmit = vi.fn() + const onClose = vi.fn() + render() + fireEvent.change(screen.getByPlaceholderText('My Server'), { target: { value: 'My NAS' } }) + fireEvent.click(screen.getByText('Add')) + expect(onSubmit).toHaveBeenCalledOnce() + expect(onSubmit.mock.calls[0][0].label).toBe('My NAS') + expect(onClose).toHaveBeenCalledOnce() + }) + + it('clears label error when user starts typing', () => { + render() + fireEvent.click(screen.getByText('Add')) + expect(screen.getByText('Label is required')).toBeDefined() + fireEvent.change(screen.getByPlaceholderText('My Server'), { target: { value: 'x' } }) + expect(screen.queryByText('Label is required')).toBeNull() + }) + + it('pre-fills form from initial prop', () => { + render( + + ) + const input = screen.getByPlaceholderText('My Server') as HTMLInputElement + expect(input.value).toBe('Pre-filled') + }) + + it('shows Save button text when title is Edit Node', () => { + render() + expect(screen.getByText('Save')).toBeDefined() + }) + + it('calls onClose when Cancel is clicked', () => { + const onClose = vi.fn() + render() + fireEvent.click(screen.getByText('Cancel')) + expect(onClose).toHaveBeenCalledOnce() + }) +})