refactor(profile): split account and career into dedicated components
Phase 2.2 — stop backing /profile and /career from one component behind a boolean. /career now renders a dedicated CareerProfilePage; /profile keeps ProfilePage. Each hardcodes its mode and saves only its own concern (identity vs master profile) via the partial-update endpoint. This commit is the behaviour-preserving checkpoint: the two components still share the full implementation (each carries all state, only its own JSX renders). The per-component pruning that removes the other concern's state/JSX follows in subsequent commits, verified by tsc at each step. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|||||||
import { ToastProvider } from './toast';
|
import { ToastProvider } from './toast';
|
||||||
import { I18nProvider } from './i18n/I18nProvider';
|
import { I18nProvider } from './i18n/I18nProvider';
|
||||||
import ProfilePage from './views/ProfilePage';
|
import ProfilePage from './views/ProfilePage';
|
||||||
|
import CareerProfilePage from './views/CareerProfilePage';
|
||||||
import { api } from './api';
|
import { api } from './api';
|
||||||
|
|
||||||
const createObjectURLMock = jest.fn(() => 'blob:mock-pdf');
|
const createObjectURLMock = jest.fn(() => 'blob:mock-pdf');
|
||||||
@@ -76,16 +77,22 @@ const structuredCv = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
function renderPage(props: { careerOnly?: boolean } = {}) {
|
function renderWith(Component: React.ComponentType) {
|
||||||
return render(
|
return render(
|
||||||
<ToastProvider>
|
<ToastProvider>
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<ProfilePage {...props} />
|
<Component />
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</ToastProvider>,
|
</ToastProvider>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// /profile (account identity) — the CV-editing surface still renders here (display:none) because
|
||||||
|
// the two components share the same underlying implementation; these tests exercise that surface.
|
||||||
|
function renderPage() {
|
||||||
|
return renderWith(ProfilePage);
|
||||||
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockedApi.get.mockImplementation((url: string) => {
|
mockedApi.get.mockImplementation((url: string) => {
|
||||||
if (url === '/auth/me') {
|
if (url === '/auth/me') {
|
||||||
@@ -282,9 +289,9 @@ test('profile page rewrite tools use selected template and saved job context', a
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('saving the master profile (career) persists structured cv json', async () => {
|
test('saving the master profile (career) persists structured cv json', async () => {
|
||||||
// Phase 2: the master-profile save lives on /career (careerOnly). It sends only the CV fields —
|
// Phase 2: the master-profile save lives on /career (CareerProfilePage). It sends only the CV
|
||||||
// identity is saved separately on /profile — so the payload carries profileCvStructureJson.
|
// fields — identity is saved separately on /profile — so the payload carries profileCvStructureJson.
|
||||||
renderPage({ careerOnly: true });
|
renderWith(CareerProfilePage);
|
||||||
|
|
||||||
expect(await screen.findByText(/cv ready/i)).toBeInTheDocument();
|
expect(await screen.findByText(/cv ready/i)).toBeInTheDocument();
|
||||||
const fullNameInput = screen.getByLabelText(/full name/i);
|
const fullNameInput = screen.getByLabelText(/full name/i);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -2,11 +2,11 @@ import React from "react";
|
|||||||
|
|
||||||
import { Alert, Box, Paper, Typography } from "@mui/material";
|
import { Alert, Box, Paper, Typography } from "@mui/material";
|
||||||
|
|
||||||
import ProfilePage from "./ProfilePage";
|
import CareerProfilePage from "./CareerProfilePage";
|
||||||
|
|
||||||
// Phase 2: /career is the Career Workspace — the master career profile, which is the single source
|
// Phase 2 / 2.2: /career is the Career Workspace — the master career profile, the single source of
|
||||||
// of truth for all generated documents. The CV Builder is deliberately NOT here yet (Phase 4);
|
// truth for all generated documents. It is a dedicated component (CareerProfilePage), no longer a
|
||||||
// Phase 2 only establishes the master profile. The previously-inert "CV Builder" tab was removed.
|
// careerOnly fork of ProfilePage. The CV Builder is deliberately NOT here yet (Phase 4).
|
||||||
export default function CareerWorkspacePage() {
|
export default function CareerWorkspacePage() {
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: "grid", gap: 2 }}>
|
<Box sx={{ display: "grid", gap: 2 }}>
|
||||||
@@ -20,7 +20,7 @@ export default function CareerWorkspacePage() {
|
|||||||
Your master profile is the source of truth. Job-specific CV drafts remain separate and never overwrite it.
|
Your master profile is the source of truth. Job-specific CV drafts remain separate and never overwrite it.
|
||||||
</Alert>
|
</Alert>
|
||||||
<Paper sx={{ borderRadius: 4, p: { xs: 1.5, md: 2.5 }, boxShadow: "0px 8px 24px -12px rgba(15,23,42,0.12)" }}>
|
<Paper sx={{ borderRadius: 4, p: { xs: 1.5, md: 2.5 }, boxShadow: "0px 8px 24px -12px rgba(15,23,42,0.12)" }}>
|
||||||
<ProfilePage careerOnly />
|
<CareerProfilePage />
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -226,18 +226,12 @@ function FieldReviewNote({ metadata }: { metadata?: StructuredCvFieldMetadata })
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// careerOnly splits the two Phase 2 surfaces this component still backs:
|
// ProfilePage backs /profile: account identity + security + preferences. The master career
|
||||||
// false -> /profile: account identity + security + preferences
|
// profile lives in CareerProfilePage (/career). Split in Phase 2.2. Saves only identity fields
|
||||||
// true -> /career: the master career profile (source of truth)
|
// (partial update), never the master profile.
|
||||||
// Fully separating this into two components is roadmap 2.2; for now the fork keeps each route's
|
export default function ProfilePage() {
|
||||||
// content — and its Save payload — cleanly scoped.
|
// Retained so the shared JSX reads identically; hardcoded for /profile.
|
||||||
export default function ProfilePage({
|
const careerOnly = false;
|
||||||
careerOnly = false,
|
|
||||||
onMasterCvAvailabilityChange,
|
|
||||||
}: {
|
|
||||||
careerOnly?: boolean;
|
|
||||||
onMasterCvAvailabilityChange?: (hasMasterCv: boolean) => void;
|
|
||||||
}) {
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const cvInputRef = useRef<HTMLInputElement | null>(null);
|
const cvInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
|||||||
Reference in New Issue
Block a user