feat: complete phase 2 UX improvements
This commit is contained in:
@@ -7,17 +7,6 @@ import ProfilePage from './views/ProfilePage';
|
||||
import CareerProfilePage from './views/CareerProfilePage';
|
||||
import { api } from './api';
|
||||
|
||||
const createObjectURLMock = jest.fn(() => 'blob:mock-pdf');
|
||||
const revokeObjectURLMock = jest.fn();
|
||||
Object.defineProperty(window.URL, 'createObjectURL', {
|
||||
writable: true,
|
||||
value: createObjectURLMock,
|
||||
});
|
||||
Object.defineProperty(window.URL, 'revokeObjectURL', {
|
||||
writable: true,
|
||||
value: revokeObjectURLMock,
|
||||
});
|
||||
|
||||
jest.mock('./api', () => ({
|
||||
api: {
|
||||
get: jest.fn(),
|
||||
@@ -34,7 +23,7 @@ jest.mock('./components/CropImageDialog', () => () => null);
|
||||
|
||||
const mockedApi = api as jest.Mocked<typeof api>;
|
||||
|
||||
const REWRITE_TEMPLATES_COUNT = 6;
|
||||
let extractionRunsResponse: any[] = [];
|
||||
|
||||
const structuredCv = {
|
||||
version: '1',
|
||||
@@ -95,6 +84,18 @@ function renderPage() {
|
||||
void ProfilePage;
|
||||
|
||||
beforeEach(() => {
|
||||
extractionRunsResponse = [{
|
||||
id: 12,
|
||||
trigger: 'upload',
|
||||
status: 'applied',
|
||||
artifactFileName: 'resume.pdf',
|
||||
startedAtUtc: '2026-03-28T12:00:00Z',
|
||||
completedAtUtc: '2026-03-28T12:00:05Z',
|
||||
appliedAtUtc: '2026-03-28T12:00:05Z',
|
||||
parserVersion: 'm005-s01',
|
||||
normalizerVersion: 'm005-s01',
|
||||
llmPromptVersion: 'm005-s01',
|
||||
}];
|
||||
mockedApi.get.mockImplementation((url: string) => {
|
||||
if (url === '/career/profile') {
|
||||
// Phase 3: /career reads the structured profile from the relational source of truth.
|
||||
@@ -125,22 +126,18 @@ beforeEach(() => {
|
||||
} as any);
|
||||
}
|
||||
if (url === '/profile-cv/runs') {
|
||||
return Promise.resolve({
|
||||
data: [
|
||||
{
|
||||
id: 12,
|
||||
trigger: 'upload',
|
||||
status: 'applied',
|
||||
artifactFileName: 'resume.pdf',
|
||||
startedAtUtc: '2026-03-28T12:00:00Z',
|
||||
completedAtUtc: '2026-03-28T12:00:05Z',
|
||||
appliedAtUtc: '2026-03-28T12:00:05Z',
|
||||
parserVersion: 'm005-s01',
|
||||
normalizerVersion: 'm005-s01',
|
||||
llmPromptVersion: 'm005-s01',
|
||||
},
|
||||
],
|
||||
} as any);
|
||||
return Promise.resolve({ data: extractionRunsResponse } as any);
|
||||
}
|
||||
if (/^\/profile-cv\/runs\/\d+\/diff$/.test(url)) {
|
||||
return Promise.resolve({ data: {
|
||||
runId: 13,
|
||||
status: 'pending_review',
|
||||
diff: { totalAdded: 3, totalUpdated: 1, totalLowConfidence: 1, hasChanges: true, categories: [
|
||||
{ category: 'Skills', added: [{ id: 'Skills|docker', label: 'Docker', confidence: 'High' }], updated: [], unchangedCount: 2, lowConfidenceCount: 0 },
|
||||
{ category: 'Languages', added: [{ id: 'Languages|french', label: 'French', confidence: 'Low' }], updated: [], unchangedCount: 1, lowConfidenceCount: 1 },
|
||||
{ category: 'Experience', added: [{ id: 'Experience|new co engineer', label: 'Engineer - New Co', confidence: 'High' }], updated: [{ id: 'Experience|demo co developer', label: 'Developer - Demo Co', confidence: 'High' }], unchangedCount: 0, lowConfidenceCount: 0 },
|
||||
] },
|
||||
} } as any);
|
||||
}
|
||||
if (url === '/jobapplications') {
|
||||
return Promise.resolve({
|
||||
@@ -180,12 +177,6 @@ beforeEach(() => {
|
||||
},
|
||||
} as any);
|
||||
}
|
||||
if (url === '/profile-cv/rewrite-preview') {
|
||||
return Promise.resolve({ data: { templateId: 'harvard', html: '<html><body>Preview</body></html>', suggestedFileName: 'harvard-preview.pdf', fullText: 'Professional Summary\nClearer, sharper positioning for backend platform roles.', rewrittenText: 'Professional Summary\nClearer, sharper positioning for backend platform roles.', structuredCv, sectionName: null, jobApplicationId: 42, targetRole: 'Senior Backend Engineer' } } as any);
|
||||
}
|
||||
if (url === '/profile-cv/export-pdf') {
|
||||
return Promise.resolve({ data: new Blob([`pdf-${payload?.templateId ?? 'ats-minimal'}`], { type: 'application/pdf' }), config } as any);
|
||||
}
|
||||
if (url === '/profile-cv/reprocess') {
|
||||
return Promise.resolve({ data: { reprocessed: true } } as any);
|
||||
}
|
||||
@@ -197,8 +188,6 @@ beforeEach(() => {
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
createObjectURLMock.mockClear();
|
||||
revokeObjectURLMock.mockClear();
|
||||
});
|
||||
|
||||
test('profile page loads persisted structured cv and can re-parse it', async () => {
|
||||
@@ -210,6 +199,8 @@ test('profile page loads persisted structured cv and can re-parse it', async ()
|
||||
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');
|
||||
@@ -299,61 +290,15 @@ test('profile page keeps raw extraction collapsed until expanded', async () => {
|
||||
expect(await screen.findByText(/cv ready/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/your career information stays front and center/i)).toBeInTheDocument();
|
||||
|
||||
// Reveal the advanced tools (Phase 1 increment 2) — the template-builder copy button lives there.
|
||||
fireEvent.click(screen.getByRole('button', { name: /advanced cv tools/i }));
|
||||
|
||||
const originalExtractionToggle = screen.getByRole('button', { name: /original import/i });
|
||||
expect(originalExtractionToggle).toHaveAttribute('aria-expanded', 'false');
|
||||
const copyButton = screen.getByRole('button', { name: /copy cv text/i });
|
||||
expect(copyButton).toBeDisabled();
|
||||
expect(screen.queryByRole('button', { name: /copy cv text/i })).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(originalExtractionToggle);
|
||||
|
||||
expect(originalExtractionToggle).toHaveAttribute('aria-expanded', 'true');
|
||||
expect(await screen.findByLabelText(/profile cv \/ master resume text/i)).toHaveValue('Professional Summary\nBuilt backend systems');
|
||||
const copyButtons = screen.getAllByRole('button', { name: /copy cv text/i });
|
||||
expect(copyButtons.some((button) => !button.hasAttribute('disabled'))).toBe(true);
|
||||
});
|
||||
|
||||
test('profile page rewrite tools use selected template and saved job context', async () => {
|
||||
renderPage();
|
||||
|
||||
// Template-driven CV builder is hidden from the default workflow (Phase 1 increment 2); reveal it.
|
||||
await screen.findByRole('button', { name: /advanced cv tools/i });
|
||||
fireEvent.click(screen.getByRole('button', { name: /advanced cv tools/i }));
|
||||
expect(await screen.findByText(/template-driven cv builder/i)).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByText(/harvard/i));
|
||||
fireEvent.change(screen.getByLabelText(/prompt-based cv brief/i), { target: { value: 'Highlight backend platform ownership, distributed systems, and cross-team delivery.' } });
|
||||
fireEvent.change(screen.getByLabelText(/target role/i), { target: { value: 'Senior Platform Engineer' } });
|
||||
const rewriteButton = screen.getByRole('button', { name: /build preview/i });
|
||||
fireEvent.click(rewriteButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockedApi.post).toHaveBeenCalledWith('/profile-cv/rewrite-preview', expect.objectContaining({
|
||||
sectionName: null,
|
||||
style: 'harvard',
|
||||
templateId: 'harvard',
|
||||
jobApplicationId: null,
|
||||
promptBackground: 'Highlight backend platform ownership, distributed systems, and cross-team delivery.',
|
||||
targetRole: 'Senior Platform Engineer',
|
||||
language: 'English',
|
||||
tone: 'Concise and direct',
|
||||
}));
|
||||
});
|
||||
|
||||
expect(await screen.findByText(/preview ready/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/clearer, sharper positioning for backend platform roles/i)).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: /pdf carousel/i })).toBeInTheDocument();
|
||||
|
||||
const buildCarouselButton = screen.getByRole('button', { name: /build pdf carousel/i });
|
||||
fireEvent.click(buildCarouselButton);
|
||||
|
||||
await waitFor(() => {
|
||||
const exportCalls = mockedApi.post.mock.calls.filter(([url]) => url === '/profile-cv/export-pdf');
|
||||
expect(exportCalls.length).toBe(REWRITE_TEMPLATES_COUNT);
|
||||
});
|
||||
|
||||
await waitFor(() => expect(createObjectURLMock).toHaveBeenCalledTimes(REWRITE_TEMPLATES_COUNT));
|
||||
expect(screen.getByRole('button', { name: /copy cv text/i })).toBeEnabled();
|
||||
});
|
||||
|
||||
test('saving the master profile (career) persists the structured profile via /career/profile', async () => {
|
||||
@@ -388,3 +333,28 @@ test('/career shows the profile completeness overview', async () => {
|
||||
expect(await screen.findByText(/profile completeness/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/70%/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('pending CV extraction can be reviewed and applied', async () => {
|
||||
extractionRunsResponse = [{
|
||||
id: 13,
|
||||
trigger: 'upload',
|
||||
status: 'pending_review',
|
||||
artifactFileName: 'new-resume.pdf',
|
||||
startedAtUtc: '2026-03-29T12:00:00Z',
|
||||
completedAtUtc: '2026-03-29T12:00:05Z',
|
||||
parserVersion: 'm005-s01',
|
||||
normalizerVersion: 'm005-s01',
|
||||
llmPromptVersion: 'm005-s01',
|
||||
}];
|
||||
renderPage();
|
||||
|
||||
expect(await screen.findByText(/3 additions/i)).toBeInTheDocument();
|
||||
const lowConfidence = screen.getByRole('checkbox', { name: /include low-confidence languages: french/i });
|
||||
expect(lowConfidence).not.toBeChecked();
|
||||
fireEvent.click(lowConfidence);
|
||||
fireEvent.click(screen.getByRole('button', { name: /apply changes/i }));
|
||||
|
||||
await waitFor(() => expect(mockedApi.post).toHaveBeenCalledWith('/profile-cv/runs/13/accept', {
|
||||
acceptedLowConfidenceIds: ['Languages|french'],
|
||||
}));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user