import { fireEvent, render, screen, waitFor } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' import { MemoryRouter, Route, Routes } from 'react-router-dom' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import EditTaskPage from '@/pages/EditTaskPage' import * as entitiesApi from '@/api/entitiesApi' import * as tasksApi from '@/api/tasksApi' type RenderPageOptions = { task?: typeof mockTask } const mockNavigate = vi.fn() vi.mock('react-router-dom', async () => { const actual = await vi.importActual('react-router-dom') return { ...actual, useNavigate: () => mockNavigate } }) vi.mock('@/api/entitiesApi') vi.mock('@/api/tasksApi') function renderPage(options: RenderPageOptions = {}) { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }) const wrapper = ({ children }: { children: React.ReactNode }) => ( ) vi.mocked(tasksApi.getTask).mockResolvedValue(options.task ?? mockTask) render(, { wrapper }) return { queryClient } } const mockEntity = { id: 'entity-1', name: 'Entity A', email: 'a@a.com', jobTitle: 'Ops', personality: 'Formal', scheduleCron: '0 9 * * 1', contextWindowDays: 3, active: true, createdAt: '', } const mockTask = { id: 'task-1', entityId: 'entity-1', name: 'Weekly Check-in', prompt: 'Summarize jokes', scheduleCron: '0 9 * * 1', emailLookback: 'last_week' as const, active: true, createdAt: '2026-03-26T10:00:00Z', } describe('EditTaskPage', () => { beforeEach(() => { vi.clearAllMocks() vi.mocked(entitiesApi.getEntity).mockResolvedValue(mockEntity) vi.mocked(tasksApi.getTask).mockResolvedValue(mockTask) vi.mocked(tasksApi.buildTaskPreviewPrompt).mockImplementation( (entity, task) => `PROMPT FOR ${entity.name}: ${task.name} | ${task.prompt} | ${task.scheduleCron} | ${task.emailLookback}` ) vi.mocked(tasksApi.activateTask).mockResolvedValue({ ...mockTask, active: true }) vi.mocked(tasksApi.inactivateTask).mockResolvedValue({ ...mockTask, active: false }) vi.mocked(tasksApi.deleteTask).mockResolvedValue(undefined) mockNavigate.mockClear() }) it('should_renderPrefilledTaskForm_when_pageLoads', async () => { renderPage() await screen.findByRole('link', { name: /back to entity a/i }) expect(screen.getByRole('heading', { name: /edit task/i })).toBeInTheDocument() expect(screen.getByLabelText(/task name/i)).toHaveValue('Weekly Check-in') expect(screen.getByLabelText(/task prompt/i)).toHaveValue('Summarize jokes') expect(screen.getByLabelText(/^Email Period$/i)).toHaveValue('last_week') expect(screen.getByLabelText(/^Minute$/i)).toHaveValue('0') expect(screen.getByLabelText(/^Hour$/i)).toHaveValue('9') expect(screen.getByLabelText(/^Day of Month$/i)).toHaveValue('*') expect(screen.getByLabelText(/^Month$/i)).toHaveValue('*') expect(screen.getByLabelText(/^Day of Week$/i)).toHaveValue('1') }) it('should_generatePreviewAndUpdateTask_when_formSubmitted', async () => { vi.mocked(tasksApi.generateTaskPreview).mockResolvedValue('SUBJECT: Preview\nBODY:\nFormal nonsense') vi.mocked(tasksApi.updateTask).mockResolvedValue({ ...mockTask, name: 'Daily Check-in', prompt: 'Ask about ceremonial coffee', scheduleCron: '0 8 * * 1-5', emailLookback: 'last_day', }) const { queryClient } = renderPage() const invalidateQueriesSpy = vi.spyOn(queryClient, 'invalidateQueries') await screen.findByRole('link', { name: /back to entity a/i }) fireEvent.change(screen.getByLabelText(/task name/i), { target: { value: 'Daily Check-in' } }) fireEvent.change(screen.getByLabelText(/task prompt/i), { target: { value: 'Ask about ceremonial coffee' }, }) fireEvent.change(screen.getByLabelText(/^Email Period$/i), { target: { value: 'last_day' }, }) fireEvent.click(screen.getByRole('button', { name: /Weekdays/i })) fireEvent.change(screen.getByLabelText(/^Hour$/i), { target: { value: '8' } }) expect(screen.getByText(/Final Prompt/i)).toBeInTheDocument() expect( screen.getByText( 'PROMPT FOR Entity A: Daily Check-in | Ask about ceremonial coffee | 0 8 * * 1-5 | last_day' ) ).toBeInTheDocument() fireEvent.click(screen.getByRole('button', { name: /generate test message/i })) await waitFor(() => { expect(tasksApi.generateTaskPreview).toHaveBeenCalled() expect(vi.mocked(tasksApi.buildTaskPreviewPrompt)).toHaveBeenCalledWith( mockEntity, expect.objectContaining({ entityId: 'entity-1', name: 'Daily Check-in', prompt: 'Ask about ceremonial coffee', scheduleCron: '0 8 * * 1-5', emailLookback: 'last_day', }) ) expect(vi.mocked(tasksApi.generateTaskPreview).mock.calls[0][0]).toEqual( expect.objectContaining({ entity: mockEntity, task: expect.objectContaining({ entityId: 'entity-1', name: 'Daily Check-in', prompt: 'Ask about ceremonial coffee', scheduleCron: '0 8 * * 1-5', emailLookback: 'last_day', }), }) ) expect(screen.getByText(/Formal nonsense/i)).toBeInTheDocument() }) fireEvent.click(screen.getByRole('button', { name: /save changes/i })) await waitFor(() => { expect(tasksApi.updateTask).toHaveBeenCalledWith('task-1', { entityId: 'entity-1', name: 'Daily Check-in', prompt: 'Ask about ceremonial coffee', scheduleCron: '0 8 * * 1-5', emailLookback: 'last_day', }) expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['entity-tasks', 'entity-1'] }) expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['entity-task', 'task-1'] }) expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['entity-tasks'] }) expect(mockNavigate).toHaveBeenCalledWith('/entities/entity-1') }) }) it('should_showReadablePreviewError_when_generationFails', async () => { vi.mocked(tasksApi.generateTaskPreview).mockRejectedValue( new Error('Unable to generate a test message from the local model. Connection refused') ) renderPage() await screen.findByRole('link', { name: /back to entity a/i }) fireEvent.click(screen.getByRole('button', { name: /generate test message/i })) await waitFor(() => { expect( screen.getByText(/Unable to generate a test message from the local model. Connection refused/i) ).toBeInTheDocument() }) }) it('should_renderActivateButton_when_taskIsInactive', async () => { renderPage({ task: { ...mockTask, active: false } }) const activateButton = await screen.findByRole('button', { name: /activate/i }) expect(activateButton).toHaveClass('border-emerald-700') expect(activateButton).toHaveClass('text-emerald-200') expect(screen.queryByRole('button', { name: /^inactivate$/i })).not.toBeInTheDocument() }) it('should_inactivateTaskAndNavigateBack_when_inactivateClicked', async () => { const { queryClient } = renderPage() const invalidateQueriesSpy = vi.spyOn(queryClient, 'invalidateQueries') const inactivateButton = await screen.findByRole('button', { name: /^inactivate$/i }) expect(inactivateButton).toHaveClass('border-amber-700') expect(inactivateButton).toHaveClass('text-amber-200') fireEvent.click(inactivateButton) await waitFor(() => { expect(tasksApi.inactivateTask).toHaveBeenCalledWith('task-1') expect(tasksApi.activateTask).not.toHaveBeenCalled() expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['entity-tasks', 'entity-1'] }) expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['entity-task', 'task-1'] }) expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['entity-tasks'] }) expect(mockNavigate).toHaveBeenCalledWith('/entities/entity-1') }) }) it('should_activateTaskAndNavigateBack_when_activateClicked', async () => { const { queryClient } = renderPage({ task: { ...mockTask, active: false } }) const invalidateQueriesSpy = vi.spyOn(queryClient, 'invalidateQueries') const activateButton = await screen.findByRole('button', { name: /^activate$/i }) fireEvent.click(activateButton) await waitFor(() => { expect(tasksApi.activateTask).toHaveBeenCalledWith('task-1') expect(tasksApi.inactivateTask).not.toHaveBeenCalled() expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['entity-tasks', 'entity-1'] }) expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['entity-task', 'task-1'] }) expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['entity-tasks'] }) expect(mockNavigate).toHaveBeenCalledWith('/entities/entity-1') }) }) it('should_deleteTaskAndNavigateBack_when_deleteClicked', async () => { const { queryClient } = renderPage() const invalidateQueriesSpy = vi.spyOn(queryClient, 'invalidateQueries') await screen.findByRole('link', { name: /back to entity a/i }) fireEvent.click(screen.getByRole('button', { name: /delete/i })) await waitFor(() => { expect(tasksApi.deleteTask).toHaveBeenCalledWith('task-1') expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['entity-tasks', 'entity-1'] }) expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['entity-task', 'task-1'] }) expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['entity-tasks'] }) expect(mockNavigate).toHaveBeenCalledWith('/entities/entity-1') }) }) })