Add structured CV editor to profile page

This commit is contained in:
2026-03-28 15:08:43 +01:00
parent 8f8a34ad9c
commit 5f14490ead
4 changed files with 474 additions and 57 deletions
+54 -14
View File
@@ -22,6 +22,37 @@ jest.mock('./components/CropImageDialog', () => () => null);
const mockedApi = api as jest.Mocked<typeof api>;
const structuredCv = {
version: '1',
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(
<ToastProvider>
@@ -44,9 +75,7 @@ beforeEach(() => {
lastName: 'User',
displayName: 'Demo User',
profileCvText: 'Professional Summary\nBuilt backend systems',
profileCvStructureJson: JSON.stringify([
{ name: 'Professional Summary', content: 'Built backend systems', wordCount: 3 },
]),
profileCvStructureJson: JSON.stringify(structuredCv),
googleLink: { linked: false },
},
} as any);
@@ -57,10 +86,14 @@ beforeEach(() => {
if (url === '/profile-cv/parse') {
return Promise.resolve({
data: {
sections: [
{ name: 'Professional Summary', content: 'Built backend systems', wordCount: 3 },
{ name: 'Core Skills', content: '.NET\nSQL\nAzure', wordCount: 3 },
],
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);
}
@@ -74,12 +107,14 @@ afterEach(() => {
jest.clearAllMocks();
});
test('profile page loads persisted cv sections and can re-parse them', async () => {
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.getAllByText(/professional summary/i).length).toBeGreaterThan(0);
expect(screen.getByLabelText(/full name/i)).toHaveValue('Demo User');
const analyzeButton = screen.getByRole('button', { name: /analyze sections/i });
await waitFor(() => expect(analyzeButton).toBeEnabled());
@@ -89,22 +124,27 @@ test('profile page loads persisted cv sections and can re-parse them', async ()
expect(mockedApi.post).toHaveBeenCalledWith('/profile-cv/parse', { text: 'Professional Summary\nBuilt backend systems' });
});
expect(await screen.findByText(/core skills/i)).toBeInTheDocument();
expect(screen.getAllByText(/core skills/i).length).toBeGreaterThan(0);
});
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).toHaveBeenCalledWith('/auth/profile', expect.objectContaining({
profileCvStructureJson: JSON.stringify([
{ name: 'Professional Summary', content: 'Built backend systems', wordCount: 3 },
]),
}));
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');
});