import React from 'react'; import '@testing-library/jest-dom'; import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { ToastProvider } from './toast'; import { I18nProvider } from './i18n/I18nProvider'; import ProfilePage from './pages/ProfilePage'; import { api } from './api'; jest.mock('./api', () => ({ api: { get: jest.fn(), post: jest.fn(), put: jest.fn(), patch: jest.fn(), delete: jest.fn(), interceptors: { request: { use: jest.fn() }, response: { use: jest.fn() } }, }, })); jest.mock('./components/GoogleAuthCard', () => () => null); jest.mock('./components/CropImageDialog', () => () => null); const mockedApi = api as jest.Mocked; const structuredCv = { version: '1', metadata: { profileVersion: 3, appliedExtractionRunId: 12, updatedAtUtc: '2026-03-28T12:00:00Z', fields: { 'contact.fullName': { confidence: 0.92, method: 'llm', sourceBlockId: 'block-1', reviewState: 'suggested', sourceSnippet: 'Demo User' }, summary: { confidence: 0.71, method: 'deterministic', sourceBlockId: 'block-2', reviewState: 'suggested', sourceSnippet: 'Built backend systems' }, skills: { confidence: 0.68, method: 'deterministic', sourceBlockId: 'block-3', reviewState: 'suggested', sourceSnippet: '.NET' }, }, }, contact: { fullName: 'Demo User', headline: 'Backend Developer', email: 'demo@example.com', }, summary: ['Built backend systems'], jobs: [ { title: 'System Developer', company: 'Demo Co', location: 'Oslo', start: '2020', end: '2024', isCurrent: false, bullets: ['Built backend systems'], skills: ['.NET', 'SQL'], }, ], education: [], skills: ['.NET', 'SQL'], languages: [{ name: 'English', level: 'Native' }], interests: ['Cooking'], otherSections: [], sections: [ { name: 'Professional Summary', content: 'Built backend systems', wordCount: 3 }, { name: 'Skills', content: '.NET\nSQL', wordCount: 2 }, ], }; function renderPage() { return render( , ); } beforeEach(() => { mockedApi.get.mockImplementation((url: string) => { if (url === '/auth/me') { return Promise.resolve({ data: { provider: 'local', email: 'demo@example.com', userName: 'demo', firstName: 'Demo', lastName: 'User', displayName: 'Demo User', profileCvText: 'Professional Summary\nBuilt backend systems', profileCvStructureJson: JSON.stringify(structuredCv), googleLink: { linked: false }, }, } as any); } if (url === '/profile-cv/runs') { return Promise.resolve({ data: [ { id: 12, trigger: 'upload', status: 'applied', artifactFileName: 'resume.pdf', startedAtUtc: '2026-03-28T12:00:00Z', completedAtUtc: '2026-03-28T12:00:05Z', appliedAtUtc: '2026-03-28T12:00:05Z', parserVersion: 'm005-s01', normalizerVersion: 'm005-s01', llmPromptVersion: 'm005-s01', }, ], } as any); } if (url === '/jobapplications') { return Promise.resolve({ data: { items: [ { id: 42, jobTitle: 'Senior Backend Engineer', company: { id: 7, name: 'Acme Systems' }, status: 'Waiting', dateApplied: '2026-03-20', daysSince: 10, description: 'Build API integrations and platform workflows.', responseReceived: false, }, ], total: 1, page: 1, pageSize: 100, }, } as any); } return Promise.resolve({ data: {} } as any); }); mockedApi.post.mockImplementation((url: string) => { if (url === '/profile-cv/parse') { return Promise.resolve({ data: { structuredCv: { ...structuredCv, sections: [ { name: 'Professional Summary', content: 'Built backend systems', wordCount: 3 }, { name: 'Core Skills', content: '.NET\nSQL\nAzure', wordCount: 3 }, ], skills: ['.NET', 'SQL', 'Azure'], }, }, } as any); } if (url === '/profile-cv/rewrite-preview') { return Promise.resolve({ data: { templateId: 'harvard', html: 'Preview', suggestedFileName: 'harvard-preview.pdf', fullText: 'Professional Summary\nClearer, sharper positioning for backend platform roles.', rewrittenText: 'Professional Summary\nClearer, sharper positioning for backend platform roles.', structuredCv, sectionName: null, jobApplicationId: 42, targetRole: 'Senior Backend Engineer' } } as any); } if (url === '/profile-cv/reprocess') { return Promise.resolve({ data: { reprocessed: true } } as any); } return Promise.resolve({ data: {} } as any); }); mockedApi.put.mockResolvedValue({ data: {} } as any); window.localStorage.clear(); }); afterEach(() => { jest.clearAllMocks(); }); test('profile page loads persisted structured cv and can re-parse it', async () => { renderPage(); expect(await screen.findByText(/cv ready/i)).toBeInTheDocument(); expect(screen.getByText(/cv structure overview/i)).toBeInTheDocument(); expect(screen.getByText(/structured cv editor/i)).toBeInTheDocument(); expect(screen.getByText(/extraction history/i)).toBeInTheDocument(); expect(screen.getByText(/resume.pdf/i)).toBeInTheDocument(); expect(screen.getByText(/current run/i)).toBeInTheDocument(); expect(screen.getAllByText(/original extraction/i).length).toBeGreaterThan(0); const originalExtractionToggle = screen.getByRole('button', { name: /original extraction/i }); expect(originalExtractionToggle).toHaveAttribute('aria-expanded', 'false'); expect(screen.getAllByText(/professional summary/i).length).toBeGreaterThan(0); expect(screen.getByLabelText(/full name/i)).toHaveValue('Demo User'); expect(screen.getByText(/high 92%/i)).toBeInTheDocument(); expect(screen.getByText(/block-1/i)).toBeInTheDocument(); fireEvent.click(originalExtractionToggle); expect(originalExtractionToggle).toHaveAttribute('aria-expanded', 'true'); expect(await screen.findByLabelText(/profile cv \/ master resume text/i)).toHaveValue('Professional Summary\nBuilt backend systems'); const analyzeButton = screen.getByRole('button', { name: /analyze sections/i }); await waitFor(() => expect(analyzeButton).toBeEnabled()); fireEvent.click(analyzeButton); await waitFor(() => { expect(mockedApi.post).toHaveBeenCalledWith('/profile-cv/parse', { text: 'Professional Summary\nBuilt backend systems' }); }); expect(screen.getAllByText(/core skills/i).length).toBeGreaterThan(0); }); test('profile page can reprocess from stored artifact history', async () => { renderPage(); expect(await screen.findByText(/extraction history/i)).toBeInTheDocument(); const reprocessButton = screen.getByRole('button', { name: /reprocess cv/i }); fireEvent.click(reprocessButton); await waitFor(() => { expect(mockedApi.post).toHaveBeenCalledWith('/profile-cv/reprocess'); }); }); test('profile page keeps raw extraction collapsed until expanded', async () => { renderPage(); expect(await screen.findByText(/cv ready/i)).toBeInTheDocument(); expect(screen.getByText(/the structured cv stays front and center/i)).toBeInTheDocument(); const originalExtractionToggle = screen.getByRole('button', { name: /original extraction/i }); expect(originalExtractionToggle).toHaveAttribute('aria-expanded', 'false'); const copyButton = screen.getByRole('button', { name: /copy cv text/i }); expect(copyButton).toBeDisabled(); fireEvent.click(originalExtractionToggle); expect(originalExtractionToggle).toHaveAttribute('aria-expanded', 'true'); expect(await screen.findByLabelText(/profile cv \/ master resume text/i)).toHaveValue('Professional Summary\nBuilt backend systems'); const copyButtons = screen.getAllByRole('button', { name: /copy cv text/i }); expect(copyButtons.some((button) => !button.hasAttribute('disabled'))).toBe(true); }); test('profile page rewrite tools use selected template and saved job context', async () => { renderPage(); expect(await screen.findByText(/template-driven cv builder/i)).toBeInTheDocument(); fireEvent.click(screen.getByText(/harvard/i)); fireEvent.mouseDown(screen.getAllByRole('combobox')[1]); fireEvent.click(await screen.findByText(/senior backend engineer ยท acme systems/i)); const rewriteButton = screen.getByRole('button', { name: /build preview/i }); fireEvent.click(rewriteButton); await waitFor(() => { expect(mockedApi.post).toHaveBeenCalledWith('/profile-cv/rewrite-preview', expect.objectContaining({ sectionName: null, style: 'harvard', templateId: 'harvard', jobApplicationId: 42, })); }); expect(await screen.findByText(/preview ready/i)).toBeInTheDocument(); expect(screen.getByText(/clearer, sharper positioning for backend platform roles/i)).toBeInTheDocument(); }); test('saving profile persists structured cv json', async () => { renderPage(); expect(await screen.findByText(/cv ready/i)).toBeInTheDocument(); const fullNameInput = screen.getByLabelText(/full name/i); fireEvent.change(fullNameInput, { target: { value: 'Updated Demo User' } }); const saveButton = screen.getByRole('button', { name: /save changes/i }); await waitFor(() => expect(saveButton).toBeEnabled()); fireEvent.click(saveButton); await waitFor(() => { expect(mockedApi.put).toHaveBeenCalled(); }); const payload = mockedApi.put.mock.calls[0][1] as any; const parsed = JSON.parse(payload.profileCvStructureJson); expect(parsed.contact.fullName).toBe('Updated Demo User'); expect(parsed.skills).toEqual(['.NET', 'SQL']); expect(parsed.jobs[0].title).toBe('System Developer'); });