import React from 'react'; import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import ExperimentForm from '../src/components/ExperimentForm'; import * as client from '../src/api/client'; jest.mock('../src/api/client', () => ({ experimentsApi: { create: jest.fn(), update: jest.fn(), }, })); const onSuccess = jest.fn(); const onCancel = jest.fn(); beforeEach(() => { jest.clearAllMocks(); }); describe('ExperimentForm', () => { it('renders title input and submit button', () => { render(); expect(screen.getByLabelText(/experiment title/i)).toBeInTheDocument(); expect(screen.getByRole('button', { name: /create experiment/i })).toBeInTheDocument(); }); it('shows validation error when submitting empty title', async () => { render(); fireEvent.click(screen.getByRole('button', { name: /create experiment/i })); expect(await screen.findByRole('alert')).toHaveTextContent(/title is required/i); expect(client.experimentsApi.create).not.toHaveBeenCalled(); }); it('shows error when title exceeds 255 chars', async () => { render(); await userEvent.type(screen.getByLabelText(/experiment title/i), 'x'.repeat(256)); fireEvent.click(screen.getByRole('button', { name: /create experiment/i })); expect(await screen.findByRole('alert')).toHaveTextContent(/255/i); expect(client.experimentsApi.create).not.toHaveBeenCalled(); }); it('calls create API with trimmed title and calls onSuccess', async () => { const experiment = { id: 'abc-123', title: 'New Study' }; client.experimentsApi.create.mockResolvedValue(experiment); render(); await userEvent.type(screen.getByLabelText(/experiment title/i), 'New Study'); fireEvent.click(screen.getByRole('button', { name: /create experiment/i })); await waitFor(() => expect(client.experimentsApi.create).toHaveBeenCalledWith({ title: 'New Study' })); await waitFor(() => expect(onSuccess).toHaveBeenCalledWith(experiment)); }); it('shows "Save Changes" button for existing experiment', () => { render( ); expect(screen.getByRole('button', { name: /save changes/i })).toBeInTheDocument(); expect(screen.getByDisplayValue('Old Title')).toBeInTheDocument(); }); it('calls update API for existing experiment', async () => { const updated = { id: 'abc', title: 'Updated Title' }; client.experimentsApi.update.mockResolvedValue(updated); render( ); const input = screen.getByDisplayValue('Old Title'); await userEvent.clear(input); await userEvent.type(input, 'Updated Title'); fireEvent.click(screen.getByRole('button', { name: /save changes/i })); await waitFor(() => expect(client.experimentsApi.update).toHaveBeenCalledWith('abc', { title: 'Updated Title' }) ); await waitFor(() => expect(onSuccess).toHaveBeenCalledWith(updated)); }); it('calls onCancel when Cancel is clicked', () => { render(); fireEvent.click(screen.getByRole('button', { name: /cancel/i })); expect(onCancel).toHaveBeenCalled(); }); it('shows API error when create fails', async () => { client.experimentsApi.create.mockRejectedValue({ message: 'Server error', details: null }); render(); await userEvent.type(screen.getByLabelText(/experiment title/i), 'Test'); fireEvent.click(screen.getByRole('button', { name: /create experiment/i })); expect(await screen.findByRole('alert')).toHaveTextContent(/server error/i); }); });