import React from 'react'; import '@testing-library/jest-dom'; import { fireEvent, render, screen } 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(() => Promise.resolve({ data: {} })), put: jest.fn(() => Promise.resolve({ data: {} })), patch: jest.fn(() => Promise.resolve({ data: {} })), delete: jest.fn(() => Promise.resolve({ data: {} })), interceptors: { request: { use: jest.fn() }, response: { use: jest.fn() } }, }, })); const mockedApi = api as jest.Mocked; const matchScore = { score: 82, band: 'Strong', matchedCount: 4, totalKeywords: 6, matchedKeywords: ['C#', '.NET', 'SQL', 'Docker'], missingKeywords: ['Kubernetes', 'GraphQL'], sectionCoverage: [ { section: 'Skills', matched: 4, total: 6 }, { section: 'Experience', matched: 3, total: 6 }, ], hasEnoughSignal: true, learningRecommendations: [ { id: 71, keyword: 'Kubernetes', status: 'pending' }, { id: 72, keyword: 'GraphQL', status: 'pending' }, ], }; function renderDialog() { return render( {}} initialTab={5} /> , ); } beforeEach(() => { mockedApi.get.mockImplementation((url: string) => { if (url === '/jobapplications/42') { return Promise.resolve({ data: { id: 42, jobTitle: 'Backend Developer', status: 'Applied', dateApplied: new Date().toISOString(), daysSince: 3, company: { name: 'Acme' } } } as any); } if (url === '/jobapplications/42/match-score') { return Promise.resolve({ data: matchScore } as any); } if (url === '/auth/me') return Promise.resolve({ data: { roles: [] } } as any); if (url === '/jobapplications/42/history') return Promise.resolve({ data: [] } as any); if (url === '/attachments/42') return Promise.resolve({ data: [] } as any); // Candidate-fit AI narrative: leave pending-ish/empty so we only assert on the fast panel. if (url === '/jobapplications/42/candidate-fit') return Promise.resolve({ data: null } as any); return Promise.resolve({ data: {} } as any); }); }); afterEach(() => { jest.clearAllMocks(); }); test('match score panel shows the score and honest important-term labels', async () => { renderDialog(); expect(await screen.findByText('82%')).toBeInTheDocument(); expect(await screen.findByText(/strong match/i)).toBeInTheDocument(); expect(await screen.findByText('4/6 important terms')).toBeInTheDocument(); expect(await screen.findByText('Important terms already in your CV')).toBeInTheDocument(); expect(await screen.findByText('Important terms from the job to review')).toBeInTheDocument(); expect(screen.queryByText(/matched keywords/i)).not.toBeInTheDocument(); // Matched keyword chips expect(await screen.findByText('C#')).toBeInTheDocument(); expect(await screen.findByText('Docker')).toBeInTheDocument(); // Missing keyword chips expect((await screen.findAllByText('Kubernetes')).length).toBeGreaterThan(0); expect((await screen.findAllByText('GraphQL')).length).toBeGreaterThan(0); // Section coverage expect(await screen.findByText('Skills: 4/6')).toBeInTheDocument(); }); test('learning recommendations can be completed or dismissed', async () => { renderDialog(); fireEvent.click((await screen.findAllByRole('button', { name: /mark learned/i }))[0]); expect(mockedApi.patch).toHaveBeenCalledWith('/jobapplications/42/checklist/71', { status: 'done' }); expect(await screen.findByText('Learned')).toBeInTheDocument(); fireEvent.click(screen.getAllByRole('button', { name: /dismiss/i })[0]); expect(mockedApi.patch).toHaveBeenCalledWith('/jobapplications/42/checklist/72', { status: 'dismissed' }); expect(await screen.findByText('1 dismissed')).toBeInTheDocument(); }); test('match score panel degrades gracefully when there is not enough signal', async () => { mockedApi.get.mockImplementation((url: string) => { if (url === '/jobapplications/42') { return Promise.resolve({ data: { id: 42, jobTitle: 'Backend Developer', status: 'Applied', dateApplied: new Date().toISOString(), daysSince: 3, company: { name: 'Acme' } } } as any); } if (url === '/jobapplications/42/match-score') { return Promise.resolve({ data: { ...matchScore, score: 0, band: 'Unknown', matchedCount: 0, matchedKeywords: [], missingKeywords: [], sectionCoverage: [], hasEnoughSignal: false } } as any); } if (url === '/auth/me') return Promise.resolve({ data: { roles: [] } } as any); if (url === '/jobapplications/42/history') return Promise.resolve({ data: [] } as any); if (url === '/attachments/42') return Promise.resolve({ data: [] } as any); if (url === '/jobapplications/42/candidate-fit') return Promise.resolve({ data: null } as any); return Promise.resolve({ data: {} } as any); }); renderDialog(); expect(await screen.findByText('—')).toBeInTheDocument(); expect(await screen.findByText(/not enough signal/i)).toBeInTheDocument(); });