diff --git a/apps/web/locales/en-US.po b/apps/web/locales/en-US.po
index a934b31f6..19aaebc51 100644
--- a/apps/web/locales/en-US.po
+++ b/apps/web/locales/en-US.po
@@ -2761,6 +2761,11 @@ msgstr "Here's your new API key"
msgid "Hidden"
msgstr "Hidden"
+#: src/features/resume/builder/section-recovery.tsx
+msgid "Hidden sections"
+msgstr "Hidden sections"
+
+#: src/features/resume/builder/section-recovery.tsx
#: src/routes/builder/$resumeId/-sidebar/left/sections/custom.tsx
#: src/routes/builder/$resumeId/-sidebar/left/shared/section-item.tsx
#: src/routes/builder/$resumeId/-sidebar/left/shared/section-menu.tsx
@@ -5041,6 +5046,10 @@ msgstr "Show"
msgid "Show {0} more"
msgstr "Show {0} more"
+#: src/features/resume/builder/section-recovery.tsx
+msgid "Show {title} section"
+msgstr "Show {title} section"
+
#: src/routes/builder/$resumeId/-sidebar/right/sections/sharing.tsx
msgid "Show Download Buttons"
msgstr "Show Download Buttons"
diff --git a/apps/web/src/features/resume/builder/section-recovery.test.tsx b/apps/web/src/features/resume/builder/section-recovery.test.tsx
new file mode 100644
index 000000000..446ea3833
--- /dev/null
+++ b/apps/web/src/features/resume/builder/section-recovery.test.tsx
@@ -0,0 +1,229 @@
+// @vitest-environment happy-dom
+
+import type { ResumeData } from "@reactive-resume/schema/resume/data";
+import type { Resume } from "./draft";
+import { act, cleanup, render, screen, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
+import { i18n } from "@lingui/core";
+import { I18nProvider } from "@lingui/react";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { sampleResumeData } from "@reactive-resume/schema/resume/sample";
+import { ConfirmDialogProvider } from "@/hooks/use-confirm";
+import { CustomSectionBuilder } from "@/routes/builder/$resumeId/-sidebar/left/sections/custom";
+import { useResumeStore } from "./draft";
+import {
+ focusLeftSidebarSection,
+ getVisibleLeftSidebarSections,
+ SectionEditorList,
+ SectionRecovery,
+} from "./section-recovery";
+
+const routerParams = vi.hoisted(() => ({ resumeId: "section-recovery" }));
+
+vi.mock("@tanstack/react-router", () => ({
+ useParams: () => routerParams,
+}));
+
+vi.mock("@/libs/orpc/client", () => ({
+ orpc: {
+ resume: {
+ getById: { queryOptions: () => ({ queryKey: ["resume", "section-recovery"] }) },
+ patch: { call: vi.fn() },
+ update: { call: vi.fn(() => new Promise(() => undefined)) },
+ },
+ },
+ streamClient: { resume: { updates: { subscribe: vi.fn() } } },
+}));
+
+vi.mock("@reactive-resume/ui/components/toast", () => ({
+ toast: { add: vi.fn(), close: vi.fn() },
+}));
+
+function makeResume(data: ResumeData, isLocked = false): Resume {
+ return {
+ id: routerParams.resumeId,
+ name: "Section Recovery",
+ slug: "section-recovery",
+ tags: [],
+ data,
+ isLocked,
+ updatedAt: new Date("2026-09-06T00:00:00.000Z"),
+ };
+}
+
+function makeHiddenData(): ResumeData {
+ const data = structuredClone(sampleResumeData);
+ data.summary.hidden = true;
+ data.sections.experience.hidden = true;
+ data.sections.experience.title = "Work History";
+ data.customSections[0].hidden = true;
+ data.customSections[0].title = "Earlier Roles";
+ return data;
+}
+
+function renderRecovery(data = makeHiddenData(), isLocked = false) {
+ useResumeStore.getState().initialize(makeResume(data, isLocked));
+ return render(
+
+
+
+
+ ,
+ );
+}
+
+beforeAll(() => {
+ i18n.loadAndActivate({ locale: "en", messages: {} });
+});
+
+beforeEach(() => {
+ vi.useFakeTimers();
+ useResumeStore.getState().reset();
+});
+
+afterEach(() => {
+ cleanup();
+ vi.clearAllTimers();
+ vi.useRealTimers();
+ useResumeStore.getState().reset();
+});
+
+describe("hidden section recovery", () => {
+ it("keeps Picture, Basics, and Custom editors while removing hidden printable editors", () => {
+ const visible = getVisibleLeftSidebarSections(makeHiddenData());
+
+ expect(visible).toContain("picture");
+ expect(visible).toContain("basics");
+ expect(visible).toContain("custom");
+ expect(visible).not.toContain("summary");
+ expect(visible).not.toContain("experience");
+ });
+
+ it("does not mount full editors for hidden printable sections", () => {
+ useResumeStore.getState().initialize(makeResume(makeHiddenData()));
+ render(
+
+
+ } />
+
+ ,
+ );
+
+ expect(screen.getByTestId("editor-picture")).toBeInTheDocument();
+ expect(screen.getByTestId("editor-basics")).toBeInTheDocument();
+ expect(screen.getByTestId("editor-custom")).toBeInTheDocument();
+ expect(screen.queryByTestId("editor-summary")).not.toBeInTheDocument();
+ expect(screen.queryByTestId("editor-experience")).not.toBeInTheDocument();
+ expect(screen.getByRole("button", { name: "Show Work History section" })).toBeInTheDocument();
+ });
+
+ it("keeps the custom editor container while omitting only hidden custom children", () => {
+ const data = makeHiddenData();
+ useResumeStore.getState().initialize(makeResume(data));
+ render(
+
+
+
+
+
+
+ ,
+ );
+
+ expect(screen.queryByText("Earlier Roles")).not.toBeInTheDocument();
+ expect(screen.getAllByText("Cover Letter").length).toBeGreaterThan(0);
+ expect(screen.getByRole("button", { name: "Add a new custom section" })).toBeInTheDocument();
+ });
+
+ it("lists hidden built-in, summary, and custom sections by effective title", () => {
+ renderRecovery();
+
+ expect(screen.getByRole("region", { name: "Hidden sections" })).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: "Show Summary section" })).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: "Show Work History section" })).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: "Show Earlier Roles section" })).toBeInTheDocument();
+ });
+
+ it("shows via keyboard, changes only hidden state, and participates in undo and redo", async () => {
+ const data = makeHiddenData();
+ const before = structuredClone(data);
+ vi.useRealTimers();
+ const user = userEvent.setup();
+ renderRecovery(data);
+ const show = screen.getByRole("button", { name: "Show Work History section" });
+
+ show.focus();
+ await user.keyboard("{Enter}");
+
+ const shown = useResumeStore.getState().resume?.data;
+ expect(shown?.sections.experience.hidden).toBe(false);
+ expect({ ...shown?.sections.experience, hidden: true }).toEqual(before.sections.experience);
+ expect(shown?.metadata.layout).toEqual(before.metadata.layout);
+ expect(screen.queryByRole("button", { name: "Show Work History section" })).not.toBeInTheDocument();
+
+ act(() => useResumeStore.getState().undo());
+ expect(useResumeStore.getState().resume?.data.sections.experience.hidden).toBe(true);
+ expect(screen.getByRole("button", { name: "Show Work History section" })).toBeInTheDocument();
+
+ act(() => useResumeStore.getState().redo());
+ expect(useResumeStore.getState().resume?.data.sections.experience.hidden).toBe(false);
+ });
+
+ it("shows an unplaced hidden section without choosing a layout location", () => {
+ const data = makeHiddenData();
+ for (const page of data.metadata.layout.pages) {
+ page.main = page.main.filter((id) => id !== "experience");
+ page.sidebar = page.sidebar.filter((id) => id !== "experience");
+ }
+ const layoutBefore = structuredClone(data.metadata.layout);
+ renderRecovery(data);
+
+ act(() => screen.getByRole("button", { name: "Show Work History section" }).click());
+
+ expect(useResumeStore.getState().resume?.data.sections.experience.hidden).toBe(false);
+ expect(useResumeStore.getState().resume?.data.metadata.layout).toEqual(layoutBefore);
+ });
+
+ it("disables recovery actions for a locked resume", () => {
+ renderRecovery(makeHiddenData(), true);
+ const show = screen.getByRole("button", { name: "Show Work History section" });
+
+ expect(show).toBeDisabled();
+ show.click();
+ expect(useResumeStore.getState().resume?.data.sections.experience.hidden).toBe(true);
+ expect(useResumeStore.getState().undoStack).toHaveLength(0);
+ });
+
+ it("reopens a collapsed recovery group before focusing and scrolling the hidden section", async () => {
+ vi.useRealTimers();
+ const user = userEvent.setup();
+ const scrollIntoView = vi.fn();
+ Object.defineProperty(HTMLElement.prototype, "scrollIntoView", {
+ configurable: true,
+ value: scrollIntoView,
+ });
+ renderRecovery();
+ const trigger = screen.getByRole("button", { name: "Hidden sections" });
+
+ await user.click(trigger);
+ await waitFor(() => expect(document.getElementById("sidebar-hidden-experience")).toBeNull());
+
+ focusLeftSidebarSection("experience");
+
+ await waitFor(() => expect(trigger).toHaveAttribute("aria-expanded", "true"));
+ const recoveryEntry = await waitFor(() => {
+ const entry = document.getElementById("sidebar-hidden-experience");
+ expect(entry).not.toBeNull();
+ return entry;
+ });
+ await waitFor(() => expect(recoveryEntry).toHaveFocus());
+ expect(scrollIntoView).toHaveBeenCalledWith({
+ block: "start",
+ inline: "nearest",
+ behavior: "smooth",
+ });
+ });
+});
diff --git a/apps/web/src/features/resume/builder/section-recovery.tsx b/apps/web/src/features/resume/builder/section-recovery.tsx
new file mode 100644
index 000000000..581261739
--- /dev/null
+++ b/apps/web/src/features/resume/builder/section-recovery.tsx
@@ -0,0 +1,155 @@
+import type { ResumeData, SectionType } from "@reactive-resume/schema/resume/data";
+import type { ReactNode } from "react";
+import type { LeftSidebarSection } from "@/libs/resume/section";
+import { t } from "@lingui/core/macro";
+import { Trans } from "@lingui/react/macro";
+import { EyeClosedIcon, EyeIcon } from "@phosphor-icons/react";
+import { Fragment } from "react";
+import { getSectionAvailability } from "@reactive-resume/resume/section-availability";
+import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@reactive-resume/ui/components/accordion";
+import { Button } from "@reactive-resume/ui/components/button";
+import { Separator } from "@reactive-resume/ui/components/separator";
+import { useCurrentBuilderResumeSelector, useUpdateResumeData } from "@/features/resume/builder/draft";
+import { getSectionTitle, leftSidebarSections } from "@/libs/resume/section";
+
+export function getVisibleLeftSidebarSections(data: ResumeData): LeftSidebarSection[] {
+ const hiddenSectionIds = new Set(
+ getSectionAvailability(data)
+ .filter((section) => section.hidden)
+ .map((section) => section.sectionId),
+ );
+
+ return leftSidebarSections.filter(
+ (section) =>
+ section === "picture" || section === "basics" || section === "custom" || !hiddenSectionIds.has(section),
+ );
+}
+
+export function focusLeftSidebarSection(section: LeftSidebarSection): void {
+ const editorTarget = document.getElementById(`sidebar-${section}`);
+ if (editorTarget) {
+ editorTarget.scrollIntoView({ block: "start", inline: "nearest", behavior: "smooth" });
+ return;
+ }
+
+ const recoveryTargetId = `sidebar-hidden-${section}`;
+ const focusRecoveryTarget = () => {
+ const recoveryTarget = document.getElementById(recoveryTargetId);
+ if (!recoveryTarget) return;
+
+ recoveryTarget.focus({ preventScroll: true });
+ recoveryTarget.scrollIntoView({ block: "start", inline: "nearest", behavior: "smooth" });
+ };
+
+ const trigger = document.getElementById("sidebar-hidden-sections-trigger");
+ if (trigger?.getAttribute("aria-expanded") === "false") {
+ trigger.click();
+ requestAnimationFrame(focusRecoveryTarget);
+ return;
+ }
+
+ focusRecoveryTarget();
+}
+
+type SectionEditorListProps = {
+ renderSection: (section: LeftSidebarSection) => ReactNode;
+};
+
+export function SectionEditorList({ renderSection }: SectionEditorListProps) {
+ const sectionKey = useCurrentBuilderResumeSelector((resume) => getVisibleLeftSidebarSections(resume.data).join(","));
+ const sections = sectionKey.split(",") as LeftSidebarSection[];
+
+ return (
+ <>
+ {sections.map((section) => (
+
+ {renderSection(section)}
+
+
+ ))}
+
+ >
+ );
+}
+
+function getRecoverySectionTitle(data: ResumeData, sectionId: string): string {
+ if (sectionId === "summary") return data.summary.title || getSectionTitle("summary");
+
+ if (Object.hasOwn(data.sections, sectionId)) {
+ const type = sectionId as SectionType;
+ return data.sections[type].title || getSectionTitle(type);
+ }
+
+ const customSection = data.customSections.find((section) => section.id === sectionId);
+ return customSection?.title || (customSection ? getSectionTitle(customSection.type) : sectionId);
+}
+
+export function SectionRecovery() {
+ const data = useCurrentBuilderResumeSelector((resume) => resume.data);
+ const updateResumeData = useUpdateResumeData();
+ const hiddenSections = getSectionAvailability(data).filter((section) => section.hidden);
+
+ if (hiddenSections.length === 0) return null;
+
+ const showSection = (sectionId: string) => {
+ updateResumeData((draft) => {
+ if (sectionId === "summary") {
+ draft.summary.hidden = false;
+ return;
+ }
+
+ if (Object.hasOwn(draft.sections, sectionId)) {
+ draft.sections[sectionId as SectionType].hidden = false;
+ return;
+ }
+
+ const customSection = draft.customSections.find((section) => section.id === sectionId);
+ if (customSection) customSection.hidden = false;
+ });
+ };
+
+ return (
+
+
+
+
+
+
+ {hiddenSections.map(({ sectionId }) => {
+ const title = getRecoverySectionTitle(data, sectionId);
+
+ return (
+
+ );
+ })}
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/routes/builder/$resumeId/-sidebar/left/index.tsx b/apps/web/src/routes/builder/$resumeId/-sidebar/left/index.tsx
index 3ddb2344c..d4b88b88d 100644
--- a/apps/web/src/routes/builder/$resumeId/-sidebar/left/index.tsx
+++ b/apps/web/src/routes/builder/$resumeId/-sidebar/left/index.tsx
@@ -3,17 +3,17 @@ import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import { LockSimpleIcon } from "@phosphor-icons/react";
import { useMutation } from "@tanstack/react-query";
-import { Fragment, useCallback, useRef } from "react";
+import { useCallback, useRef } from "react";
import { match } from "ts-pattern";
import { Avatar, AvatarFallback, AvatarImage } from "@reactive-resume/ui/components/avatar";
import { Button } from "@reactive-resume/ui/components/button";
import { ScrollArea } from "@reactive-resume/ui/components/scroll-area";
-import { Separator } from "@reactive-resume/ui/components/separator";
import { toast } from "@reactive-resume/ui/components/toast";
import { Tooltip, TooltipContent, TooltipTrigger } from "@reactive-resume/ui/components/tooltip";
import { getInitials } from "@reactive-resume/utils/string";
import { CoverLetterLibraryDialog } from "@/features/cover-letters/library";
import { useCurrentResume, useIsResumeLocked, usePatchResume, useResumeStore } from "@/features/resume/builder/draft";
+import { focusLeftSidebarSection, SectionEditorList } from "@/features/resume/builder/section-recovery";
import { UserDropdownMenu } from "@/features/user/dropdown-menu";
import { getResumeErrorMessage } from "@/libs/error-message";
import { orpc } from "@/libs/orpc/client";
@@ -74,12 +74,7 @@ export function BuilderSidebarLeft() {
@@ -132,11 +127,7 @@ function SidebarEdge() {
const scrollToSection = useCallback(
(section: LeftSidebarSection) => {
toggleSidebar("left", true);
- // Section ids are globally unique; document.getElementById reliably resolves the scroll target
- // (querying through the ScrollArea ref did not — its ref does not expose the scroll container).
- document
- .getElementById(`sidebar-${section}`)
- ?.scrollIntoView({ block: "start", inline: "nearest", behavior: "smooth" });
+ focusLeftSidebarSection(section);
},
[toggleSidebar],
);
diff --git a/apps/web/src/routes/builder/$resumeId/-sidebar/left/sections/custom.tsx b/apps/web/src/routes/builder/$resumeId/-sidebar/left/sections/custom.tsx
index ad0d57eba..8e8d34438 100644
--- a/apps/web/src/routes/builder/$resumeId/-sidebar/left/sections/custom.tsx
+++ b/apps/web/src/routes/builder/$resumeId/-sidebar/left/sections/custom.tsx
@@ -127,9 +127,11 @@ export function CustomSectionBuilder() {
return (
- {customSections.map((section) => (
-
- ))}
+ {customSections
+ .filter((section) => !section.hidden)
+ .map((section) => (
+
+ ))}
{/* Add Custom Section Button */}
diff --git a/packages/resume/package.json b/packages/resume/package.json
index 98175b0a0..fa0bf05a2 100644
--- a/packages/resume/package.json
+++ b/packages/resume/package.json
@@ -11,6 +11,7 @@
"./icons": "./src/icons.ts",
"./markdown": "./src/markdown.ts",
"./patch": "./src/patch.ts",
+ "./section-availability": "./src/section-availability.ts",
"./social-meta": "./src/social-meta.ts",
"./stylesheet": "./src/stylesheet/index.ts",
"./stylesheet/registry": "./src/stylesheet/registry/index.ts",
diff --git a/packages/resume/src/section-availability.test.ts b/packages/resume/src/section-availability.test.ts
new file mode 100644
index 000000000..ce4729545
--- /dev/null
+++ b/packages/resume/src/section-availability.test.ts
@@ -0,0 +1,70 @@
+import { describe, expect, it } from "vitest";
+import { sampleResumeData } from "@reactive-resume/schema/resume/sample";
+import { getSectionAvailability } from "./section-availability";
+
+describe("section availability", () => {
+ it("includes every printable section and records every authored location", () => {
+ const data = structuredClone(sampleResumeData);
+ const customSection = data.customSections[0];
+ const laterPage = data.metadata.layout.pages[2];
+ if (!customSection || !laterPage) throw new Error("Sample resume lacks expected section fixtures.");
+ const customSectionId = customSection.id;
+ laterPage.sidebar.push(customSectionId, customSectionId, "unknown-section");
+
+ const availability = getSectionAvailability(data);
+ const sectionIds = availability.map((entry) => entry.sectionId);
+
+ expect(sectionIds).toEqual([
+ "summary",
+ ...Object.keys(data.sections),
+ ...data.customSections.map((section) => section.id),
+ ]);
+ expect(sectionIds).not.toContain("picture");
+ expect(sectionIds).not.toContain("basics");
+ expect(sectionIds).not.toContain("custom");
+ expect(sectionIds).not.toContain("unknown-section");
+ expect(availability.find((entry) => entry.sectionId === customSectionId)?.locations).toEqual([
+ { pageIndex: 1, columnId: "main" },
+ { pageIndex: 2, columnId: "sidebar" },
+ { pageIndex: 2, columnId: "sidebar" },
+ ]);
+ });
+
+ it("derives hidden and placement state independently without mutation", () => {
+ const data = structuredClone(sampleResumeData);
+ data.sections.experience.hidden = true;
+ data.sections.awards.items = [];
+ for (const page of data.metadata.layout.pages) {
+ page.main = page.main.filter((id) => id !== "experience");
+ page.sidebar = page.sidebar.filter((id) => id !== "experience");
+ page.main = page.main.filter((id) => id !== "projects");
+ page.sidebar = page.sidebar.filter((id) => id !== "projects");
+ }
+ data.summary.hidden = true;
+ const before = structuredClone(data);
+
+ const availability = getSectionAvailability(data);
+
+ expect(availability.find((entry) => entry.sectionId === "experience")).toEqual({
+ sectionId: "experience",
+ hidden: true,
+ locations: [],
+ });
+ expect(availability.find((entry) => entry.sectionId === "summary")).toEqual({
+ sectionId: "summary",
+ hidden: true,
+ locations: [{ pageIndex: 0, columnId: "main" }],
+ });
+ expect(availability.find((entry) => entry.sectionId === "projects")).toEqual({
+ sectionId: "projects",
+ hidden: false,
+ locations: [],
+ });
+ expect(availability.find((entry) => entry.sectionId === "awards")).toEqual({
+ sectionId: "awards",
+ hidden: false,
+ locations: [{ pageIndex: 1, columnId: "main" }],
+ });
+ expect(data).toEqual(before);
+ });
+});
diff --git a/packages/resume/src/section-availability.ts b/packages/resume/src/section-availability.ts
new file mode 100644
index 000000000..d76efacb2
--- /dev/null
+++ b/packages/resume/src/section-availability.ts
@@ -0,0 +1,39 @@
+import type { ResumeData } from "@reactive-resume/schema/resume/data";
+
+export type SectionLocation = {
+ pageIndex: number;
+ columnId: "main" | "sidebar";
+};
+
+export type SectionAvailability = {
+ sectionId: string;
+ hidden: boolean;
+ locations: SectionLocation[];
+};
+
+function getSectionLocations(data: ResumeData, sectionId: string): SectionLocation[] {
+ const locations: SectionLocation[] = [];
+
+ for (const [pageIndex, page] of data.metadata.layout.pages.entries()) {
+ for (const columnId of ["main", "sidebar"] as const) {
+ for (const id of page[columnId]) {
+ if (id === sectionId) locations.push({ pageIndex, columnId });
+ }
+ }
+ }
+
+ return locations;
+}
+
+export function getSectionAvailability(data: ResumeData): SectionAvailability[] {
+ const sections = [
+ { sectionId: "summary", hidden: data.summary.hidden },
+ ...Object.entries(data.sections).map(([sectionId, section]) => ({ sectionId, hidden: section.hidden })),
+ ...data.customSections.map((section) => ({ sectionId: section.id, hidden: section.hidden })),
+ ];
+
+ return sections.map((section) => ({
+ ...section,
+ locations: getSectionLocations(data, section.sectionId),
+ }));
+}
diff --git a/tests/e2e/specs/section-recovery.spec.ts b/tests/e2e/specs/section-recovery.spec.ts
new file mode 100644
index 000000000..0b5a26e10
--- /dev/null
+++ b/tests/e2e/specs/section-recovery.spec.ts
@@ -0,0 +1,199 @@
+import type { Page, TestInfo } from "@playwright/test";
+import { readFile } from "node:fs/promises";
+import { getDocument } from "pdfjs-dist/legacy/build/pdf.mjs";
+import { Pool } from "pg";
+import { createSampleResumeFromDashboard, openResumeCardMenu, openSidebarSection } from "../fixtures/resume";
+import { expect, test } from "../fixtures/test";
+
+const summaryMarker = "RECOVERY_SUMMARY_MARKER";
+const builtInMarker = "RECOVERY_BUILTIN_MARKER";
+const customMarker = "RECOVERY_CUSTOM_MARKER";
+
+type AuthoredLayout = {
+ pages: Array<{ fullWidth: boolean; main: string[]; sidebar: string[] }>;
+};
+
+async function seedRecoveryResume(resumeId: string): Promise {
+ const pool = new Pool({ connectionString: process.env.DATABASE_URL });
+
+ try {
+ const result = await pool.query<{ data: Record }>('select data from "resume" where id = $1', [
+ resumeId,
+ ]);
+ const data = result.rows[0]?.data as {
+ summary: { title: string; content: string; hidden: boolean };
+ sections: { experience: { title: string; hidden: boolean; items: Array<{ company: string }> } };
+ customSections: Array<{
+ id: string;
+ title: string;
+ hidden: boolean;
+ items: Array<{ company?: string }>;
+ }>;
+ metadata: {
+ layout: AuthoredLayout;
+ typography: { body: { fontFamily: string }; heading: { fontFamily: string } };
+ };
+ };
+ if (!data?.sections.experience.items[0] || !data.customSections[0]?.items[0]) {
+ throw new Error("Sample resume lacks recovery fixture sections.");
+ }
+
+ data.summary.title = "Recovery Summary";
+ data.summary.content = `${summaryMarker}
`;
+ data.summary.hidden = false;
+ data.sections.experience.title = "Recovery Experience";
+ data.sections.experience.items[0].company = builtInMarker;
+ data.sections.experience.hidden = false;
+ data.customSections[0].title = "Recovery Custom";
+ data.customSections[0].items[0].company = customMarker;
+ data.customSections[0].hidden = false;
+ data.metadata.typography.body.fontFamily = "Helvetica";
+ data.metadata.typography.heading.fontFamily = "Helvetica";
+ const layout = structuredClone(data.metadata.layout);
+
+ await pool.query('update "resume" set data = $2, updated_at = now() where id = $1', [resumeId, data]);
+ return layout;
+ } finally {
+ await pool.end();
+ }
+}
+
+async function readRecoveryState(resumeId: string) {
+ const pool = new Pool({ connectionString: process.env.DATABASE_URL });
+
+ try {
+ const result = await pool.query<{
+ data: {
+ summary: { hidden: boolean };
+ sections: { experience: { hidden: boolean } };
+ customSections: Array<{ title: string; hidden: boolean }>;
+ metadata: { layout: AuthoredLayout };
+ };
+ }>('select data from "resume" where id = $1', [resumeId]);
+ const data = result.rows[0]?.data;
+ if (!data) throw new Error(`Resume ${resumeId} was not found.`);
+ return data;
+ } finally {
+ await pool.end();
+ }
+}
+
+function waitForResumeSave(page: Page) {
+ return page.waitForResponse((response) => {
+ if (!response.url().includes("/api/rpc") || response.request().method() !== "POST") return false;
+ if (!response.ok()) return false;
+ return response.request().postData()?.includes('"data"') ?? false;
+ });
+}
+
+async function hideStandardSection(page: Page, navigationTitle: string, title: string) {
+ await page.getByRole("button", { name: navigationTitle, exact: true }).first().click();
+ const heading = page.getByRole("heading", { name: title, exact: true }).filter({ visible: true }).first();
+ await expect(heading).toBeVisible();
+ await heading.locator("xpath=../..").getByRole("button", { name: "Section options" }).click();
+ const saved = waitForResumeSave(page);
+ await page.getByRole("menuitem", { name: "Hide", exact: true }).click();
+ await saved;
+}
+
+async function hideCustomSection(page: Page, title: string) {
+ await openSidebarSection(page, "Custom Sections");
+ const titleElement = page.getByText(title, { exact: true }).filter({ visible: true }).first();
+ const card = titleElement.locator("xpath=../../..");
+ await card.getByRole("button", { name: "Section options" }).click();
+ const saved = waitForResumeSave(page);
+ await page.getByRole("menuitem", { name: "Hide", exact: true }).click();
+ await saved;
+}
+
+async function showSection(page: Page, title: string) {
+ const saved = waitForResumeSave(page);
+ await page.getByRole("button", { name: `Show ${title} section` }).click();
+ await saved;
+}
+
+async function downloadPdfText(page: Page, testInfo: TestInfo, name: string) {
+ await openSidebarSection(page, "Export");
+ await page.getByRole("button", { name: /Choose PDF, DOCX, Markdown, or JSON/ }).click();
+ const pending = page.waitForEvent("download");
+ await page.getByRole("button", { name: "Download PDF", exact: true }).click();
+ const download = await pending;
+ const path = testInfo.outputPath(`${name}.pdf`);
+ await download.saveAs(path);
+ await page.keyboard.press("Escape");
+
+ const loading = getDocument({ data: new Uint8Array(await readFile(path)), useSystemFonts: true });
+ try {
+ const pdf = await loading.promise;
+ const text: string[] = [];
+ for (let pageNumber = 1; pageNumber <= pdf.numPages; pageNumber++) {
+ const pdfPage = await pdf.getPage(pageNumber);
+ text.push(...(await pdfPage.getTextContent()).items.flatMap((item) => ("str" in item ? [item.str] : [])));
+ }
+ return text.join(" ");
+ } finally {
+ await loading.destroy();
+ }
+}
+
+test("recovers hidden printable sections without changing authored placement", async ({ authPage: page }, testInfo) => {
+ test.setTimeout(120_000);
+ const resumeName = await createSampleResumeFromDashboard(page, testInfo);
+ const resumeId = new URL(page.url()).pathname.split("/").at(-1);
+ if (!resumeId) throw new Error("Builder URL lacks resume id.");
+ const authoredLayout = await seedRecoveryResume(resumeId);
+ await page.reload();
+
+ await hideStandardSection(page, "Summary", "Recovery Summary");
+ await hideStandardSection(page, "Experience", "Recovery Experience");
+ await hideCustomSection(page, "Recovery Custom");
+ await page.reload();
+
+ for (const title of ["Recovery Summary", "Recovery Experience", "Recovery Custom"]) {
+ await expect(page.getByRole("button", { name: `Show ${title} section` })).toBeVisible();
+ }
+ await expect(
+ page.getByRole("heading", { name: "Recovery Summary", exact: true }).filter({ visible: true }),
+ ).toHaveCount(0);
+ await expect(
+ page.getByRole("heading", { name: "Recovery Experience", exact: true }).filter({ visible: true }),
+ ).toHaveCount(0);
+ await expect(page.getByText("Recovery Custom", { exact: true }).filter({ visible: true })).toHaveCount(1);
+
+ const hiddenPdf = await downloadPdfText(page, testInfo, "section-recovery-hidden");
+ expect(hiddenPdf).not.toContain(summaryMarker);
+ expect(hiddenPdf).not.toContain(builtInMarker);
+ expect(hiddenPdf).not.toContain(customMarker);
+
+ await showSection(page, "Recovery Summary");
+ await showSection(page, "Recovery Experience");
+ await showSection(page, "Recovery Custom");
+ const shownState = await readRecoveryState(resumeId);
+ expect(shownState.metadata.layout).toEqual(authoredLayout);
+ expect(shownState.summary.hidden).toBe(false);
+ expect(shownState.sections.experience.hidden).toBe(false);
+ expect(shownState.customSections.find((section) => section.title === "Recovery Custom")?.hidden).toBe(false);
+
+ const shownPdf = await downloadPdfText(page, testInfo, "section-recovery-shown");
+ expect(shownPdf).toContain(summaryMarker);
+ expect(shownPdf).toContain(builtInMarker);
+ expect(shownPdf).toContain(customMarker);
+
+ await page.getByRole("button", { name: "Undo", exact: true }).click();
+ await expect(page.getByRole("button", { name: "Show Recovery Custom section" })).toBeVisible();
+ await page.getByRole("button", { name: "Redo", exact: true }).click();
+ await expect(page.getByRole("button", { name: "Show Recovery Custom section" })).toHaveCount(0);
+
+ const saved = waitForResumeSave(page);
+ await page.getByRole("button", { name: "Undo", exact: true }).click();
+ await saved;
+ await openResumeCardMenu(page, resumeName);
+ const locked = page.waitForResponse((response) => (response.request().postData() ?? "").includes('"isLocked":true'));
+ await page.getByRole("menuitem", { name: "Lock" }).click();
+ await page.getByRole("alertdialog").getByRole("button", { name: "Confirm" }).click();
+ await locked;
+ await page.goto(`/builder/${resumeId}`);
+
+ await expect(page.getByRole("button", { name: "Show Recovery Custom section" })).toBeDisabled();
+ expect((await readRecoveryState(resumeId)).metadata.layout).toEqual(authoredLayout);
+});