import React from 'react'; import '@testing-library/jest-dom'; import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react'; import { createMemoryRouter, RouterProvider } from 'react-router-dom'; import CvBuilderEditor from './views/CvBuilderEditor'; import { I18nProvider } from './i18n/I18nProvider'; import { ToastProvider } from './toast'; import { api } from './api'; import { emptyCvVariantSettings } from './cvBuilder'; import { ConfirmProvider } from './confirm'; import { PromptProvider } from './prompt'; // The Application Workspace CV section deep-links straight into the builder at // /career/builder/:id. These tests cover that entry point: the variant loads, and a variant the // server refuses (missing, or someone else's) lands on a safe error state rather than a blank editor. jest.mock('./api', () => ({ api: { get: jest.fn(), post: jest.fn(), put: jest.fn(), delete: jest.fn(), interceptors: { request: { use: jest.fn() }, response: { use: jest.fn() } }, }, getApiErrorMessage: (_e: any, fallback?: string) => fallback || 'Request failed.', })); const mockedApi = api as jest.Mocked; const variant = { id: 3, name: 'Backend CV', settings: emptyCvVariantSettings(), isPublic: false, publicSlug: 'abc123', version: 4, jobApplicationId: 7, updatedAtUtc: '2026-07-19T10:00:00Z', }; function renderAt(id: number) { const router = createMemoryRouter([ { path: '/career/builder/:id', element: }, { path: '/career/builder', element:
CV list destination
}, ], { initialEntries: [`/career/builder/${id}`] }); return render( , ); } beforeEach(() => { window.localStorage.removeItem('uiLanguage'); jest.clearAllMocks(); mockedApi.put.mockReset(); mockedApi.put.mockResolvedValue({ data: { ...variant, version: variant.version + 1 } } as any); mockedApi.post.mockResolvedValue({ data: { themeId: 'nordic', html: '

cv

', suggestedFileName: 'cv.pdf' } } as any); }); afterEach(() => window.localStorage.removeItem('uiLanguage')); function routeGet(onVariant: () => Promise, outline: any = { sections: [] }, history: any[] = []) { mockedApi.get.mockImplementation((url: string) => { if (url === '/cv/variants/3') return onVariant(); if (url === '/cv/themes') return Promise.resolve({ data: [] } as any); if (url === '/cv/outline') return Promise.resolve({ data: outline } as any); if (url === '/cv/variants/3/versions') return Promise.resolve({ data: history } as any); return Promise.resolve({ data: [] } as any); }); } async function openContentTab() { await screen.findByDisplayValue('Backend CV'); fireEvent.click(screen.getByRole('tab', { name: 'Content' })); } test('content sections follow the editing-region heading without mutating CV settings', async () => { routeGet(() => Promise.resolve({ data: variant })); renderAt(3); await openContentTab(); expect(screen.getByRole('heading', { name: 'CV editing controls', level: 2 })).toBeInTheDocument(); expect(screen.getByRole('heading', { name: 'Sections', level: 3 })).toBeInTheDocument(); expect(mockedApi.put).not.toHaveBeenCalled(); }); test('section navigation opens and focuses omitted/custom sections without changing their data', async () => { const settings = { ...variant.settings, sections: [{ key: 'skills', hidden: true }, { key: 'custom:sample', hidden: false }], customSections: [{ key: 'sample', title: 'Volunteer work', items: ['Bokmål: frivillig arbeid'], hidden: false }], }; routeGet(() => Promise.resolve({ data: { ...variant, settings } })); renderAt(3); await openContentTab(); const navigation = screen.getByRole('navigation', { name: 'CV section navigation' }); fireEvent.mouseDown(within(navigation).getByRole('combobox', { name: 'Go to section' })); fireEvent.click(await screen.findByRole('option', { name: 'Skills · Omitted' })); await waitFor(() => expect(screen.getByRole('button', { name: 'Collapse Skills' })).toHaveFocus()); expect(screen.getByRole('button', { name: 'Show Skills section' })).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: 'Collapse Skills' })); fireEvent.mouseDown(within(navigation).getByRole('combobox', { name: /Go to section/ })); fireEvent.click(await screen.findByRole('option', { name: 'Skills · Omitted' })); await waitFor(() => expect(screen.getByRole('button', { name: 'Collapse Skills' })).toHaveFocus()); fireEvent.mouseDown(within(navigation).getByRole('combobox', { name: 'Go to section' })); fireEvent.click(await screen.findByRole('option', { name: 'Volunteer work · Included' })); await waitFor(() => expect(screen.getByRole('button', { name: 'Collapse Volunteer work' })).toHaveFocus()); expect(screen.getByDisplayValue('Bokmål: frivillig arbeid')).toBeInTheDocument(); fireEvent.click(screen.getByRole('tab', { name: 'Customize' })); fireEvent.click(screen.getByRole('tab', { name: 'Content' })); expect(within(navigation).getByRole('combobox')).toHaveTextContent('Volunteer work'); expect(screen.getByRole('button', { name: 'Collapse Skills' })).toHaveAttribute('aria-expanded', 'true'); expect(screen.getByRole('button', { name: 'Collapse Volunteer work' })).toHaveAttribute('aria-expanded', 'true'); expect(mockedApi.put).not.toHaveBeenCalled(); }); test('keyboard move controls retain focus and persist the same section order shown by navigation', async () => { routeGet(() => Promise.resolve({ data: { ...variant, settings: { ...variant.settings, sections: [{ key: 'summary' }, { key: 'experience' }, { key: 'skills', hidden: true }], } } })); renderAt(3); await openContentTab(); const move = screen.getByRole('button', { name: 'Move Experience section down' }); move.focus(); fireEvent.click(move); expect(move).toHaveFocus(); fireEvent.mouseDown(screen.getByRole('combobox', { name: 'Go to section' })); expect(screen.getAllByRole('option').slice(0, 3).map(option => option.textContent)).toEqual([ 'Professional Summary · Included', 'Skills · Omitted', 'Experience · Included', ]); fireEvent.keyDown(screen.getByRole('listbox'), { key: 'Escape' }); fireEvent.click(screen.getByRole('button', { name: 'Save now' })); await screen.findByText('Saved'); expect(mockedApi.put).toHaveBeenLastCalledWith('/cv/variants/3', expect.objectContaining({ expectedVersion: 4, settings: expect.objectContaining({ sections: expect.arrayContaining([ { key: 'summary' }, { key: 'skills', hidden: true }, { key: 'experience' }, ]) }), })); expect((mockedApi.put.mock.calls[mockedApi.put.mock.calls.length - 1][1] as any).settings.sections.slice(0, 3).map((row: any) => row.key)).toEqual(['summary', 'skills', 'experience']); }); test('secondary duplicate action saves the current draft before creating the copy', async () => { routeGet(() => Promise.resolve({ data: variant })); mockedApi.post.mockImplementation((url: string) => Promise.resolve({ data: url.endsWith('/duplicate') ? { ...variant, id: 8, name: 'Backend CV copy' } : { html: '

CV

' } } as any)); renderAt(3); await openContentTab(); fireEvent.change(screen.getByLabelText('Headline override'), { target: { value: 'Draft before copy' } }); fireEvent.click(screen.getByRole('button', { name: 'More document actions' })); fireEvent.click(screen.getByRole('menuitem', { name: 'Duplicate' })); await waitFor(() => expect(mockedApi.post).toHaveBeenCalledWith('/cv/variants/3/duplicate', expect.anything())); expect(mockedApi.put).toHaveBeenCalledWith('/cv/variants/3', expect.objectContaining({ expectedVersion: 4, settings: expect.objectContaining({ headline: 'Draft before copy' }), })); expect(mockedApi.put.mock.invocationCallOrder[0]).toBeLessThan(mockedApi.post.mock.invocationCallOrder[ mockedApi.post.mock.calls.findIndex(call => call[0] === '/cv/variants/3/duplicate') ]); }); test('pane navigation focuses the existing preview without changing the pending draft', async () => { routeGet(() => Promise.resolve({ data: variant })); renderAt(3); await openContentTab(); const headline = screen.getByLabelText('Headline override'); fireEvent.change(headline, { target: { value: 'Retained draft' } }); const preview = screen.getByRole('region', { name: 'Live preview' }); preview.scrollIntoView = jest.fn(); fireEvent.click(screen.getByRole('button', { name: 'Go to preview' })); expect(preview).toHaveFocus(); expect(headline).toHaveValue('Retained draft'); expect(screen.getByText('Unsaved')).toBeInTheDocument(); expect(mockedApi.put).not.toHaveBeenCalled(); }); test('deep link loads the exact variant named in the route', async () => { routeGet(() => Promise.resolve({ data: variant } as any)); renderAt(3); expect(await screen.findByDisplayValue('Backend CV')).toBeInTheDocument(); expect(mockedApi.get).toHaveBeenCalledWith('/cv/variants/3'); }); test('a variant that does not exist shows a safe error instead of an empty editor', async () => { routeGet(() => Promise.reject(new Error('404'))); renderAt(3); expect(await screen.findByText(/Could not open this CV/i)).toBeInTheDocument(); expect(screen.queryByDisplayValue('Backend CV')).not.toBeInTheDocument(); }); test("another user's variant is refused by the server and never rendered", async () => { // Ownership is enforced server-side: CvVariantService scopes every read to the owner and the // controller returns 404, so the client only ever sees the same safe failure. routeGet(() => Promise.reject({ response: { status: 404 } })); renderAt(3); expect(await screen.findByText(/Could not open this CV/i)).toBeInTheDocument(); }); test('invalid names are explained and are not silently autosaved', async () => { routeGet(() => Promise.resolve({ data: variant } as any)); renderAt(3); fireEvent.change(await screen.findByLabelText('CV name'), { target: { value: '' } }); expect(screen.getByText('Enter a name before saving.')).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Save now' })).toBeDisabled(); expect(mockedApi.put).not.toHaveBeenCalled(); }); test('failed autosave is visible and can be retried with the latest data', async () => { routeGet(() => Promise.resolve({ data: variant } as any)); mockedApi.put.mockRejectedValueOnce(new Error('offline')).mockResolvedValueOnce({ data: variant } as any); renderAt(3); await openContentTab(); fireEvent.change(await screen.findByLabelText('Headline override'), { target: { value: 'Platform Engineer' } }); expect(screen.getByText('Unsaved')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: 'Save now' })); expect(await screen.findByText('Save failed')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: 'Save now' })); expect(await screen.findByText('Saved')).toBeInTheDocument(); expect(mockedApi.put).toHaveBeenLastCalledWith('/cv/variants/3', expect.objectContaining({ name: 'Backend CV', settings: expect.objectContaining({ headline: 'Platform Engineer' }), })); }); test('session undo and redo restore content edits before autosave', async () => { routeGet(() => Promise.resolve({ data: variant } as any)); mockedApi.put.mockResolvedValue({ data: variant } as any); renderAt(3); await openContentTab(); const headline = await screen.findByLabelText('Headline override'); fireEvent.change(headline, { target: { value: 'Platform Engineer' } }); expect(screen.getByRole('button', { name: 'Undo' })).toBeEnabled(); fireEvent.click(screen.getByRole('button', { name: 'Undo' })); expect(headline).toHaveValue(''); expect(screen.getByRole('button', { name: 'Redo' })).toBeEnabled(); fireEvent.click(screen.getByRole('button', { name: 'Redo' })); expect(headline).toHaveValue('Platform Engineer'); }); test('queued saves use acknowledged server versions without replacing newer local editing state', async () => { routeGet(() => Promise.resolve({ data: variant } as any)); let release!: (value: any) => void; mockedApi.put.mockImplementationOnce(() => new Promise(resolve => { release = resolve; })); mockedApi.put.mockResolvedValueOnce({ data: { ...variant, version: 6 } } as any); renderAt(3); await openContentTab(); const headline = screen.getByLabelText('Headline override'); fireEvent.change(headline, { target: { value: 'First edit' } }); fireEvent.click(screen.getByRole('button', { name: 'Save now' })); await waitFor(() => expect(mockedApi.put).toHaveBeenCalledTimes(1)); expect(mockedApi.put.mock.calls[0][1]).toEqual(expect.objectContaining({ expectedVersion: 4 })); fireEvent.change(headline, { target: { value: 'Second edit' } }); fireEvent.click(screen.getByRole('button', { name: 'Save now' })); expect(mockedApi.put).toHaveBeenCalledTimes(1); release({ data: { ...variant, version: 5 } }); await screen.findByText('Saved'); expect(mockedApi.put.mock.calls[1][1]).toEqual(expect.objectContaining({ expectedVersion: 5, settings: expect.objectContaining({ headline: 'Second edit' }) })); expect(headline).toHaveValue('Second edit'); }); test('revision conflict retains the draft and blocks later autosaves until explicit reload', async () => { let saved = variant; routeGet(() => Promise.resolve({ data: saved } as any)); mockedApi.put.mockRejectedValueOnce({ response: { status: 409, data: { code: 'cv_revision_conflict' } } }); renderAt(3); await openContentTab(); const headline = screen.getByLabelText('Headline override'); fireEvent.change(headline, { target: { value: 'Recoverable draft' } }); fireEvent.click(screen.getByRole('button', { name: 'Save now' })); expect(await screen.findByText('CV changed elsewhere')).toBeInTheDocument(); expect(headline).toHaveValue('Recoverable draft'); fireEvent.change(headline, { target: { value: 'Still recoverable' } }); expect(screen.queryByRole('button', { name: 'Save now' })).not.toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Download unsaved copy' })).toBeEnabled(); fireEvent.click(screen.getByRole('button', { name: 'Reload saved CV' })); fireEvent.click(within(await screen.findByRole('dialog')).getByRole('button', { name: 'Cancel' })); await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument()); expect(headline).toHaveValue('Still recoverable'); saved = { ...variant, version: 6, settings: { ...variant.settings, headline: 'Reviewed remote state' } }; fireEvent.click(screen.getByRole('button', { name: 'Reload saved CV' })); fireEvent.click(within(await screen.findByRole('dialog')).getByRole('button', { name: 'Reload saved CV' })); await waitFor(() => expect(headline).toHaveValue('Reviewed remote state')); await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument()); expect(mockedApi.put).toHaveBeenCalledTimes(1); mockedApi.put.mockResolvedValue({ data: { ...saved, version: 7 } } as any); fireEvent.change(headline, { target: { value: 'After review' } }); fireEvent.click(screen.getByRole('button', { name: 'Save now' })); await screen.findByText('Saved'); expect(mockedApi.put).toHaveBeenLastCalledWith('/cv/variants/3', expect.objectContaining({ expectedVersion: 6 })); }); test('edits made while a confirmed reload is in flight remain recoverable', async () => { let release!: (value: any) => void; routeGet(() => Promise.resolve({ data: variant })); mockedApi.put.mockRejectedValue({ response: { status: 409, data: { code: 'cv_revision_conflict' } } }); renderAt(3); await openContentTab(); const headline = screen.getByLabelText('Headline override'); fireEvent.change(headline, { target: { value: 'Conflicting draft' } }); fireEvent.click(screen.getByRole('button', { name: 'Save now' })); await screen.findByText('CV changed elsewhere'); routeGet(() => new Promise(resolve => { release = resolve; })); fireEvent.click(screen.getByRole('button', { name: 'Reload saved CV' })); fireEvent.click(within(await screen.findByRole('dialog')).getByRole('button', { name: 'Reload saved CV' })); await waitFor(() => expect(release).toBeDefined()); fireEvent.change(headline, { target: { value: 'New edit during reload' } }); release({ data: { ...variant, version: 6, settings: { ...variant.settings, headline: 'Remote state' } } }); await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument()); expect(headline).toHaveValue('New edit during reload'); expect(screen.getByText('CV changed elsewhere')).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Download unsaved copy' })).toBeEnabled(); }); test('restoring history uses the current version and never replaces edits made during the request', async () => { routeGet(() => Promise.resolve({ data: variant }), { sections: [] }, [ { version: 4, isCurrent: true, source: 'manual', createdAtUtc: variant.updatedAtUtc }, { version: 3, isCurrent: false, source: 'manual', createdAtUtc: variant.updatedAtUtc }, ]); let release!: (value: any) => void; mockedApi.post.mockImplementation((url: string) => url.endsWith('/restore') ? new Promise(resolve => { release = resolve; }) : Promise.resolve({ data: { html: '

CV

' } } as any)); renderAt(3); await screen.findByDisplayValue('Backend CV'); expect(screen.getByRole('region', { name: 'CV editing controls' })).toBeInTheDocument(); expect(screen.getByRole('tablist', { name: 'CV studio sections' })).toBeInTheDocument(); fireEvent.click(screen.getByRole('tab', { name: 'History' })); fireEvent.click(await screen.findByRole('button', { name: 'Restore version 3' })); fireEvent.click(within(await screen.findByRole('dialog')).getByRole('button', { name: 'Restore version' })); await waitFor(() => expect(mockedApi.post).toHaveBeenCalledWith('/cv/variants/3/versions/3/restore', { expectedVersion: 4 })); await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument()); fireEvent.click(screen.getByRole('tab', { name: 'Content' })); const headline = screen.getByLabelText('Headline override'); fireEvent.change(headline, { target: { value: 'New edit during restore' } }); release({ data: { ...variant, version: 5, settings: { ...variant.settings, headline: 'Historical content' } } }); await screen.findByText('CV changed elsewhere'); expect(headline).toHaveValue('New edit during restore'); expect(mockedApi.put).not.toHaveBeenCalled(); }); test('Content is the primary studio panel and all advanced tabs retain their controls', async () => { routeGet(() => Promise.resolve({ data: variant } as any)); renderAt(3); await screen.findByDisplayValue('Backend CV'); const content = screen.getByRole('tab', { name: 'Content' }); expect(content).toHaveAttribute('aria-selected', 'true'); expect(screen.getByRole('tablist', { name: 'CV studio sections' }).firstElementChild).toBe(content); expect(screen.getByRole('tabpanel', { name: 'Content' })).toBeInTheDocument(); expect(screen.getByText('This CV only')).toBeInTheDocument(); expect(screen.getByRole('link', { name: 'Linked application' })).toHaveAttribute('href', '/jobs/7'); fireEvent.click(screen.getByRole('tab', { name: 'Customize' })); expect(screen.getByRole('region', { name: 'Style' })).toBeInTheDocument(); expect(screen.getByLabelText('Body size')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: 'Advanced document settings' })); expect(screen.getByLabelText('Skills presentation')).toBeInTheDocument(); expect(screen.getByLabelText('Page size')).toBeInTheDocument(); expect(screen.getByLabelText('Columns')).toBeInTheDocument(); expect(screen.getByLabelText('Language')).toBeInTheDocument(); fireEvent.click(screen.getByRole('tab', { name: 'AI Tools' })); expect(screen.getByText('Translate resume')).toBeInTheDocument(); expect(screen.getByText('Improve writing')).toBeInTheDocument(); expect(screen.getByText('Check spelling and grammar')).toBeInTheDocument(); }); test('choosing a template restores its own structure instead of inheriting the previous template layout', async () => { const customizedVariant = { ...variant, settings: { ...emptyCvVariantSettings(), themeId: 'minimal', layout: 'single', headerStyle: 'plain', headingStyle: 'plain', sidebarWidthMm: 72, sidebarSections: ['skills'], accentColor: '#123456', pageMarginMm: 9, }, }; mockedApi.get.mockImplementation((url: string) => { if (url === '/cv/variants/3') return Promise.resolve({ data: customizedVariant } as any); if (url === '/cv/themes') return Promise.resolve({ data: [ { id: 'minimal', name: 'Minimal', category: 'ATS', description: 'Quiet single column', layout: 'single', available: true, swatches: ['#111827', '#fff', '#fff'] }, { id: 'technical', name: 'Technical', category: 'Technical', description: 'Structured sidebar', layout: 'sidebar-left', available: true, swatches: ['#0f4c5c', '#0f4c5c', '#fff'] }, ] } as any); if (url === '/cv/outline') return Promise.resolve({ data: { sections: [] } } as any); if (url.startsWith('/cv/themes/')) return Promise.resolve({ data: { themeId: url.includes('technical') ? 'technical' : 'minimal', html: '

preview

', suggestedFileName: 'preview.pdf' } } as any); return Promise.resolve({ data: [] } as any); }); mockedApi.put.mockResolvedValue({ data: customizedVariant } as any); renderAt(3); await screen.findByDisplayValue('Backend CV'); fireEvent.click(screen.getByRole('tab', { name: 'Template' })); fireEvent.click(screen.getByRole('button', { name: 'All templates' })); fireEvent.click(screen.getByRole('button', { name: /Technical/ })); fireEvent.click(within(await screen.findByRole('dialog', { name: 'Apply template defaults?' })).getByRole('button', { name: 'Apply template' })); await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument()); fireEvent.click(screen.getByRole('button', { name: 'Save now' })); await waitFor(() => expect(mockedApi.put).toHaveBeenCalledWith('/cv/variants/3', expect.objectContaining({ settings: expect.objectContaining({ themeId: 'technical', layout: null, headerStyle: null, headingStyle: null, sidebarWidthMm: null, sidebarSections: null, accentColor: null, pageMarginMm: null, }), }))); }); test('editor sections, design controls and document settings render in Bokmål', async () => { window.localStorage.setItem('uiLanguage', 'nb'); routeGet(() => Promise.resolve({ data: variant } as any)); renderAt(3); await screen.findByDisplayValue('Backend CV'); fireEvent.click(screen.getByRole('tab', { name: 'Innhold' })); expect(await screen.findByLabelText('Egen overskrift')).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Utvid Erfaring' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Legg til innhold' })).toBeInTheDocument(); fireEvent.click(screen.getByRole('tab', { name: 'Tilpass' })); expect(screen.getByRole('region', { name: 'Stil' })).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: 'Avanserte dokumentinnstillinger' })); expect(screen.getByLabelText('Visning av ferdigheter')).toBeInTheDocument(); expect(screen.getByLabelText('Sidestørrelse')).toBeInTheDocument(); expect(screen.getByLabelText('Dokumentspråk')).toBeInTheDocument(); }); test('internal navigation warns and can be cancelled before discarding a pending edit', async () => { routeGet(() => Promise.resolve({ data: variant } as any)); renderAt(3); await openContentTab(); fireEvent.change(await screen.findByLabelText('Headline override'), { target: { value: 'Unsaved headline' } }); fireEvent.click(screen.getByRole('button', { name: 'Back to CVs' })); const dialog = await screen.findByRole('dialog', { name: 'Discard unsaved CV changes?' }); expect(within(dialog).getByText('This CV has unsaved changes. Leave and discard them?')).toBeInTheDocument(); fireEvent.click(within(dialog).getByRole('button', { name: 'Cancel' })); await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument(), { timeout: 5000 }); expect(screen.getByDisplayValue('Backend CV')).toBeInTheDocument(); }); test('custom entries can be added, edited, reordered and deleted with confirmation', async () => { routeGet(() => Promise.resolve({ data: variant } as any)); mockedApi.put.mockResolvedValue({ data: variant } as any); renderAt(3); await openContentTab(); await screen.findByLabelText('Headline override'); fireEvent.click(screen.getByRole('button', { name: 'Add content' })); const addContentDialog = await screen.findByRole('dialog', { name: 'Add content' }); const additionalExperienceCard = within(addContentDialog).getByText('Additional Experience').closest('.MuiPaper-root'); expect(additionalExperienceCard).not.toBeNull(); fireEvent.click(within(additionalExperienceCard as HTMLElement).getByRole('button', { name: 'Add' })); await waitFor(() => expect(screen.queryByRole('dialog', { name: 'Add content' })).not.toBeInTheDocument()); const title = screen.getByLabelText(/Section name for custom:/); fireEvent.change(title, { target: { value: 'Selected projects' } }); fireEvent.click(screen.getByRole('button', { name: 'Expand Selected projects' })); expect(within(screen.getByRole('group', { name: 'Selected projects' })).getByText('CV-only content')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: 'Add entry' })); fireEvent.change(screen.getByLabelText('Entry 1'), { target: { value: 'First project' } }); fireEvent.click(screen.getByRole('button', { name: 'Add entry' })); fireEvent.change(screen.getByLabelText('Entry 2'), { target: { value: 'Second project' } }); fireEvent.click(screen.getByRole('button', { name: 'Move custom entry 2 up' })); expect(screen.getByLabelText('Entry 1')).toHaveValue('Second project'); expect(screen.getByLabelText('Entry 2')).toHaveValue('First project'); const deleteEntry = screen.getByRole('button', { name: 'Delete custom entry 1' }); deleteEntry.focus(); fireEvent.click(deleteEntry); let dialog = await screen.findByRole('dialog', { name: 'Delete custom entry' }); fireEvent.click(within(dialog).getByRole('button', { name: 'Cancel' })); await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument()); expect(deleteEntry).toHaveFocus(); expect(screen.getAllByLabelText(/^Entry /)).toHaveLength(2); fireEvent.click(screen.getByRole('button', { name: 'Delete custom entry 1' })); dialog = await screen.findByRole('dialog', { name: 'Delete custom entry' }); fireEvent.click(within(dialog).getByRole('button', { name: 'Delete entry' })); await waitFor(() => expect(screen.getAllByLabelText(/^Entry /)).toHaveLength(1)); await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument()); expect(screen.getByLabelText('Entry 1')).toHaveFocus(); await waitFor(() => expect(mockedApi.put).toHaveBeenLastCalledWith('/cv/variants/3', expect.objectContaining({ settings: expect.objectContaining({ customSections: [expect.objectContaining({ title: 'Selected projects', items: ['First project'] })], }), })), { timeout: 5000 }); expect(await screen.findByText('Saved')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: 'Remove Selected projects section from this CV' })); dialog = await screen.findByRole('dialog', { name: 'Delete custom section' }); fireEvent.click(within(dialog).getByRole('button', { name: 'Cancel' })); await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument()); expect(screen.getByLabelText(/Section name for custom:/)).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: 'Remove Selected projects section from this CV' })); dialog = await screen.findByRole('dialog', { name: 'Delete custom section' }); fireEvent.click(within(dialog).getByRole('button', { name: 'Delete section' })); await waitFor(() => expect(screen.queryByLabelText(/Section name for custom:/)).not.toBeInTheDocument()); }); const contentOutline = { sections: [{ key: 'experience', title: 'Experience', kind: 'entries', bullets: [], tags: [], entries: [ { key: 'job-1', title: 'Engineer', subtitle: 'First Co', meta: '2021-01 – Present', bullets: ['Built APIs'], tags: [] }, { key: 'job-2', title: 'Lead', subtitle: 'Second Co', bullets: ['Led teams'], tags: [] }, ], }], }; test('[P5-10 PRES-C01 C07] source-backed entry edits identify their scope and save only this CV', async () => { routeGet(() => Promise.resolve({ data: variant }), contentOutline); renderAt(3); await openContentTab(); fireEvent.click(screen.getByRole('button', { name: 'Expand Experience' })); const entry = screen.getByRole('group', { name: 'Engineer' }); expect(entry).not.toHaveTextContent('2021-01 – Present'); expect(within(entry).getAllByText('Using Career Profile')).toHaveLength(3); fireEvent.change(within(entry).getByLabelText('Title'), { target: { value: ' ' } }); expect(screen.getByRole('group', { name: 'Engineer' })).toBe(entry); expect(within(entry).getByText('Blank uses Career Profile')).toBeInTheDocument(); fireEvent.change(within(entry).getByLabelText('Title'), { target: { value: 'Platform Engineer' } }); fireEvent.change(within(entry).getByLabelText('Subtitle'), { target: { value: 'CV-specific employer label' } }); fireEvent.change(within(entry).getByLabelText('Bullet points (one per line)'), { target: { value: 'Reviewed CV bullet\nSecond CV bullet' } }); expect(within(entry).getAllByText('Customized for this CV')).toHaveLength(3); fireEvent.click(screen.getByRole('button', { name: 'Save now' })); await screen.findByText('Saved'); expect(mockedApi.put).toHaveBeenLastCalledWith('/cv/variants/3', expect.objectContaining({ expectedVersion: 4, settings: expect.objectContaining({ overrides: { 'job-1': { title: 'Platform Engineer', subtitle: 'CV-specific employer label', bullets: ['Reviewed CV bullet', 'Second CV bullet'] }, } }), })); expect(mockedApi.put.mock.calls.every(([url]) => url === '/cv/variants/3')).toBe(true); expect(contentOutline.sections[0].entries[0]).toEqual({ key: 'job-1', title: 'Engineer', subtitle: 'First Co', meta: '2021-01 – Present', bullets: ['Built APIs'], tags: [] }); }); test('[P5-10 PRES-C07 C10] resetting one entry field preserves omission, sibling override and order and supports undo', async () => { const settings = { ...variant.settings, sections: [{ key: 'experience', itemOrder: ['job-2', 'job-1'] }], overrides: { 'job-1': { title: 'CV Engineer', subtitle: 'CV employer', bullets: ['CV bullet'], hidden: true }, 'job-2': { title: 'CV Lead', bullets: ['Sibling item override'] }, }, }; routeGet(() => Promise.resolve({ data: { ...variant, settings } }), contentOutline); renderAt(3); await openContentTab(); fireEvent.click(screen.getByRole('button', { name: 'Expand Experience' })); const entry = screen.getByRole('group', { name: 'CV Engineer' }); fireEvent.click(within(entry).getByRole('button', { name: 'Use Career Profile: Title' })); expect(within(entry).getByLabelText('Title')).toHaveValue('Engineer'); expect(within(entry).getByLabelText('Subtitle')).toHaveValue('CV employer'); expect(within(entry).getByLabelText('Bullet points (one per line)')).toHaveValue('CV bullet'); fireEvent.click(within(entry).getByRole('button', { name: 'Use Career Profile: Subtitle' })); expect(within(entry).getByLabelText('Subtitle')).toHaveValue('First Co'); fireEvent.click(within(entry).getByRole('button', { name: 'Use Career Profile: Bullet points (one per line)' })); expect(within(entry).getByLabelText('Bullet points (one per line)')).toHaveValue('Built APIs'); expect(within(entry).getAllByText('Using Career Profile')).toHaveLength(3); expect(within(entry).getByRole('button', { name: 'Show Engineer entry' })).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: 'Undo' })); expect(within(entry).getByLabelText('Title')).toHaveValue('Engineer'); expect(within(entry).getByLabelText('Subtitle')).toHaveValue('First Co'); expect(within(entry).getByLabelText('Bullet points (one per line)')).toHaveValue('CV bullet'); fireEvent.click(screen.getByRole('button', { name: 'Redo' })); fireEvent.click(screen.getByRole('button', { name: 'Save now' })); await screen.findByText('Saved'); const saved = mockedApi.put.mock.calls[mockedApi.put.mock.calls.length - 1][1] as any; expect(saved.settings.overrides).toEqual({ 'job-1': { hidden: true }, 'job-2': settings.overrides['job-2'] }); expect(saved.settings.sections[0].itemOrder).toEqual(['job-2', 'job-1']); }); test('[P5-10 PRES-C07 C10 E08] omitted entries remain editable and retain their draft after a failed save', async () => { routeGet(() => Promise.resolve({ data: { ...variant, settings: { ...variant.settings, overrides: { 'job-1': { hidden: true } } } } }), contentOutline); mockedApi.put.mockRejectedValueOnce(new Error('offline')).mockResolvedValueOnce({ data: { ...variant, version: 5 } } as any); renderAt(3); await openContentTab(); fireEvent.click(screen.getByRole('button', { name: 'Expand Experience' })); const entry = screen.getByRole('group', { name: 'Engineer' }); expect(within(entry).getByText('Omitted', { exact: true })).toBeInTheDocument(); expect(within(entry).getByLabelText('Title')).toBeEnabled(); expect(within(entry).getByLabelText('Subtitle')).toBeEnabled(); const bullets = within(entry).getByLabelText('Bullet points (one per line)'); fireEvent.change(bullets, { target: { value: 'Retained omitted-entry draft' } }); fireEvent.click(screen.getByRole('button', { name: 'Save now' })); await screen.findByText('Save failed'); expect(bullets).toHaveValue('Retained omitted-entry draft'); expect(within(entry).getByRole('button', { name: 'Show Engineer entry' })).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: 'Save now' })); await screen.findByText('Saved'); expect(mockedApi.put).toHaveBeenLastCalledWith('/cv/variants/3', expect.objectContaining({ settings: expect.objectContaining({ overrides: { 'job-1': { hidden: true, bullets: ['Retained omitted-entry draft'] } } }), })); }); test('[P5-10 PRES-C01 C07] section content reset inherits source without changing omission or presentation', async () => { routeGet(() => Promise.resolve({ data: { ...variant, settings: { ...variant.settings, sections: [{ key: 'skills', items: ['CV-specific skill'], hidden: true, presentation: 'bubble' }], } } }), { sections: [{ key: 'skills', title: 'Skills', kind: 'tags', tags: ['C#', 'Norsk'], bullets: [], entries: [] }] }); renderAt(3); await openContentTab(); fireEvent.click(screen.getByRole('button', { name: 'Expand Skills' })); const section = screen.getByRole('group', { name: 'Skills' }); expect(within(section).getByLabelText('Items (one per line)')).toHaveValue('CV-specific skill'); fireEvent.click(within(section).getByRole('button', { name: 'Use Career Profile content' })); expect(within(section).getByLabelText('Items (one per line)')).toHaveValue('C#\nNorsk'); expect(within(section).getByRole('button', { name: 'Show Skills section' })).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: 'Save now' })); await screen.findByText('Saved'); const saved = mockedApi.put.mock.calls[mockedApi.put.mock.calls.length - 1][1] as any; const row = saved.settings.sections.find((item: any) => item.key === 'skills'); expect(row).toEqual(expect.objectContaining({ key: 'skills', hidden: true, presentation: 'bubble' })); expect(row.items).toBeUndefined(); expect(mockedApi.put.mock.calls.every(([url]) => url === '/cv/variants/3')).toBe(true); }); test('profile-backed sections expand, reorder, hide and persist variant-only overrides', async () => { routeGet(() => Promise.resolve({ data: variant } as any), { sections: [{ key: 'experience', title: 'Experience', kind: 'entries', bullets: [], tags: [], entries: [ { key: 'job-1', title: 'Engineer', subtitle: 'First Co', bullets: ['Built APIs'], tags: [] }, { key: 'job-2', title: 'Lead', subtitle: 'Second Co', bullets: ['Led teams'], tags: [] }, ], }], }); mockedApi.put.mockResolvedValue({ data: variant } as any); renderAt(3); await openContentTab(); fireEvent.click(await screen.findByRole('button', { name: 'Expand Experience' })); expect(screen.getByRole('button', { name: 'Collapse Experience' })).toHaveAttribute('aria-expanded', 'true'); fireEvent.mouseDown(screen.getByLabelText('Presentation')); fireEvent.click(await screen.findByRole('option', { name: 'Grid' })); fireEvent.click(within(screen.getByRole('group', { name: 'Columns' })).getByRole('button', { name: '2' })); fireEvent.click(screen.getByRole('button', { name: 'Move Engineer entry down' })); fireEvent.click(screen.getByRole('button', { name: 'Hide Lead entry' })); fireEvent.click(screen.getByRole('button', { name: 'Move Experience section down' })); await waitFor(() => expect(mockedApi.put).toHaveBeenLastCalledWith('/cv/variants/3', expect.objectContaining({ settings: expect.objectContaining({ sections: expect.arrayContaining([expect.objectContaining({ key: 'experience', itemOrder: ['job-2', 'job-1'], presentation: 'grid', columns: 2 })]), overrides: expect.objectContaining({ 'job-2': expect.objectContaining({ hidden: true }) }), }), })), { timeout: 5000 }); expect(await screen.findByText('Saved')).toBeInTheDocument(); }); test('preview failure is visible and retryable without leaving the editor', async () => { routeGet(() => Promise.resolve({ data: variant } as any)); mockedApi.post.mockRejectedValueOnce(new Error('preview offline')).mockResolvedValueOnce({ data: { themeId: 'modern', html: '

retry

', suggestedFileName: 'cv.pdf' } } as any); renderAt(3); expect(await screen.findByText('Preview unavailable')).toBeInTheDocument(); expect(screen.getByRole('region', { name: 'Live preview' })).toHaveAttribute('aria-busy', 'false'); expect(screen.queryByText('updating…')).not.toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: 'Retry preview' })); await waitFor(() => expect(screen.getByTitle('CV preview').getAttribute('srcdoc')).toContain('

retry

')); expect(screen.getByTitle('CV preview')).toHaveAttribute('sandbox', 'allow-same-origin allow-scripts'); await waitFor(() => expect(screen.queryByText('Preview unavailable')).not.toBeInTheDocument()); }); function settlePreview(count: number) { const frame = screen.getByTitle('CV preview') as HTMLIFrameElement; const doc = frame.contentDocument!; doc.body.innerHTML = '
Measured document
'; delete doc.documentElement.dataset.cvPreviewReady; fireEvent.load(frame); act(() => { doc.documentElement.dataset.cvPreviewPages = String(count); doc.documentElement.dataset.cvPreviewReady = 'true'; frame.contentWindow!.dispatchEvent(new Event('cv-preview-ready')); }); } test('[P5-11 PRES-C06 E08] automatic fit attaches when the asynchronously loaded preview pane appears', async () => { const width = jest.spyOn(HTMLElement.prototype, 'clientWidth', 'get').mockImplementation(function (this: HTMLElement) { return this.getAttribute('aria-label') === 'Live preview' ? 400 : 0; }); try { routeGet(() => Promise.resolve({ data: variant })); renderAt(3); await screen.findByDisplayValue('Backend CV'); expect(await screen.findByText('Fit width · 48%')).toBeInTheDocument(); } finally { width.mockRestore(); } }); test('[P5-11 PRES-C06 C12] page count is announced only after the renderer has settled', async () => { routeGet(() => Promise.resolve({ data: variant })); renderAt(3); const region = await screen.findByRole('region', { name: 'Live preview' }); await waitFor(() => expect(screen.getByTitle('CV preview').getAttribute('srcdoc')).toContain('

cv

')); expect(region).toHaveAttribute('aria-busy', 'true'); expect(screen.queryByText('1-page CV')).not.toBeInTheDocument(); settlePreview(3); expect(region).toHaveAttribute('aria-busy', 'false'); expect(screen.getByText('3-page CV')).toBeInTheDocument(); expect(region.querySelectorAll('[data-cv-page]')).toHaveLength(3); }); test('[P5-11 PRES-C06 E09] overlapping preview successes and failures cannot replace the newest document', async () => { const pending: { resolve: (value: any) => void; reject: (reason: Error) => void }[] = []; mockedApi.post.mockImplementation(() => new Promise((resolve, reject) => { pending.push({ resolve, reject }); })); try { routeGet(() => Promise.resolve({ data: variant })); renderAt(3); await openContentTab(); const headline = screen.getByLabelText('Headline override'); const region = screen.getByRole('region', { name: 'Live preview' }); await waitFor(() => expect(pending).toHaveLength(1)); fireEvent.change(headline, { target: { value: 'Newer draft' } }); await waitFor(() => expect(pending).toHaveLength(2)); await act(async () => { pending[1].resolve({ data: { html: '

Newer document

' } }); }); settlePreview(2); await act(async () => { pending[0].resolve({ data: { html: '

Stale document

' } }); }); expect(screen.getByTitle('CV preview').getAttribute('srcdoc')).toContain('Newer document'); expect(region).toHaveAttribute('aria-busy', 'false'); fireEvent.change(headline, { target: { value: 'Intermediate draft' } }); await waitFor(() => expect(pending).toHaveLength(3)); fireEvent.change(headline, { target: { value: 'Newest draft' } }); await waitFor(() => expect(pending).toHaveLength(4)); await act(async () => { pending[3].resolve({ data: { html: '

Newest document

' } }); }); settlePreview(4); await act(async () => { pending[2].reject(new Error('stale failure')); }); expect(screen.getByTitle('CV preview').getAttribute('srcdoc')).toContain('Newest document'); expect(screen.queryByText('Preview unavailable')).not.toBeInTheDocument(); expect(region).toHaveAttribute('aria-busy', 'false'); expect(screen.getByText('4-page CV')).toBeInTheDocument(); expect(headline).toHaveValue('Newest draft'); } finally { mockedApi.post.mockResolvedValue({ data: { html: '

cv

' } } as any); } }); test('[P5-11 PRES-C06 C10 E08] returning from preview focuses the existing editor without changing the pending draft', async () => { routeGet(() => Promise.resolve({ data: variant })); renderAt(3); await openContentTab(); const headline = screen.getByLabelText('Headline override'); jest.useFakeTimers(); try { fireEvent.change(headline, { target: { value: 'Retained preview draft' } }); const editor = screen.getByRole('region', { name: 'CV editing controls' }); editor.scrollIntoView = jest.fn(); fireEvent.click(screen.getByRole('button', { name: 'Go to editor' })); await waitFor(() => expect(editor).toHaveFocus()); expect(headline).toHaveValue('Retained preview draft'); expect(screen.getByText('Unsaved')).toBeInTheDocument(); expect(mockedApi.put).not.toHaveBeenCalled(); await act(async () => { jest.advanceTimersByTime(800); }); expect(mockedApi.put).toHaveBeenCalledWith('/cv/variants/3', expect.objectContaining({ expectedVersion: 4, settings: expect.objectContaining({ headline: 'Retained preview draft' }) })); expect(screen.getByText('Saved')).toBeInTheDocument(); expect(headline).toHaveValue('Retained preview draft'); } finally { jest.useRealTimers(); } }); async function reviewDocumentSuggestion(result: string) { routeGet(() => Promise.resolve({ data: variant }), { headline: 'Canonical headline', sections: [] }); mockedApi.post.mockImplementation((url: string) => Promise.resolve({ data: url === '/cv/ai/assist' ? { original: JSON.stringify([{ id: 'headline', items: ['Canonical headline'] }]), result, proposalToken: 'bound-proposal' } : url === '/cv/variants/3/ai/apply' ? { ...variant, version: 5, settings: { ...variant.settings, headline: 'Reviewed headline' } } : { html: '

Preview

' } } as never)); mockedApi.put.mockImplementation((_url, body) => Promise.resolve({ data: { ...variant, ...(body as object) } } as never)); renderAt(3); await screen.findByDisplayValue('Backend CV'); fireEvent.click(screen.getByRole('tab', { name: 'AI Tools' })); fireEvent.click(screen.getAllByRole('button', { name: 'Review suggestion' })[0]); await waitFor(() => expect(mockedApi.post).toHaveBeenCalledWith('/cv/ai/assist', expect.objectContaining({ action: 'document-translate', variantId: 3, expectedVersion: 4, }))); const request = mockedApi.post.mock.calls.find(([url]) => url === '/cv/ai/assist')![1] as any; expect(request).not.toHaveProperty('text'); expect(request).not.toHaveProperty('targetId'); } test.each(['Apply', 'Reject'])('[PRES-C11 PRES-C07] whole-document AI %s changes only an explicitly accepted CV override', async action => { await reviewDocumentSuggestion(JSON.stringify([{ id: 'headline', items: ['Reviewed headline'] }])); expect(await screen.findByText('Review 1 proposed change(s)')).toBeInTheDocument(); expect(screen.getByText('Canonical headline')).toBeInTheDocument(); expect(screen.getByText('Reviewed headline')).toBeInTheDocument(); expect(mockedApi.put).not.toHaveBeenCalled(); fireEvent.click(screen.getByRole('button', { name: action })); await waitFor(() => expect(screen.queryByText('Review 1 proposed change(s)')).not.toBeInTheDocument()); fireEvent.click(screen.getByRole('tab', { name: 'Content' })); expect(await screen.findByLabelText('Headline override')).toHaveValue(action === 'Apply' ? 'Reviewed headline' : ''); if (action === 'Apply') { expect(mockedApi.post).toHaveBeenCalledWith('/cv/variants/3/ai/apply', { proposalToken: 'bound-proposal' }); expect(screen.getByText('Saved')).toBeInTheDocument(); expect(mockedApi.put).not.toHaveBeenCalled(); } else { expect(screen.getByText('Saved')).toBeInTheDocument(); expect(mockedApi.put).not.toHaveBeenCalled(); } }); test.each([ ['invalid JSON', 'not JSON'], ['missing original ID', JSON.stringify([{ id: 'unknown', items: ['Wrong document'] }])], ['duplicate original ID', JSON.stringify([{ id: 'headline', items: ['First'] }, { id: 'headline', items: ['Second'] }])], ])('[PRES-C11] unsafe whole-document response (%s) cannot be applied', async (_case, result) => { await reviewDocumentSuggestion(result); expect(await screen.findByText('AI suggestion failed.')).toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'Apply' })).not.toBeInTheDocument(); fireEvent.click(screen.getByRole('tab', { name: 'Content' })); expect(await screen.findByLabelText('Headline override')).toHaveValue(''); expect(mockedApi.put).not.toHaveBeenCalled(); }); test('bound document generation saves the pending draft, reviews server Original, and accepted AI remains undoable', async () => { routeGet(() => Promise.resolve({ data: variant }), { headline: 'Outline headline', sections: [] }); mockedApi.put.mockImplementation((_url, body) => Promise.resolve({ data: { ...variant, ...(body as object), version: 5 } } as never)); mockedApi.post.mockImplementation((url: string) => Promise.resolve({ data: url === '/cv/ai/assist' ? { original: JSON.stringify([{ id: 'headline', items: ['Server source headline'] }]), result: JSON.stringify([{ id: 'headline', items: ['Accepted headline'] }]), proposalToken: 'saved-source-token' } : url === '/cv/variants/3/ai/apply' ? { ...variant, version: 6, settings: { ...variant.settings, headline: 'Accepted headline' } } : { html: '

Preview

' } } as never)); renderAt(3); await openContentTab(); fireEvent.change(screen.getByLabelText('Headline override'), { target: { value: 'Draft before generation' } }); fireEvent.click(screen.getByRole('tab', { name: 'AI Tools' })); fireEvent.click(screen.getAllByRole('button', { name: 'Review suggestion' })[0]); expect(await screen.findByText('Server source headline')).toBeInTheDocument(); expect(screen.queryByText('Outline headline')).not.toBeInTheDocument(); expect(mockedApi.put).toHaveBeenCalledTimes(1); expect(mockedApi.post).toHaveBeenCalledWith('/cv/ai/assist', expect.objectContaining({ variantId: 3, expectedVersion: 5 })); fireEvent.click(screen.getByRole('button', { name: 'Apply' })); await waitFor(() => expect(screen.queryByRole('button', { name: 'Apply' })).not.toBeInTheDocument()); expect(mockedApi.put).toHaveBeenCalledTimes(1); fireEvent.click(screen.getByRole('tab', { name: 'Content' })); expect(screen.getByLabelText('Headline override')).toHaveValue('Accepted headline'); fireEvent.click(screen.getByRole('button', { name: 'Undo' })); expect(screen.getByLabelText('Headline override')).toHaveValue('Draft before generation'); fireEvent.click(screen.getByRole('button', { name: 'Save now' })); await waitFor(() => expect(mockedApi.put).toHaveBeenLastCalledWith('/cv/variants/3', expect.objectContaining({ expectedVersion: 6, settings: expect.objectContaining({ headline: 'Draft before generation' }) }))); await screen.findByText('Saved'); fireEvent.click(screen.getByRole('button', { name: 'Undo' })); expect(screen.getByLabelText('Headline override')).toHaveValue(''); fireEvent.click(screen.getByRole('button', { name: 'Redo' })); expect(screen.getByLabelText('Headline override')).toHaveValue('Draft before generation'); }); test('a newer draft is flushed before stale proposal acceptance fails without rebasing or changing content', async () => { await reviewDocumentSuggestion(JSON.stringify([{ id: 'headline', items: ['Reviewed headline'] }])); mockedApi.post.mockImplementation((url: string) => url === '/cv/variants/3/ai/apply' ? Promise.reject({ response: { status: 409, data: { code: 'cv_ai_source_changed' } } }) : Promise.resolve({ data: { html: '

Preview

' } } as never)); mockedApi.put.mockImplementation((_url, body) => Promise.resolve({ data: { ...variant, ...(body as object), version: 5 } } as never)); fireEvent.change(screen.getByDisplayValue('Backend CV'), { target: { value: 'Newer retained name' } }); fireEvent.click(screen.getByRole('button', { name: 'Apply' })); expect(await screen.findByText('AI suggestion failed.')).toBeInTheDocument(); expect(mockedApi.put).toHaveBeenCalledWith('/cv/variants/3', expect.objectContaining({ name: 'Newer retained name', expectedVersion: 4 })); expect(mockedApi.post).toHaveBeenCalledWith('/cv/variants/3/ai/apply', { proposalToken: 'bound-proposal' }); expect(mockedApi.post.mock.calls.filter(([url]) => url === '/cv/ai/assist')).toHaveLength(1); expect(screen.getByDisplayValue('Newer retained name')).toBeInTheDocument(); fireEvent.click(screen.getByRole('tab', { name: 'Content' })); expect(screen.getByLabelText('Headline override')).toHaveValue(''); expect(screen.getByRole('button', { name: 'Undo' })).toBeDisabled(); }); test('late document proposals cannot return after tab unmount or a source undo', async () => { let resolveProposal: (response: any) => void = () => {}; routeGet(() => Promise.resolve({ data: variant }), { headline: 'Canonical headline', sections: [] }); mockedApi.post.mockImplementation((url: string) => url === '/cv/ai/assist' ? new Promise(resolve => { resolveProposal = resolve; }) : Promise.resolve({ data: { html: '

Preview

' } } as never)); renderAt(3); await openContentTab(); fireEvent.change(screen.getByLabelText('Headline override'), { target: { value: 'Generation source' } }); fireEvent.click(screen.getByRole('tab', { name: 'AI Tools' })); fireEvent.click(screen.getAllByRole('button', { name: 'Review suggestion' })[0]); await waitFor(() => expect(mockedApi.post).toHaveBeenCalledWith('/cv/ai/assist', expect.any(Object))); fireEvent.click(screen.getByRole('button', { name: 'Undo' })); await act(async () => resolveProposal({ data: { original: JSON.stringify([{ id: 'headline', items: ['Generation source'] }]), result: JSON.stringify([{ id: 'headline', items: ['Late proposal'] }]), proposalToken: 'late-token' } })); expect(screen.queryByText('Late proposal')).not.toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'Apply' })).not.toBeInTheDocument(); fireEvent.click(screen.getAllByRole('button', { name: 'Review suggestion' })[0]); await waitFor(() => expect(mockedApi.post.mock.calls.filter(([url]) => url === '/cv/ai/assist')).toHaveLength(2)); fireEvent.click(screen.getByRole('tab', { name: 'Content' })); await act(async () => resolveProposal({ data: { original: JSON.stringify([{ id: 'headline', items: ['Canonical headline'] }]), result: JSON.stringify([{ id: 'headline', items: ['Unmounted proposal'] }]), proposalToken: 'unmounted-token' } })); fireEvent.click(screen.getByRole('tab', { name: 'AI Tools' })); expect(screen.queryByText('Unmounted proposal')).not.toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'Apply' })).not.toBeInTheDocument(); }); test.each([ { label: 'Skills', targetId: 'section:skills', outline: { sections: [{ key: 'skills', title: 'Skills', kind: 'tags', tags: ['Original item'], bullets: [], entries: [] }] }, customSections: [] }, { label: 'Custom evidence', targetId: 'custom:sample', outline: { sections: [] }, customSections: [{ key: 'sample', title: 'Custom evidence', contentType: 'paragraphs', items: ['Original item'] }] }, { label: 'Experience', targetId: 'entry:experience:job-1', outline: { sections: [{ key: 'experience', title: 'Experience', kind: 'entries', tags: [], bullets: [], entries: [{ key: 'job-1', title: 'Original role', subtitle: 'Original employer', bullets: ['Original item'], tags: [] }] }] }, customSections: [] }, ])('bound $targetId assistant sends explicit scope and edited wording to server acceptance', async fixture => { const originalSettings = { ...variant.settings, customSections: fixture.customSections }; routeGet(() => Promise.resolve({ data: { ...variant, settings: originalSettings } }), fixture.outline); mockedApi.post.mockImplementation((url: string) => Promise.resolve({ data: url === '/cv/ai/assist' ? { original: 'Original item', result: 'Suggested item', proposalToken: `token:${fixture.targetId}` } : url === '/cv/variants/3/ai/apply' ? { ...variant, version: 5, settings: { ...originalSettings, headline: 'Server accepted readback' } } : { html: '

Preview

' } } as never)); renderAt(3); await openContentTab(); fireEvent.click(screen.getByRole('button', { name: `Expand ${fixture.label}` })); fireEvent.click(await screen.findByRole('button', { name: 'AI suggestions' })); fireEvent.click(screen.getByRole('button', { name: 'Suggest' })); const suggestion = await screen.findByLabelText('Suggested — edit before applying'); expect(mockedApi.post).toHaveBeenCalledWith('/cv/ai/assist', expect.objectContaining({ variantId: 3, expectedVersion: 4, targetId: fixture.targetId, text: 'Original item' })); fireEvent.change(suggestion, { target: { value: 'Edited approved item' } }); fireEvent.click(screen.getByRole('button', { name: 'Apply' })); await waitFor(() => expect(screen.getByLabelText('Headline override')).toHaveValue('Server accepted readback')); expect(mockedApi.post).toHaveBeenCalledWith('/cv/variants/3/ai/apply', { proposalToken: `token:${fixture.targetId}`, result: 'Edited approved item' }); expect(mockedApi.put).not.toHaveBeenCalled(); expect(screen.getByRole('button', { name: 'Undo' })).toBeEnabled(); }); test('user edits made during server acceptance are retained instead of overwritten by its late saved response', async () => { await reviewDocumentSuggestion(JSON.stringify([{ id: 'headline', items: ['Reviewed headline'] }])); let resolveApply: (response: any) => void = () => {}; mockedApi.post.mockImplementation((url: string) => url === '/cv/variants/3/ai/apply' ? new Promise(resolve => { resolveApply = resolve; }) : Promise.resolve({ data: { html: '

Preview

' } } as never)); fireEvent.click(screen.getByRole('button', { name: 'Apply' })); await waitFor(() => expect(mockedApi.post).toHaveBeenCalledWith('/cv/variants/3/ai/apply', expect.any(Object))); expect(screen.getByRole('button', { name: 'Apply' })).toBeDisabled(); fireEvent.change(screen.getByDisplayValue('Backend CV'), { target: { value: 'Retained during acceptance' } }); await act(async () => resolveApply({ data: { ...variant, version: 5, settings: { ...variant.settings, headline: 'Late accepted headline' } } })); expect(await screen.findByText('CV changed elsewhere')).toBeInTheDocument(); expect(screen.getByDisplayValue('Retained during acceptance')).toBeInTheDocument(); fireEvent.click(screen.getByRole('tab', { name: 'Content' })); expect(screen.getByLabelText('Headline override')).toHaveValue(''); expect(mockedApi.put).not.toHaveBeenCalled(); });