133 lines
5.4 KiB
TypeScript
133 lines
5.4 KiB
TypeScript
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<typeof api>;
|
|
|
|
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(
|
|
<ToastProvider>
|
|
<I18nProvider>
|
|
<ConfirmProvider>
|
|
<PromptProvider>
|
|
<JobDetailsDialog open jobId={42} onClose={() => {}} initialTab={5} />
|
|
</PromptProvider>
|
|
</ConfirmProvider>
|
|
</I18nProvider>
|
|
</ToastProvider>,
|
|
);
|
|
}
|
|
|
|
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();
|
|
});
|