import React from 'react'; import '@testing-library/jest-dom'; import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { ConfirmProvider } from './confirm'; import { PromptProvider } from './prompt'; import { ToastProvider } from './toast'; import { I18nProvider } from './i18n/I18nProvider'; import JobDetailsDialog from './components/JobDetailsDialog'; import { api } from './api'; jest.setTimeout(15000); jest.mock('./api', () => ({ api: { get: jest.fn(), post: jest.fn(), put: jest.fn(), patch: jest.fn(), delete: jest.fn(), interceptors: { request: { use: jest.fn() }, response: { use: jest.fn() } }, }, })); const mockedApi = api as jest.Mocked; function renderDialog() { return render( {}} /> , ); } beforeEach(() => { Object.assign(navigator, { clipboard: { writeText: jest.fn().mockResolvedValue(undefined), }, }); mockedApi.get.mockImplementation((url: string) => { if (url === '/jobapplications/42') { return Promise.resolve({ data: { id: 42, jobTitle: 'Backend Developer', status: 'Waiting', dateApplied: new Date().toISOString(), daysSince: 10, company: { name: 'Acme', recruiterEmail: 'recruiter@acme.test' }, tailoredCvText: 'Saved CV', coverLetterText: 'Saved cover letter', recruiterMessageDraft: 'Saved recruiter message', notes: 'Original notes\n\n<<>>\nSaved application answer\n<<>>', shortSummary: 'summary' } } as any); } if (url === '/auth/me') { return Promise.resolve({ data: { roles: [], profileCvText: 'Master CV text' } } as any); } if (url === '/jobapplications/42/history') { return Promise.resolve({ data: [] } as any); } if (url === '/attachments/42') { return Promise.resolve({ data: [{ id: 9, fileName: 'resume.pdf', uploadDate: new Date().toISOString(), fileType: 'application/pdf', fileSize: 1234, purpose: 'resume', useForAi: true }] } as any); } if (url === '/jobapplications/42/followup-draft') { return Promise.resolve({ data: { subject: 'Re: Backend Developer application update', body: 'Hi Maria,\n\nI wanted to follow up on the Backend Developer thread and reiterate my fit for owning the API layer.\n\nThanks,\nCasey', reason: 'Scheduled follow-up is due.', suggestedSendOn: new Date().toISOString(), contextSummary: 'Scheduled follow-up is due. Latest thread activity was on March 10, 2026. Saved application package material is available for reuse.', contextSignals: ['Saved cover letter available', 'Saved tailored CV available', 'Thread participants: Maria Recruiter , user@example.test'], threadSubject: 'Backend Developer application update', lastCorrespondenceFrom: 'Maria Recruiter ', lastCorrespondenceAt: new Date().toISOString(), } } as any); } return Promise.resolve({ data: [] } as any); }); mockedApi.post.mockResolvedValue({ data: {} } as any); mockedApi.put.mockResolvedValue({ data: {} } as any); }); afterEach(() => { jest.clearAllMocks(); }); test('follow-up workspace shows thread grounding and keeps sending manual', async () => { renderDialog(); fireEvent.click(await screen.findByRole('tab', { name: /follow up/i })); expect(await screen.findByText(/follow-up context/i)).toBeInTheDocument(); expect(await screen.findByText(/saved application package material is available for reuse/i)).toBeInTheDocument(); expect(await screen.findByText(/saved cover letter available/i)).toBeInTheDocument(); expect(await screen.findByText(/manual send only/i)).toBeInTheDocument(); expect(await screen.findByDisplayValue(/i wanted to follow up on the backend developer thread/i)).toBeInTheDocument(); const draft = screen.getByDisplayValue(/i wanted to follow up on the backend developer thread/i); fireEvent.change(draft, { target: { value: 'Hi Maria,\n\nEdited follow-up.\n\nThanks,\nCasey' } }); fireEvent.click(screen.getByRole('button', { name: /send and log email/i })); await waitFor(() => { expect(mockedApi.post).toHaveBeenCalledWith('/jobapplications/42/send-followup', { toEmail: 'recruiter@acme.test', subject: 'Re: Backend Developer application update', body: 'Hi Maria,\n\nEdited follow-up.\n\nThanks,\nCasey', nextFollowUpAt: expect.any(String), }); }); });