diff --git a/.gitignore b/.gitignore index 2240070..1eab56b 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,6 @@ todo jobtracker.txt *.db-* Attachments/ website_details.md + +# Private local test files +/tmp/ diff --git a/job-tracker-ui/src/attachments.test.tsx b/job-tracker-ui/src/attachments.test.tsx index d955239..dfb195b 100644 --- a/job-tracker-ui/src/attachments.test.tsx +++ b/job-tracker-ui/src/attachments.test.tsx @@ -1,10 +1,11 @@ import React from 'react'; 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 { PromptProvider } from './prompt'; import Attachments from './components/Attachments'; import { ToastProvider } from './toast'; +import { I18nProvider } from './i18n/I18nProvider'; import { api } from './api'; jest.mock('./api', () => ({ @@ -23,11 +24,13 @@ test('attachments empty state renders drag and drop guidance', async () => { render( - - - - - + + + + + + + , ); @@ -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(/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( + + + + + + + + + , + ); + + 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 })); + }); +}); diff --git a/job-tracker-ui/src/job-details-generated-drafts.test.tsx b/job-tracker-ui/src/job-details-generated-drafts.test.tsx index ce1b615..7fbfb2d 100644 --- a/job-tracker-ui/src/job-details-generated-drafts.test.tsx +++ b/job-tracker-ui/src/job-details-generated-drafts.test.tsx @@ -4,6 +4,7 @@ import { fireEvent, render, screen, waitFor } 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'; @@ -25,11 +26,13 @@ const mockedApi = api as jest.Mocked; function renderDialog() { return render( - - - {}} /> - - + + + + {}} /> + + + , ); } @@ -51,9 +54,18 @@ beforeEach(() => { if (url === '/jobapplications/42/history') { 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); }); - 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); }); @@ -70,6 +82,7 @@ test('generated application package can be edited and saved', async () => { fireEvent.click(screen.getByRole('button', { name: /generate application package/i })); expect(await screen.findByDisplayValue('Generated CV')).toBeInTheDocument(); + expect(await screen.findByText(/cover letter variants/i)).toBeInTheDocument(); const coverLetter = await screen.findByDisplayValue('Draft 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' }); }); }); + +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(); +});