feat(workspace): refine career workflows

Make job/CV comparisons language-aware and filter recruitment noise. Improve responsive career navigation, shared spacing, dashboard priorities, settings, localized workspace controls, and portable browser tests.
This commit is contained in:
cesnimda
2026-08-29 16:44:44 +02:00
parent 4d4af47651
commit 9edcbfc5de
29 changed files with 827 additions and 236 deletions
+33 -13
View File
@@ -78,7 +78,8 @@ function renderWith(Component: React.ComponentType) {
// These exercise the master-CV editing surface, which lives on /career (CareerProfilePage) after
// the Phase 2.2 split. ProfilePage no longer carries it.
function renderPage() {
function renderPage(section: 'profile' | 'import' = 'profile') {
window.history.replaceState({}, '', `/career?section=${section}`);
return renderWith(CareerProfilePage);
}
void ProfilePage;
@@ -184,6 +185,7 @@ beforeEach(() => {
});
mockedApi.put.mockResolvedValue({ data: {} } as any);
window.localStorage.clear();
window.history.replaceState({}, '', '/career');
});
afterEach(() => {
@@ -196,18 +198,19 @@ test('profile page loads persisted structured cv and can re-parse it', async ()
expect(await screen.findByText(/cv ready/i)).toBeInTheDocument();
expect(screen.getByText(/profile sections/i)).toBeInTheDocument();
expect(screen.getAllByText(/career information/i).length).toBeGreaterThan(0);
expect(screen.getByLabelText(/full name/i)).toHaveValue('Demo User');
expect(screen.getByText(/high 92%/i)).toBeInTheDocument();
expect(screen.getByText(/block-1/i)).toBeInTheDocument();
fireEvent.click(screen.getByRole('tab', { name: /import review/i }));
expect(screen.getByText(/extraction history/i)).toBeInTheDocument();
expect(screen.getByText(/resume.pdf/i)).toBeInTheDocument();
expect(screen.getByText(/current run/i)).toBeInTheDocument();
expect(screen.queryByText(/template-driven cv builder/i)).not.toBeInTheDocument();
expect(screen.getByRole('link', { name: /open cv builder/i })).toHaveAttribute('href', '/career/builder');
expect(screen.getAllByText(/original import/i).length).toBeGreaterThan(0);
const originalExtractionToggle = screen.getByRole('button', { name: /original import/i });
expect(originalExtractionToggle).toHaveAttribute('aria-expanded', 'false');
expect(screen.getAllByText(/professional summary/i).length).toBeGreaterThan(0);
expect(screen.getByLabelText(/full name/i)).toHaveValue('Demo User');
expect(screen.getByText(/high 92%/i)).toBeInTheDocument();
expect(screen.getByText(/block-1/i)).toBeInTheDocument();
fireEvent.click(originalExtractionToggle);
expect(originalExtractionToggle).toHaveAttribute('aria-expanded', 'true');
@@ -273,7 +276,7 @@ test('editing a field in an extracted section updates parent state and flows int
});
test('profile page can reprocess from stored artifact history', async () => {
renderPage();
renderPage('import');
expect(await screen.findByText(/extraction history/i)).toBeInTheDocument();
const reprocessButton = screen.getByRole('button', { name: /reprocess cv/i });
@@ -354,7 +357,7 @@ test('profile page shows durable CV operation state and retries a failed run', a
canRetry: true,
},
}];
renderPage();
renderPage('import');
expect(await screen.findByText('failed')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: /retry processing/i }));
@@ -365,10 +368,9 @@ test('profile page shows durable CV operation state and retries a failed run', a
});
test('profile page keeps raw extraction collapsed until expanded', async () => {
renderPage();
renderPage('import');
expect(await screen.findByText(/cv ready/i)).toBeInTheDocument();
expect(screen.getByText(/your career information stays front and center/i)).toBeInTheDocument();
expect(await screen.findByText(/your career information stays front and center/i)).toBeInTheDocument();
const originalExtractionToggle = screen.getByRole('button', { name: /original import/i });
expect(originalExtractionToggle).toHaveAttribute('aria-expanded', 'false');
@@ -385,7 +387,7 @@ test('saving the master profile (career) persists the structured profile via /ca
// Phase 3: /career saves the master profile through the relational API. The payload carries the
// structured profile object (not the legacy flat blob) and never identity fields.
mockedApi.put.mockResolvedValue({ data: { profile: structuredCv, completeness: { percent: 70, missing: [], sections: [] }, cvText: '' } } as any);
renderWith(CareerProfilePage);
renderPage();
expect(await screen.findByText(/cv ready/i)).toBeInTheDocument();
const fullNameInput = screen.getByLabelText(/full name/i);
@@ -411,7 +413,25 @@ test('saving the master profile (career) persists the structured profile via /ca
test('/career shows the profile completeness overview', async () => {
renderWith(CareerProfilePage);
expect(await screen.findByText(/profile completeness/i)).toBeInTheDocument();
expect(screen.getByText(/70%/)).toBeInTheDocument();
expect(await screen.findByText(/70%/)).toBeInTheDocument();
});
test('/career separates overview, profile and import while preserving unsaved profile edits', async () => {
renderWith(CareerProfilePage);
await screen.findByText(/70%/);
fireEvent.click(screen.getByRole('link', { name: /edit career profile/i }));
const nameField = await screen.findByLabelText(/full name/i);
fireEvent.change(nameField, { target: { value: 'Unsaved Section Switch' } });
fireEvent.click(screen.getByRole('tab', { name: /import review/i }));
expect(screen.getByRole('heading', { name: /master cv/i })).toBeInTheDocument();
expect(window.location.search).toBe('?section=import');
fireEvent.click(screen.getByRole('tab', { name: /career profile/i }));
expect(await screen.findByLabelText(/full name/i)).toHaveValue('Unsaved Section Switch');
expect(screen.getByText('Unsaved changes')).toBeInTheDocument();
expect(window.location.search).toBe('?section=profile');
});
test('pending CV extraction can be reviewed and applied', async () => {
@@ -426,7 +446,7 @@ test('pending CV extraction can be reviewed and applied', async () => {
normalizerVersion: 'm005-s01',
llmPromptVersion: 'm005-s01',
}];
renderPage();
renderPage('import');
expect(await screen.findByText(/3 additions/i)).toBeInTheDocument();
const lowConfidence = screen.getByRole('checkbox', { name: /include low-confidence languages: french/i });