feat(cv): redesign editor frame and section navigation
This commit is contained in:
@@ -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' }));
|
||||
|
||||
Reference in New Issue
Block a user