import React from 'react'; import '@testing-library/jest-dom'; import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'; import { MemoryRouter, Route, Routes, useLocation } from 'react-router-dom'; import { ConfirmProvider } from './confirm'; import { PromptProvider } from './prompt'; import { ToastProvider } from './toast'; import { I18nProvider } from './i18n/I18nProvider'; import DashboardView from './components/DashboardView'; import RemindersView from './components/RemindersView'; import JobTable from './components/JobTable'; import { api } from './api'; const mockedApi = api as jest.Mocked; const pagedJobs = { items: [ { id: 42, jobTitle: 'Backend Developer', status: 'Waiting', dateApplied: new Date().toISOString(), daysSince: 10, company: { name: 'Acme' }, companyId: 1, needsFollowUp: true, followUpReason: 'Follow-up due soon', shortSummary: 'Strong backend match', tailoredCvText: 'Saved CV', notes: 'Notes\n\n<<>>\nSaved answer\n<<>>', workflowSignal: { actionKey: 'follow-up', reason: 'Follow-up due soon', workspaceTab: 'follow-up', followMode: 'waiting-update', needsAttention: true, hasPackageGap: false, needsInterviewPrep: false, needsFollowUpAction: true, hasTailoredCv: true, hasSavedApplicationAnswerDraft: true, hasInterviewPrepNotes: true, }, }, { id: 43, jobTitle: 'Platform Engineer', status: 'Applied', dateApplied: new Date().toISOString(), daysSince: 4, company: { name: 'Beta' }, companyId: 2, needsFollowUp: false, followUpReason: 'Tailored CV missing', shortSummary: 'Platform work', tailoredCvText: null, notes: null, workflowSignal: { actionKey: 'package-work', reason: 'Tailored CV missing for this role.', workspaceTab: 'tailored-cv', followMode: null, needsAttention: true, hasPackageGap: true, needsInterviewPrep: false, needsFollowUpAction: false, hasTailoredCv: false, hasSavedApplicationAnswerDraft: false, hasInterviewPrepNotes: false, }, }, ], total: 2, page: 1, pageSize: 15, }; const reminderItems = [ { id: 42, jobTitle: 'Backend Developer', status: 'Waiting', dateApplied: new Date().toISOString(), daysSince: 10, company: { name: 'Acme' }, needsFollowUp: true, followUpReason: 'Follow-up due soon', tailoredCvText: 'Saved CV', followUpAt: new Date().toISOString(), workflowSignal: { actionKey: 'follow-up', reason: 'Follow-up due soon', workspaceTab: 'follow-up', followMode: 'waiting-update', needsAttention: true, hasPackageGap: false, needsInterviewPrep: false, needsFollowUpAction: true, hasTailoredCv: true, hasSavedApplicationAnswerDraft: true, hasInterviewPrepNotes: true, }, }, { id: 43, jobTitle: 'Platform Engineer', status: 'Applied', dateApplied: new Date().toISOString(), daysSince: 4, company: { name: 'Beta' }, needsFollowUp: true, followUpReason: 'Tailored CV missing', tailoredCvText: null, followUpAt: new Date().toISOString(), workflowSignal: { actionKey: 'package-work', reason: 'Tailored CV missing for this role.', workspaceTab: 'tailored-cv', followMode: null, needsAttention: true, hasPackageGap: true, needsInterviewPrep: false, needsFollowUpAction: true, hasTailoredCv: false, hasSavedApplicationAnswerDraft: false, hasInterviewPrepNotes: false, }, }, ]; function setupApiMocks() { mockedApi.get.mockImplementation((url: string) => { if (url === '/companies') return Promise.resolve({ data: [{ id: 1, name: 'Acme' }, { id: 2, name: 'Beta' }] } as any); if (url === '/jobapplications/reminders') return Promise.resolve({ data: reminderItems } as any); if (url === '/jobapplications') return Promise.resolve({ data: pagedJobs } as any); if (url === '/jobapplications/stats') return Promise.resolve({ data: { total: 2, active: 2, deleted: 0, byStatus: {}, appliedLast30Days: 2, averageDaysSinceApplied: 7 } } as any); if (url === '/jobapplications/analytics-overview') return Promise.resolve({ data: { funnel: [], responseRateBySource: [], topCompanies: [], totalResponses: 1, totalActive: 2 } } as any); if (url === '/jobapplications/analytics' || url === '/jobapplications/tags') return Promise.resolve({ data: [] } as any); if (url === '/jobapplications/tag-trends') return Promise.resolve({ data: { months: [], series: [] } } as any); 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: 'maria@acme.test' }, tailoredCvText: 'Saved CV', coverLetterText: 'Saved cover letter', recruiterMessageDraft: 'Saved recruiter message', notes: 'Notes\n\n<<>>\nSaved answer\n<<>>', shortSummary: 'Strong backend match' } } as any); } if (url === '/jobapplications/43') { return Promise.resolve({ data: { id: 43, jobTitle: 'Platform Engineer', status: 'Applied', dateApplied: new Date().toISOString(), daysSince: 4, company: { name: 'Beta', recruiterEmail: 'recruiter@beta.test' }, tailoredCvText: '', coverLetterText: '', recruiterMessageDraft: '', notes: '', shortSummary: 'Platform work' } } as any); } if (url === '/jobapplications/42/history' || url === '/jobapplications/43/history') return Promise.resolve({ data: [] } as any); if (url === '/attachments/42' || url === '/attachments/43') return Promise.resolve({ data: [] } as any); if (url === '/jobapplications/42/followup-draft') { return Promise.resolve({ data: { subject: 'Re: Backend Developer application update', body: 'Hi Maria,\n\nFollow-up draft.\n\nThanks,\nDemo', reason: 'Follow-up due soon', suggestedSendOn: new Date().toISOString(), contextSummary: 'Saved application package material is available for reuse.', contextSignals: ['Saved cover letter available'], threadSubject: 'Backend Developer application update', lastCorrespondenceFrom: 'Maria Recruiter ', lastCorrespondenceAt: new Date().toISOString() } } as any); } return Promise.resolve({ data: [] } as any); }); } function LocationIndicator() { const location = useLocation(); return
{location.pathname}{location.search}
; } function renderLoop(initialPath: string) { return render( } /> } /> {}} columns={{ status: true, dateApplied: true, daysSince: true, jobUrl: false }} onColumnsChange={() => {}} mode="jobs" />} /> , ); } beforeEach(() => { window.localStorage.clear(); setupApiMocks(); }); afterEach(() => { jest.clearAllMocks(); }); test('dashboard attention card opens follow-up workspace', async () => { renderLoop('/dashboard'); expect(await screen.findByText(/needs follow-up/i)).toBeInTheDocument(); fireEvent.click(await screen.findByRole('button', { name: /follow up/i })); expect(await screen.findByText(/follow-up context/i)).toBeInTheDocument(); expect(await screen.findByText(/saved cover letter available/i)).toBeInTheDocument(); }); test('reminders open action routes tailored-cv gaps into the tailored cv workspace', async () => { renderLoop('/reminders'); expect(await screen.findByText(/missing tailored cv/i)).toBeInTheDocument(); const platformJob = await screen.findByText(/platform engineer/i); const platformCard = platformJob.closest('.MuiPaper-root') ?? platformJob.parentElement?.parentElement; expect(platformCard).toBeTruthy(); fireEvent.click(within(platformCard as HTMLElement).getByRole('button', { name: /build package/i })); await waitFor(() => { expect(screen.getByTestId('location-indicator')).toHaveTextContent('/jobs'); }); expect(await screen.findByText(/saved working material/i)).toBeInTheDocument(); }); test('job table urgency signals and next actions route into the shared workspace flow', async () => { const firstRender = renderLoop('/jobs'); fireEvent.click(await screen.findByRole('button', { name: /backend developer — follow up signal/i })); await waitFor(() => { expect(screen.getByTestId('location-indicator')).toHaveTextContent('/jobs'); }); expect(await screen.findByText(/follow-up context/i)).toBeInTheDocument(); expect(await screen.findByText(/saved cover letter available/i)).toBeInTheDocument(); firstRender.unmount(); renderLoop('/jobs'); fireEvent.click(await screen.findByRole('button', { name: /next action: platform engineer — build package/i })); await waitFor(() => { expect(screen.getByTestId('location-indicator')).toHaveTextContent('/jobs'); }); expect(await screen.findByText(/saved working material/i)).toBeInTheDocument(); expect(await screen.findByText(/platform work/i)).toBeInTheDocument(); });