feat(cv): redesign document editing with revision-safe letters and writing tools

This commit is contained in:
cesnimda
2026-10-25 22:44:14 +01:00
parent 01e10996c5
commit 2bfaf6c14b
57 changed files with 2859 additions and 118 deletions
@@ -58,6 +58,7 @@ function renderAt(id: number) {
beforeEach(() => {
window.localStorage.removeItem('uiLanguage');
global.ResizeObserver = class { observe() {} unobserve() {} disconnect() {} };
jest.clearAllMocks();
mockedApi.put.mockReset();
mockedApi.put.mockResolvedValue({ data: { ...variant, version: variant.version + 1 } } as any);
@@ -90,6 +91,66 @@ test('content sections follow the editing-region heading without mutating CV set
expect(mockedApi.put).not.toHaveBeenCalled();
});
test('settings history explains current Career Profile resolution without writing data', async () => {
routeGet(() => Promise.resolve({ data: variant }));
renderAt(3);
await openContentTab();
await act(async () => fireEvent.click(screen.getByRole('tab', { name: 'History' })));
expect(screen.getByText(/These are CV settings revisions/)).toHaveTextContent('current Career Profile');
expect(mockedApi.put).not.toHaveBeenCalled();
});
test('PDF export locks immediately, rejects a newer unsaved draft, and keeps that draft recoverable', async () => {
let complete!: (value: any) => void;
mockedApi.put.mockImplementation(() => new Promise(resolve => { complete = resolve; }));
routeGet(() => Promise.resolve({ data: variant }));
renderAt(3);
await openContentTab();
const headline = screen.getByLabelText('Headline override');
fireEvent.change(headline, { target: { value: 'First intended draft' } });
fireEvent.click(screen.getByRole('button', { name: 'Download PDF' }));
await waitFor(() => expect(mockedApi.put).toHaveBeenCalledTimes(1));
expect(screen.getByRole('button', { name: /Exporting/ })).toBeDisabled();
fireEvent.change(headline, { target: { value: 'Newer retained draft' } });
await act(async () => complete({ data: { ...variant, version: 5 } }));
await waitFor(() => expect(screen.getByRole('button', { name: 'Download PDF' })).toBeEnabled());
expect(headline).toHaveValue('Newer retained draft');
expect(mockedApi.post.mock.calls.some(([url]) => url.includes('/export-pdf'))).toBe(false);
});
test('edits during a held PDF response prevent an older document download', async () => {
let complete!: (value: any) => void;
const createUrl = jest.fn(() => 'blob:test-pdf');
Object.defineProperty(URL, 'createObjectURL', { configurable: true, value: createUrl });
const click = jest.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {});
mockedApi.post.mockImplementation((url: string) => url.endsWith('/export-pdf')
? new Promise(resolve => { complete = resolve; })
: Promise.resolve({ data: { html: '<p>preview</p>' } } as any));
try {
routeGet(() => Promise.resolve({ data: variant }));
renderAt(3);
await openContentTab();
fireEvent.click(screen.getByRole('button', { name: 'Download PDF' }));
await waitFor(() => expect(mockedApi.post).toHaveBeenCalledWith('/cv/variants/3/export-pdf', {}, { responseType: 'blob', params: { expectedVersion: 4 } }));
fireEvent.change(screen.getByLabelText('Headline override'), { target: { value: 'Edit during PDF rendering' } });
await act(async () => complete({ data: new Blob(['older document']) }));
expect(createUrl).not.toHaveBeenCalled();
expect(screen.getByLabelText('Headline override')).toHaveValue('Edit during PDF rendering');
} finally { click.mockRestore(); Reflect.deleteProperty(URL, 'createObjectURL'); }
});
test('moving a section to the first position announces its actual order and focuses its row', async () => {
routeGet(() => Promise.resolve({ data: { ...variant, settings: { ...variant.settings,
sections: [{ key: 'summary' }, { key: 'experience' }],
} } }));
renderAt(3);
await openContentTab();
const move = screen.getByRole('button', { name: 'Move Experience section up' });
act(() => { move.focus(); fireEvent.click(move); });
expect(screen.getByRole('status', { name: 'Section order' }).textContent).toMatch(/Experience: position 1 of/);
await waitFor(() => expect(screen.getByRole('button', { name: 'Expand Experience' })).toHaveFocus());
});
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 }],
@@ -448,6 +509,10 @@ test('choosing a template restores its own structure instead of inheriting the p
fireEvent.click(screen.getByRole('tab', { name: 'Template' }));
fireEvent.click(screen.getByRole('button', { name: 'All templates' }));
fireEvent.click(screen.getByRole('button', { name: /Technical/ }));
const candidate = await screen.findByRole('dialog', { name: /Preview design/ });
const apply = within(candidate).getByRole('button', { name: 'Apply template' });
await waitFor(() => expect(apply).toBeEnabled());
fireEvent.click(apply);
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' }));
@@ -771,6 +836,12 @@ test('[P5-11 PRES-C06 C12] page count is announced only after the renderer has s
expect(region).toHaveAttribute('aria-busy', 'false');
expect(screen.getByText('3-page CV')).toBeInTheDocument();
expect(region.querySelectorAll('[data-cv-page]')).toHaveLength(3);
const second = region.querySelector('[data-cv-page="2"]') as HTMLElement;
second.getBoundingClientRect = () => ({ top: 850 } as DOMRect);
region.getBoundingClientRect = () => ({ top: 50 } as DOMRect);
fireEvent.mouseDown(screen.getByRole('combobox', { name: 'Go to page' }));
fireEvent.click(screen.getByRole('option', { name: 'Page 2 of 3' }));
expect(region.scrollTop).toBe(800);
});
test('[P5-11 PRES-C06 E09] overlapping preview successes and failures cannot replace the newest document', async () => {