fix(cv): protect revisions and measure rendering readiness
This commit is contained in:
@@ -60,16 +60,18 @@ 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: '<p>cv</p>', suggestedFileName: 'cv.pdf' } } as any);
|
||||
});
|
||||
|
||||
afterEach(() => window.localStorage.removeItem('uiLanguage'));
|
||||
|
||||
function routeGet(onVariant: () => Promise<any>, outline: any = { sections: [] }) {
|
||||
function routeGet(onVariant: () => Promise<any>, 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);
|
||||
});
|
||||
}
|
||||
@@ -153,6 +155,107 @@ test('session undo and redo restore content edits before autosave', async () =>
|
||||
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: '<p>CV</p>' } } 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('professional editor starts with templates and combines design and layout controls', async () => {
|
||||
routeGet(() => Promise.resolve({ data: variant } as any));
|
||||
renderAt(3);
|
||||
|
||||
Reference in New Issue
Block a user