Add frontend coverage for attachment metadata and strategy snapshot

This commit is contained in:
cesnimda
2026-03-23 23:04:41 +01:00
parent 603f5e8b74
commit eb4b517d58
3 changed files with 85 additions and 12 deletions
+3
View File
@@ -46,3 +46,6 @@ todo jobtracker.txt
*.db-* *.db-*
Attachments/ Attachments/
website_details.md website_details.md
# Private local test files
/tmp/
+53 -6
View File
@@ -1,10 +1,11 @@
import React from 'react'; import React from 'react';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import { fireEvent, render, screen } from '@testing-library/react'; import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { ConfirmProvider } from './confirm'; import { ConfirmProvider } from './confirm';
import { PromptProvider } from './prompt'; import { PromptProvider } from './prompt';
import Attachments from './components/Attachments'; import Attachments from './components/Attachments';
import { ToastProvider } from './toast'; import { ToastProvider } from './toast';
import { I18nProvider } from './i18n/I18nProvider';
import { api } from './api'; import { api } from './api';
jest.mock('./api', () => ({ jest.mock('./api', () => ({
@@ -23,11 +24,13 @@ test('attachments empty state renders drag and drop guidance', async () => {
render( render(
<ToastProvider> <ToastProvider>
<ConfirmProvider> <I18nProvider>
<PromptProvider> <ConfirmProvider>
<Attachments jobId={42} /> <PromptProvider>
</PromptProvider> <Attachments jobId={42} />
</ConfirmProvider> </PromptProvider>
</ConfirmProvider>
</I18nProvider>
</ToastProvider>, </ToastProvider>,
); );
@@ -35,3 +38,47 @@ test('attachments empty state renders drag and drop guidance', async () => {
expect(screen.getByText(/no attachments yet/i)).toBeInTheDocument(); expect(screen.getByText(/no attachments yet/i)).toBeInTheDocument();
expect(screen.getByText(/max 10 mb each/i)).toBeInTheDocument(); expect(screen.getByText(/max 10 mb each/i)).toBeInTheDocument();
}); });
test('attachments metadata controls update purpose and ai usage', async () => {
mockedApi.get.mockResolvedValueOnce({
data: [
{
id: 7,
fileName: 'resume.pdf',
uploadDate: new Date().toISOString(),
fileType: 'application/pdf',
fileSize: 2048,
purpose: 'resume',
useForAi: true,
},
],
} as any);
mockedApi.patch.mockResolvedValue({ data: {} } as any);
render(
<ToastProvider>
<I18nProvider>
<ConfirmProvider>
<PromptProvider>
<Attachments jobId={42} />
</PromptProvider>
</ConfirmProvider>
</I18nProvider>
</ToastProvider>,
);
expect(await screen.findByDisplayValue(/resume/i)).toBeInTheDocument();
fireEvent.mouseDown(screen.getByRole('combobox'));
fireEvent.click(await screen.findByRole('option', { name: /portfolio/i }));
await waitFor(() => {
expect(mockedApi.patch).toHaveBeenCalledWith('/attachments/7', expect.objectContaining({ purpose: 'portfolio', useForAi: true }));
});
fireEvent.click(screen.getByRole('switch'));
await waitFor(() => {
expect(mockedApi.patch).toHaveBeenCalledWith('/attachments/7', expect.objectContaining({ purpose: 'portfolio', useForAi: false }));
});
});
@@ -4,6 +4,7 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { ConfirmProvider } from './confirm'; import { ConfirmProvider } from './confirm';
import { PromptProvider } from './prompt'; import { PromptProvider } from './prompt';
import { ToastProvider } from './toast'; import { ToastProvider } from './toast';
import { I18nProvider } from './i18n/I18nProvider';
import JobDetailsDialog from './components/JobDetailsDialog'; import JobDetailsDialog from './components/JobDetailsDialog';
import { api } from './api'; import { api } from './api';
@@ -25,11 +26,13 @@ const mockedApi = api as jest.Mocked<typeof api>;
function renderDialog() { function renderDialog() {
return render( return render(
<ToastProvider> <ToastProvider>
<ConfirmProvider> <I18nProvider>
<PromptProvider> <ConfirmProvider>
<JobDetailsDialog open jobId={42} onClose={() => {}} /> <PromptProvider>
</PromptProvider> <JobDetailsDialog open jobId={42} onClose={() => {}} />
</ConfirmProvider> </PromptProvider>
</ConfirmProvider>
</I18nProvider>
</ToastProvider>, </ToastProvider>,
); );
} }
@@ -51,9 +54,18 @@ beforeEach(() => {
if (url === '/jobapplications/42/history') { if (url === '/jobapplications/42/history') {
return Promise.resolve({ data: [] } as any); 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/candidate-fit') {
return Promise.resolve({ data: { matchSummary: 'Strong fit summary', fitLevel: 'Strong match', matchScore: 84, strengths: ['.NET'], gaps: ['Kubernetes'], mention: [], avoid: [], cvImprovements: [], missingKeywords: [], interviewPrep: [], tailoredPitch: 'Pitch', guidance: { cv: [], coverLetter: [], interview: [], recruiterMessage: [] } } } as any);
}
if (url === '/jobapplications/42/focus-plan') {
return Promise.resolve({ data: { strategicSummary: 'Lead with backend delivery and measurable outcomes.', immediatePriorities: ['Highlight .NET ownership'], cvBulletIdeas: [], proofPointsToLeadWith: [], coverLetterAngles: [], followUpApproach: [] } } as any);
}
return Promise.resolve({ data: [] } as any); return Promise.resolve({ data: [] } as any);
}); });
mockedApi.post.mockResolvedValue({ data: { tailoredCvText: 'Generated CV', coverLetterDraft: 'Draft letter', applicationAnswerDraft: 'Draft answer', recruiterMessageDraft: 'Recruiter hello', keyPoints: ['Lead with .NET'] } } as any); mockedApi.post.mockResolvedValue({ data: { tailoredCvText: 'Generated CV', coverLetterDraft: 'Draft letter', applicationAnswerDraft: 'Draft answer', recruiterMessageDraft: 'Recruiter hello', keyPoints: ['Lead with .NET'], attachmentSignals: [], attachmentFilesUsed: [], coverLetterVariants: ['Variant A'], recruiterMessageVariants: ['Variant B'] } } as any);
mockedApi.put.mockResolvedValue({ data: {} } as any); mockedApi.put.mockResolvedValue({ data: {} } as any);
}); });
@@ -70,6 +82,7 @@ test('generated application package can be edited and saved', async () => {
fireEvent.click(screen.getByRole('button', { name: /generate application package/i })); fireEvent.click(screen.getByRole('button', { name: /generate application package/i }));
expect(await screen.findByDisplayValue('Generated CV')).toBeInTheDocument(); expect(await screen.findByDisplayValue('Generated CV')).toBeInTheDocument();
expect(await screen.findByText(/cover letter variants/i)).toBeInTheDocument();
const coverLetter = await screen.findByDisplayValue('Draft letter'); const coverLetter = await screen.findByDisplayValue('Draft letter');
fireEvent.change(coverLetter, { target: { value: 'Edited cover letter' } }); fireEvent.change(coverLetter, { target: { value: 'Edited cover letter' } });
@@ -80,3 +93,13 @@ test('generated application package can be edited and saved', async () => {
expect(mockedApi.put).toHaveBeenCalledWith('/jobapplications/42/application-drafts', { coverLetterText: 'Edited cover letter' }); expect(mockedApi.put).toHaveBeenCalledWith('/jobapplications/42/application-drafts', { coverLetterText: 'Edited cover letter' });
}); });
}); });
test('strategy snapshot can be generated from overview', async () => {
renderDialog();
expect(await screen.findByRole('button', { name: /generate strategy snapshot/i })).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: /generate strategy snapshot/i }));
expect(await screen.findByText(/lead with backend delivery and measurable outcomes/i)).toBeInTheDocument();
expect(await screen.findByText(/highlight \.net ownership/i)).toBeInTheDocument();
});