import React from 'react'; import '@testing-library/jest-dom'; import { fireEvent, render, screen, waitFor } 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'; import { JobApplication } from './types'; const mockedApi = api as jest.Mocked; function buildJob(overrides: Partial): JobApplication { return { id: 1, jobTitle: 'Backend Developer', company: { id: 1, name: 'Acme' }, companyId: 1, status: 'Waiting', dateApplied: new Date('2026-03-01T00:00:00Z').toISOString(), location: 'Oslo', salary: undefined, nextAction: undefined, followUpAt: new Date('2026-03-20T00:00:00Z').toISOString(), feedbackRequestedAt: undefined, responseReceived: false, responseDate: undefined, description: 'Role summary', translatedDescription: undefined, descriptionLanguage: undefined, tags: undefined, deadline: undefined, notes: 'General notes', coverLetterText: undefined, recruiterMessageDraft: undefined, jobUrl: undefined, shortSummary: 'Strong match', fullSummary: null, tailoredCvText: 'Saved CV', tailoredCvUpdatedAt: null, workflowSignal: { actionKey: 'follow-up', reason: 'Follow-up is due for this role.', workspaceTab: 'follow-up', followMode: 'waiting-update', needsAttention: true, hasPackageGap: false, needsInterviewPrep: false, needsFollowUpAction: true, hasTailoredCv: true, hasSavedApplicationAnswerDraft: true, hasInterviewPrepNotes: true, }, hasResume: true, hasCoverLetter: false, hasPortfolio: false, hasOtherAttachment: false, daysSince: 10, isDeleted: false, deletedAt: undefined, needsFollowUp: true, followUpReason: 'Follow-up is due for this role.', ...overrides, }; } function setupApiMocks({ reminders, jobs }: { reminders?: JobApplication[]; jobs?: JobApplication[] }) { 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: reminders ?? [] } as any); if (url === '/jobapplications') return Promise.resolve({ data: { items: jobs ?? [], total: jobs?.length ?? 0, page: 1, pageSize: 15 } } as any); if (url === '/jobapplications/stats') return Promise.resolve({ data: { total: reminders?.length ?? 0, active: reminders?.length ?? 0, deleted: 0, byStatus: {}, appliedLast30Days: reminders?.length ?? 0, averageDaysSinceApplied: 7 } } as any); if (url === '/jobapplications/analytics-overview') return Promise.resolve({ data: { funnel: [], responseRateBySource: [], topCompanies: [], totalResponses: 1, totalActive: reminders?.length ?? 0 } } 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); return Promise.resolve({ data: [] } as any); }); } function LocationIndicator() { const location = useLocation(); return
{location.pathname}{location.search}
; } function renderWithProviders(initialPath: string, routes: React.ReactNode) { return render( {routes} , ); } beforeEach(() => { window.localStorage.clear(); }); afterEach(() => { jest.clearAllMocks(); }); test('follow-up workflow signals route all overview surfaces to the same follow-up workspace', async () => { const job = buildJob({ id: 42, followUpReason: 'Tailored CV missing', workflowSignal: { actionKey: 'follow-up', reason: 'Follow-up is due for this role.', workspaceTab: 'follow-up', followMode: 'waiting-update', needsAttention: true, hasPackageGap: false, needsInterviewPrep: false, needsFollowUpAction: true, hasTailoredCv: true, hasSavedApplicationAnswerDraft: true, hasInterviewPrepNotes: true, }, }); setupApiMocks({ reminders: [job], jobs: [job] }); const dashboardRender = renderWithProviders('/dashboard', <> } /> } /> ); await screen.findByText(/follow-up is due for this role/i); fireEvent.click(await screen.findByRole('button', { name: /follow up/i })); await waitFor(() => expect(screen.getByTestId('location-indicator')).toHaveTextContent('/jobs?open=42&tab=4&followMode=waiting-update')); dashboardRender.unmount(); setupApiMocks({ reminders: [job], jobs: [job] }); const remindersRender = renderWithProviders('/reminders', <> } /> } /> ); fireEvent.click(await screen.findByRole('button', { name: /follow up/i })); await waitFor(() => expect(screen.getByTestId('location-indicator')).toHaveTextContent('/jobs?open=42&tab=4&followMode=waiting-update')); remindersRender.unmount(); setupApiMocks({ reminders: [job], jobs: [job] }); renderWithProviders('/table', <> {}} columns={{ status: true, dateApplied: true, daysSince: true, jobUrl: false }} onColumnsChange={() => {}} mode="jobs" />} /> } /> ); fireEvent.click(await screen.findByRole('button', { name: /backend developer — follow up signal/i })); await waitFor(() => expect(screen.getByTestId('location-indicator')).toHaveTextContent('/jobs?open=42&tab=4&followMode=waiting-update')); }); test('package-work workflow signals route all overview surfaces to the shared tailored-cv workspace', async () => { const job = buildJob({ id: 43, jobTitle: 'Platform Engineer', followUpReason: 'Follow-up due soon', workflowSignal: { actionKey: 'package-work', reason: 'Saved application answers still need work.', workspaceTab: 'tailored-cv', followMode: null, needsAttention: true, hasPackageGap: true, needsInterviewPrep: false, needsFollowUpAction: true, hasTailoredCv: true, hasSavedApplicationAnswerDraft: false, hasInterviewPrepNotes: true, }, }); setupApiMocks({ reminders: [job], jobs: [job] }); const dashboardRender = renderWithProviders('/dashboard', <> } /> } /> ); fireEvent.click(await screen.findByRole('button', { name: /build package/i })); await waitFor(() => expect(screen.getByTestId('location-indicator')).toHaveTextContent('/jobs?open=43&tab=3')); dashboardRender.unmount(); setupApiMocks({ reminders: [job], jobs: [job] }); const remindersRender = renderWithProviders('/reminders', <> } /> } /> ); fireEvent.click(await screen.findByRole('button', { name: /build package/i })); await waitFor(() => expect(screen.getByTestId('location-indicator')).toHaveTextContent('/jobs?open=43&tab=3')); remindersRender.unmount(); setupApiMocks({ reminders: [job], jobs: [job] }); renderWithProviders('/table', <> {}} columns={{ status: true, dateApplied: true, daysSince: true, jobUrl: false }} onColumnsChange={() => {}} mode="jobs" />} /> } /> ); fireEvent.click(await screen.findByRole('button', { name: /platform engineer — build package signal/i })); await waitFor(() => expect(screen.getByTestId('location-indicator')).toHaveTextContent('/jobs?open=43&tab=3')); }); test('job table readiness filter follows workflow signals instead of raw notes or cv text heuristics', async () => { const packageGapJob = buildJob({ id: 44, jobTitle: 'Application Engineer', status: 'Applied', notes: 'General notes only', tailoredCvText: 'Saved tailored CV', workflowSignal: { actionKey: 'package-work', reason: 'Saved application answers still need work.', workspaceTab: 'tailored-cv', followMode: null, needsAttention: true, hasPackageGap: true, needsInterviewPrep: false, needsFollowUpAction: false, hasTailoredCv: true, hasSavedApplicationAnswerDraft: false, hasInterviewPrepNotes: true, }, }); const readyRejectedJob = buildJob({ id: 45, jobTitle: 'Operations Analyst', status: 'Rejected', notes: 'Some notes', tailoredCvText: null, needsFollowUp: false, followUpReason: null, workflowSignal: { actionKey: 'review-readiness', reason: 'No urgent workflow gaps are blocking this job right now.', workspaceTab: 'readiness', followMode: null, needsAttention: false, hasPackageGap: false, needsInterviewPrep: false, needsFollowUpAction: false, hasTailoredCv: false, hasSavedApplicationAnswerDraft: false, hasInterviewPrepNotes: false, }, }); setupApiMocks({ jobs: [packageGapJob, readyRejectedJob] }); renderWithProviders('/table', <> {}} columns={{ status: true, dateApplied: true, daysSince: true, jobUrl: false }} onColumnsChange={() => {}} mode="jobs" />} /> ); const readinessSelect = (await screen.findAllByRole('combobox')).find((element) => /all readiness/i.test(element.textContent ?? '')); expect(readinessSelect).toBeTruthy(); fireEvent.mouseDown(readinessSelect as HTMLElement); fireEvent.click(await screen.findByRole('option', { name: /needs work/i })); expect(await screen.findByText(/application engineer/i)).toBeInTheDocument(); expect(screen.queryByText(/operations analyst/i)).not.toBeInTheDocument(); });