feat(cv): redesign editor frame and section navigation

This commit is contained in:
cesnimda
2026-10-07 12:42:16 +02:00
parent efc9e7bdd4
commit 44b944e5ed
45 changed files with 19230 additions and 55 deletions
@@ -81,6 +81,93 @@ async function openContentTab() {
fireEvent.click(screen.getByRole('tab', { name: 'Content' }));
}
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: '<p>CV</p>' } } 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));
@@ -256,12 +343,17 @@ test('restoring history uses the current version and never replaces edits made d
expect(mockedApi.put).not.toHaveBeenCalled();
});
test('professional editor starts with templates and combines design and layout controls', async () => {
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');
expect(screen.getByRole('tab', { name: 'Template' })).toHaveAttribute('aria-selected', 'true');
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.getByText('Typography')).toBeInTheDocument();
expect(screen.getByLabelText('Body size')).toBeInTheDocument();
@@ -304,6 +396,7 @@ test('choosing a template restores its own structure instead of inheriting the p
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(screen.getByRole('button', { name: 'Save now' }));