mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-08-25 15:52:21 +10:00
refactor(stylesheet): move Semantic CSS to the browser (#3329)
This commit is contained in:
@@ -2,7 +2,6 @@
|
||||
|
||||
import { render, waitFor } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createPublicStyleProjection } from "@reactive-resume/pdf/public-projection";
|
||||
import { sampleResumeData } from "@reactive-resume/schema/resume/sample";
|
||||
|
||||
const pdfViewerMock = vi.hoisted(() => {
|
||||
@@ -120,67 +119,25 @@ describe("PdfViewer", () => {
|
||||
expect(pdfViewerMock.loadingTask.destroy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("renders a valid public projection through the shared PDF entrypoint", async () => {
|
||||
it("renders exposed semantic source through the shared PDF entrypoint", async () => {
|
||||
const semanticData = structuredClone(sampleResumeData);
|
||||
const applied = { languageVersion: 1, text: "@version 1;\nname { color: #123456; }\n" };
|
||||
semanticData.metadata.stylesheet = { mode: "semantic", source: applied, applied };
|
||||
const projection = await createPublicStyleProjection({ data: semanticData });
|
||||
const refetchStyleProjection = vi.fn();
|
||||
semanticData.metadata.stylesheet = {
|
||||
mode: "semantic",
|
||||
source: { languageVersion: 1, text: "@version 1;\nname { color: #123456; }\n" },
|
||||
};
|
||||
|
||||
render(
|
||||
<PdfViewer
|
||||
data={sampleResumeData}
|
||||
stylesheetMode="semantic"
|
||||
styleProjection={projection}
|
||||
refetchStyleProjection={refetchStyleProjection}
|
||||
publicResume={{ username: "amruth", slug: "sample" }}
|
||||
/>,
|
||||
);
|
||||
render(<PdfViewer data={semanticData} publicResume={{ username: "amruth", slug: "sample" }} />);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(pdfViewerMock.createResumePdfBlob).toHaveBeenCalledWith(sampleResumeData, undefined, undefined, {
|
||||
publicStyleProjection: projection,
|
||||
}),
|
||||
);
|
||||
expect(refetchStyleProjection).not.toHaveBeenCalled();
|
||||
await waitFor(() => expect(pdfViewerMock.createResumePdfBlob).toHaveBeenCalledWith(semanticData));
|
||||
expect(pdfViewerMock.fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("refetches a mismatched projection once before using the authorized PDF fallback", async () => {
|
||||
const semanticData = structuredClone(sampleResumeData);
|
||||
const applied = { languageVersion: 1, text: "@version 1;\nname { color: #123456; }\n" };
|
||||
semanticData.metadata.stylesheet = { mode: "semantic", source: applied, applied };
|
||||
const projection = await createPublicStyleProjection({ data: semanticData });
|
||||
const mismatchedProjection = { ...projection, renderDataHash: "0".repeat(64) };
|
||||
const refetchStyleProjection = vi.fn(async () => mismatchedProjection);
|
||||
it("uses the authorized PDF fallback after browser generation rejects", async () => {
|
||||
pdfViewerMock.createResumePdfBlob.mockRejectedValueOnce(new Error("browser renderer failed"));
|
||||
|
||||
const view = render(
|
||||
<PdfViewer
|
||||
data={sampleResumeData}
|
||||
stylesheetMode="semantic"
|
||||
styleProjection={mismatchedProjection}
|
||||
refetchStyleProjection={refetchStyleProjection}
|
||||
publicResume={{ username: "amruth", slug: "sample" }}
|
||||
/>,
|
||||
);
|
||||
render(<PdfViewer data={sampleResumeData} publicResume={{ username: "amruth", slug: "sample" }} />);
|
||||
|
||||
await waitFor(() => expect(refetchStyleProjection).toHaveBeenCalledTimes(1));
|
||||
await waitFor(() => expect(pdfViewerMock.fetch).toHaveBeenCalledTimes(1));
|
||||
expect(String(pdfViewerMock.fetch.mock.calls[0]?.[0])).toContain("/api/resumes/amruth/sample/pdf");
|
||||
expect(String(pdfViewerMock.fetch.mock.calls[0]?.[0])).toContain("reason=render-data-hash");
|
||||
expect(pdfViewerMock.createResumePdfBlob).not.toHaveBeenCalled();
|
||||
|
||||
view.rerender(
|
||||
<PdfViewer
|
||||
data={sampleResumeData}
|
||||
stylesheetMode="semantic"
|
||||
styleProjection={{ ...mismatchedProjection, renderDataHash: "1".repeat(64) }}
|
||||
refetchStyleProjection={refetchStyleProjection}
|
||||
publicResume={{ username: "amruth", slug: "sample" }}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(pdfViewerMock.fetch).toHaveBeenCalledTimes(2));
|
||||
expect(refetchStyleProjection).toHaveBeenCalledTimes(1);
|
||||
expect(pdfViewerMock.fetch).toHaveBeenCalledWith("/api/resumes/amruth/sample/pdf", { credentials: "include" });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import type { PublicStyleProjection } from "@reactive-resume/pdf/public-projection";
|
||||
import type { ResumeData } from "@reactive-resume/schema/resume/data";
|
||||
import type { SemanticStylesheet } from "@reactive-resume/schema/resume/stylesheet";
|
||||
import type { PDFDocumentLoadingTask, PDFDocumentProxy } from "pdfjs-dist/legacy/build/pdf.mjs";
|
||||
import { AnnotationMode, GlobalWorkerOptions, getDocument } from "pdfjs-dist/legacy/build/pdf.mjs";
|
||||
import { EventBus, LinkTarget, PDFLinkService, PDFViewer } from "pdfjs-dist/legacy/web/pdf_viewer.mjs";
|
||||
@@ -17,9 +15,6 @@ GlobalWorkerOptions.workerSrc = new URL("pdfjs-dist/legacy/build/pdf.worker.min.
|
||||
type PdfViewerProps = {
|
||||
className?: string;
|
||||
data: ResumeData;
|
||||
stylesheetMode?: SemanticStylesheet["mode"];
|
||||
styleProjection?: PublicStyleProjection;
|
||||
refetchStyleProjection?: () => Promise<PublicStyleProjection | undefined>;
|
||||
publicResume?: {
|
||||
username: string;
|
||||
slug: string;
|
||||
@@ -77,21 +72,11 @@ function pdfViewerReducer(state: PdfViewerState, action: PdfViewerAction): PdfVi
|
||||
}
|
||||
}
|
||||
|
||||
export function PdfViewer({
|
||||
className,
|
||||
data,
|
||||
stylesheetMode,
|
||||
styleProjection,
|
||||
refetchStyleProjection,
|
||||
publicResume,
|
||||
}: PdfViewerProps) {
|
||||
export function PdfViewer({ className, data, publicResume }: PdfViewerProps) {
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const viewerRef = useRef<HTMLDivElement>(null);
|
||||
const fileRef = useRef<Blob | null>(null);
|
||||
const projectionRetryRef = useRef<{ data?: ResumeData; publicKey?: string; retried: boolean }>({
|
||||
retried: false,
|
||||
});
|
||||
const [{ error, fileVersion, isReady, viewerHeight }, dispatch] = useReducer(
|
||||
pdfViewerReducer,
|
||||
INITIAL_PDF_VIEWER_STATE,
|
||||
@@ -103,27 +88,8 @@ export function PdfViewer({
|
||||
fileRef.current = null;
|
||||
dispatch({ type: "resetForData" });
|
||||
|
||||
const createPdf = () => {
|
||||
if (!stylesheetMode || !publicResume) return createResumePdfBlob(data);
|
||||
const publicKey = `${publicResume.username}/${publicResume.slug}`;
|
||||
if (projectionRetryRef.current.data !== data || projectionRetryRef.current.publicKey !== publicKey) {
|
||||
projectionRetryRef.current = { data, publicKey, retried: false };
|
||||
}
|
||||
const retryProjection =
|
||||
refetchStyleProjection && !projectionRetryRef.current.retried
|
||||
? () => {
|
||||
projectionRetryRef.current.retried = true;
|
||||
return refetchStyleProjection();
|
||||
}
|
||||
: undefined;
|
||||
return resolvePublicResumePdfBlob({
|
||||
data,
|
||||
stylesheetMode,
|
||||
publicResume,
|
||||
...(styleProjection ? { styleProjection } : {}),
|
||||
...(retryProjection ? { refetchStyleProjection: retryProjection } : {}),
|
||||
});
|
||||
};
|
||||
const createPdf = () =>
|
||||
publicResume ? resolvePublicResumePdfBlob({ data, publicResume }) : createResumePdfBlob(data);
|
||||
|
||||
void createPdf()
|
||||
.then((blob) => {
|
||||
@@ -142,7 +108,7 @@ export function PdfViewer({
|
||||
return () => {
|
||||
isCancelled = true;
|
||||
};
|
||||
}, [data, publicResume, refetchStyleProjection, styleProjection, stylesheetMode]);
|
||||
}, [data, publicResume]);
|
||||
|
||||
useEffect(() => {
|
||||
void fileVersion;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createPublicStyleProjection } from "@reactive-resume/pdf/public-projection";
|
||||
import { sampleResumeData } from "@reactive-resume/schema/resume/sample";
|
||||
import { resolvePublicResumePdfBlob } from "./public-pdf";
|
||||
|
||||
@@ -15,58 +14,35 @@ vi.mock("@/features/resume/export/pdf-document", () => ({
|
||||
const publicResume = { username: "amruth", slug: "sample" };
|
||||
|
||||
beforeEach(() => {
|
||||
mocks.createResumePdfBlob.mockClear();
|
||||
mocks.fetch.mockClear();
|
||||
mocks.createResumePdfBlob.mockReset();
|
||||
mocks.createResumePdfBlob.mockResolvedValue(new Blob(["local"], { type: "application/pdf" }));
|
||||
mocks.fetch.mockReset();
|
||||
mocks.fetch.mockResolvedValue(new Response(new Blob(["server"], { type: "application/pdf" })));
|
||||
vi.stubGlobal("fetch", mocks.fetch);
|
||||
});
|
||||
|
||||
describe("resolvePublicResumePdfBlob", () => {
|
||||
it("keeps legitimate legacy resumes on the local PDF path", async () => {
|
||||
await resolvePublicResumePdfBlob({
|
||||
data: sampleResumeData,
|
||||
stylesheetMode: "legacy",
|
||||
publicResume,
|
||||
});
|
||||
it("renders the exposed stylesheet source directly in the browser", async () => {
|
||||
const data = structuredClone(sampleResumeData);
|
||||
data.metadata.stylesheet = {
|
||||
mode: "semantic",
|
||||
source: { languageVersion: 1, text: "@version 1;\nname { color: #123456; }\n" },
|
||||
};
|
||||
|
||||
const blob = await resolvePublicResumePdfBlob({ data, publicResume });
|
||||
|
||||
expect(mocks.createResumePdfBlob).toHaveBeenCalledWith(data);
|
||||
expect(mocks.fetch).not.toHaveBeenCalled();
|
||||
expect(await blob.text()).toBe("local");
|
||||
});
|
||||
|
||||
it("fetches the server PDF only after browser rendering rejects", async () => {
|
||||
mocks.createResumePdfBlob.mockRejectedValue(new Error("browser renderer failed"));
|
||||
|
||||
const blob = await resolvePublicResumePdfBlob({ data: sampleResumeData, publicResume });
|
||||
|
||||
expect(mocks.createResumePdfBlob).toHaveBeenCalledWith(sampleResumeData);
|
||||
expect(mocks.fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses the authorized server fallback when a semantic projection is unavailable", async () => {
|
||||
const refetchStyleProjection = vi.fn().mockRejectedValue(new Error("projection unavailable"));
|
||||
|
||||
await resolvePublicResumePdfBlob({
|
||||
data: sampleResumeData,
|
||||
stylesheetMode: "semantic",
|
||||
publicResume,
|
||||
refetchStyleProjection,
|
||||
});
|
||||
|
||||
expect(refetchStyleProjection).toHaveBeenCalledTimes(1);
|
||||
expect(String(mocks.fetch.mock.calls[0]?.[0])).toContain("/api/resumes/amruth/sample/pdf");
|
||||
expect(String(mocks.fetch.mock.calls[0]?.[0])).toContain("reason=missing-projection");
|
||||
expect(mocks.createResumePdfBlob).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("refetches a mismatched projection once before returning the authorized server blob", async () => {
|
||||
const semanticData = structuredClone(sampleResumeData);
|
||||
const source = { languageVersion: 1, text: "@version 1;\nname { color: #123456; }\n" };
|
||||
semanticData.metadata.stylesheet = { mode: "semantic", source, applied: source };
|
||||
const projection = await createPublicStyleProjection({ data: semanticData });
|
||||
const mismatchedProjection = { ...projection, renderDataHash: "0".repeat(64) };
|
||||
const refetchStyleProjection = vi.fn(async () => mismatchedProjection);
|
||||
|
||||
const blob = await resolvePublicResumePdfBlob({
|
||||
data: sampleResumeData,
|
||||
stylesheetMode: "semantic",
|
||||
styleProjection: mismatchedProjection,
|
||||
publicResume,
|
||||
refetchStyleProjection,
|
||||
});
|
||||
|
||||
expect(refetchStyleProjection).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.fetch).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.fetch).toHaveBeenCalledWith("/api/resumes/amruth/sample/pdf", { credentials: "include" });
|
||||
expect(await blob.text()).toBe("server");
|
||||
expect(mocks.createResumePdfBlob).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,97 +1,28 @@
|
||||
import type { PublicStyleProjection } from "@reactive-resume/pdf/public-projection";
|
||||
import type { ResumeData } from "@reactive-resume/schema/resume/data";
|
||||
import type { SemanticStylesheet } from "@reactive-resume/schema/resume/stylesheet";
|
||||
import {
|
||||
getPublicStyleProjectionFingerprints,
|
||||
PUBLIC_STYLE_PROJECTION_FORMAT_VERSION,
|
||||
SEMANTIC_TREE_VERSION,
|
||||
validatePublicStyleProjection,
|
||||
} from "@reactive-resume/pdf/public-projection";
|
||||
import { createResumePdfBlob } from "@/features/resume/export/pdf-document";
|
||||
|
||||
export type PublicResumePdfOptions = {
|
||||
stylesheetMode: SemanticStylesheet["mode"];
|
||||
styleProjection?: PublicStyleProjection;
|
||||
refetchStyleProjection?: () => Promise<PublicStyleProjection | undefined>;
|
||||
publicResume: {
|
||||
username: string;
|
||||
slug: string;
|
||||
};
|
||||
};
|
||||
|
||||
type ProjectionMismatchReason =
|
||||
| "format-version"
|
||||
| "language-version"
|
||||
| "semantic-tree-version"
|
||||
| "registry-fingerprint"
|
||||
| "adapter-fingerprint"
|
||||
| "render-data-hash"
|
||||
| "invalid-projection";
|
||||
|
||||
const projectionMismatchReason = async (
|
||||
data: ResumeData,
|
||||
projection: PublicStyleProjection,
|
||||
): Promise<ProjectionMismatchReason | null> => {
|
||||
if (projection.formatVersion !== PUBLIC_STYLE_PROJECTION_FORMAT_VERSION) return "format-version";
|
||||
if (projection.languageVersion !== 1) return "language-version";
|
||||
if (projection.semanticTreeVersion !== SEMANTIC_TREE_VERSION) return "semantic-tree-version";
|
||||
const fingerprints = await getPublicStyleProjectionFingerprints();
|
||||
if (projection.registryFingerprint !== fingerprints.registryFingerprint) return "registry-fingerprint";
|
||||
if (projection.adapterFingerprint !== fingerprints.adapterFingerprint) return "adapter-fingerprint";
|
||||
return (await validatePublicStyleProjection(data, projection)) ? null : "render-data-hash";
|
||||
};
|
||||
|
||||
const fetchPublicResumePdf = async (
|
||||
publicResume: PublicResumePdfOptions["publicResume"],
|
||||
reason: ProjectionMismatchReason | "missing-projection",
|
||||
projection?: PublicStyleProjection,
|
||||
) => {
|
||||
const search = new URLSearchParams({
|
||||
reason,
|
||||
...(projection
|
||||
? {
|
||||
registryFingerprint: projection.registryFingerprint,
|
||||
adapterFingerprint: projection.adapterFingerprint,
|
||||
}
|
||||
: {}),
|
||||
const fetchPublicResumePdf = async ({ username, slug }: PublicResumePdfOptions["publicResume"]) => {
|
||||
const response = await fetch(`/api/resumes/${encodeURIComponent(username)}/${encodeURIComponent(slug)}/pdf`, {
|
||||
credentials: "include",
|
||||
});
|
||||
const response = await fetch(
|
||||
`/api/resumes/${encodeURIComponent(publicResume.username)}/${encodeURIComponent(publicResume.slug)}/pdf?${search}`,
|
||||
{ credentials: "include" },
|
||||
);
|
||||
if (!response.ok) throw new Error(`Public PDF fallback failed with ${response.status}`);
|
||||
return response.blob();
|
||||
};
|
||||
|
||||
export async function resolvePublicResumePdfBlob({
|
||||
data,
|
||||
...options
|
||||
publicResume,
|
||||
}: PublicResumePdfOptions & { data: ResumeData }): Promise<Blob> {
|
||||
if (options.stylesheetMode === "legacy") return createResumePdfBlob(data);
|
||||
|
||||
let projection = options.styleProjection;
|
||||
let refetched = false;
|
||||
const refetch = async () => {
|
||||
if (!options.refetchStyleProjection || refetched) return;
|
||||
refetched = true;
|
||||
try {
|
||||
projection = await options.refetchStyleProjection();
|
||||
} catch {
|
||||
projection = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
if (!projection) await refetch();
|
||||
if (!projection) return fetchPublicResumePdf(options.publicResume, "missing-projection");
|
||||
|
||||
let reason = await projectionMismatchReason(data, projection).catch(() => "invalid-projection" as const);
|
||||
if (reason) {
|
||||
await refetch();
|
||||
if (!projection) return fetchPublicResumePdf(options.publicResume, "missing-projection");
|
||||
reason = await projectionMismatchReason(data, projection).catch(() => "invalid-projection" as const);
|
||||
try {
|
||||
return await createResumePdfBlob(data);
|
||||
} catch {
|
||||
return fetchPublicResumePdf(publicResume);
|
||||
}
|
||||
|
||||
return reason
|
||||
? fetchPublicResumePdf(options.publicResume, reason, projection)
|
||||
: createResumePdfBlob(data, undefined, undefined, { publicStyleProjection: projection });
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import type { PublicStyleProjection } from "@reactive-resume/pdf/public-projection";
|
||||
import type { ResumeData } from "@reactive-resume/schema/resume/data";
|
||||
import type { ReactNode } from "react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
@@ -12,30 +11,12 @@ import { sampleResumeData } from "@reactive-resume/schema/resume/sample";
|
||||
type PdfViewerProps = {
|
||||
className?: string;
|
||||
data: ResumeData;
|
||||
stylesheetMode?: "legacy" | "semantic";
|
||||
styleProjection?: PublicStyleProjection;
|
||||
refetchStyleProjection?: () => Promise<PublicStyleProjection | undefined>;
|
||||
publicResume?: { username: string; slug: string };
|
||||
};
|
||||
|
||||
const publicResumeMock = vi.hoisted(() => ({
|
||||
onDownloadPDF: vi.fn(),
|
||||
PdfViewer: vi.fn<(_props: PdfViewerProps) => ReactNode>(() => null),
|
||||
projection: {
|
||||
formatVersion: 1,
|
||||
languageVersion: 1,
|
||||
semanticTreeVersion: 1,
|
||||
registryFingerprint: "1".repeat(64),
|
||||
adapterFingerprint: "2".repeat(64),
|
||||
renderDataHash: "3".repeat(64),
|
||||
nodes: { resume: {} },
|
||||
} as PublicStyleProjection,
|
||||
refetchProjection: vi.fn(),
|
||||
projectionResult: {
|
||||
data: undefined as PublicStyleProjection | undefined,
|
||||
isError: false,
|
||||
isPending: false,
|
||||
},
|
||||
useResumeExport: vi.fn(),
|
||||
resume: undefined as
|
||||
| undefined
|
||||
@@ -43,61 +24,28 @@ const publicResumeMock = vi.hoisted(() => ({
|
||||
data: ResumeData;
|
||||
name: string;
|
||||
slug: string;
|
||||
stylesheetMode: "legacy" | "semantic";
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@tanstack/react-query", () => ({
|
||||
useQuery: (options: { query: "resume" | "projection" }) =>
|
||||
options.query === "resume"
|
||||
? { data: publicResumeMock.resume }
|
||||
: { ...publicResumeMock.projectionResult, refetch: publicResumeMock.refetchProjection },
|
||||
}));
|
||||
|
||||
vi.mock("@tanstack/react-query", () => ({ useQuery: () => ({ data: publicResumeMock.resume }) }));
|
||||
vi.mock("@tanstack/react-router", () => ({
|
||||
getRouteApi: () => ({
|
||||
useParams: () => ({ username: "amruth", slug: "sample" }),
|
||||
}),
|
||||
getRouteApi: () => ({ useParams: () => ({ username: "amruth", slug: "sample" }) }),
|
||||
}));
|
||||
|
||||
vi.mock("./pdf-viewer", () => ({
|
||||
PdfViewer: publicResumeMock.PdfViewer,
|
||||
}));
|
||||
|
||||
vi.mock("./pdf-viewer", () => ({ PdfViewer: publicResumeMock.PdfViewer }));
|
||||
vi.mock("@/libs/orpc/client", () => ({
|
||||
orpc: {
|
||||
resume: {
|
||||
getBySlug: { queryOptions: () => ({ query: "resume" }) },
|
||||
getStyleProjection: { queryOptions: () => ({ query: "projection" }) },
|
||||
},
|
||||
},
|
||||
orpc: { resume: { getBySlug: { queryOptions: () => ({ query: "resume" }) } } },
|
||||
}));
|
||||
|
||||
vi.mock("@/features/resume/export/use-resume-export", () => ({
|
||||
useResumeExport: publicResumeMock.useResumeExport,
|
||||
}));
|
||||
|
||||
const { PublicResumeRoute } = await import("./public-resume");
|
||||
|
||||
beforeAll(() => {
|
||||
i18n.loadAndActivate({ locale: "en", messages: {} });
|
||||
});
|
||||
beforeAll(() => i18n.loadAndActivate({ locale: "en", messages: {} }));
|
||||
|
||||
beforeEach(() => {
|
||||
publicResumeMock.resume = {
|
||||
data: sampleResumeData,
|
||||
name: "Sample Resume",
|
||||
slug: "sample",
|
||||
stylesheetMode: "semantic",
|
||||
};
|
||||
publicResumeMock.projectionResult = {
|
||||
data: publicResumeMock.projection,
|
||||
isError: false,
|
||||
isPending: false,
|
||||
};
|
||||
publicResumeMock.resume = { data: sampleResumeData, name: "Sample Resume", slug: "sample" };
|
||||
publicResumeMock.PdfViewer.mockClear();
|
||||
publicResumeMock.refetchProjection.mockReset();
|
||||
publicResumeMock.refetchProjection.mockResolvedValue({ data: publicResumeMock.projection });
|
||||
publicResumeMock.useResumeExport.mockReset();
|
||||
publicResumeMock.useResumeExport.mockReturnValue({
|
||||
onDownloadPDF: publicResumeMock.onDownloadPDF,
|
||||
@@ -116,78 +64,26 @@ const renderPublicResumeRoute = () =>
|
||||
);
|
||||
|
||||
describe("PublicResumeRoute", () => {
|
||||
it("renders the public resume through the route-local PDF.js viewer", () => {
|
||||
it("passes exposed source data directly to the browser viewer and export fallback", () => {
|
||||
renderPublicResumeRoute();
|
||||
|
||||
expect(screen.getByTestId("pdf-viewer")).toHaveClass("block", "w-full");
|
||||
expect(publicResumeMock.PdfViewer).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ data: sampleResumeData }),
|
||||
expect.objectContaining({
|
||||
data: sampleResumeData,
|
||||
publicResume: { username: "amruth", slug: "sample" },
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
expect(publicResumeMock.useResumeExport).toHaveBeenCalledWith(publicResumeMock.resume, {
|
||||
publicResumePdf: expect.objectContaining({
|
||||
stylesheetMode: "semantic",
|
||||
styleProjection: publicResumeMock.projection,
|
||||
publicResume: { username: "amruth", slug: "sample" },
|
||||
}),
|
||||
publicResumePdf: { publicResume: { username: "amruth", slug: "sample" } },
|
||||
});
|
||||
});
|
||||
|
||||
it("routes a missing semantic projection to the shared fallback seam", () => {
|
||||
publicResumeMock.projectionResult = { data: undefined, isError: true, isPending: false };
|
||||
|
||||
renderPublicResumeRoute();
|
||||
|
||||
expect(publicResumeMock.PdfViewer).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
stylesheetMode: "semantic",
|
||||
styleProjection: undefined,
|
||||
refetchStyleProjection: expect.any(Function),
|
||||
publicResume: { username: "amruth", slug: "sample" },
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
expect(publicResumeMock.useResumeExport).toHaveBeenCalledWith(
|
||||
publicResumeMock.resume,
|
||||
expect.objectContaining({
|
||||
publicResumePdf: expect.objectContaining({
|
||||
stylesheetMode: "semantic",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps missing legacy projections on the local rendering path", () => {
|
||||
if (publicResumeMock.resume) publicResumeMock.resume.stylesheetMode = "legacy";
|
||||
publicResumeMock.projectionResult = { data: undefined, isError: false, isPending: false };
|
||||
|
||||
renderPublicResumeRoute();
|
||||
|
||||
expect(publicResumeMock.PdfViewer).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ stylesheetMode: "legacy", styleProjection: undefined }),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it("loads the public projection and passes it to the shared viewer", () => {
|
||||
renderPublicResumeRoute();
|
||||
|
||||
expect(publicResumeMock.PdfViewer).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
styleProjection: publicResumeMock.projection,
|
||||
refetchStyleProjection: expect.any(Function),
|
||||
publicResume: { username: "amruth", slug: "sample" },
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it("lets the public resume page grow to the full PDF length", () => {
|
||||
renderPublicResumeRoute();
|
||||
|
||||
const viewerFrame = screen.getByTestId("pdf-viewer").parentElement;
|
||||
const page = viewerFrame?.parentElement;
|
||||
|
||||
expect(page).not.toHaveClass("min-h-svh", "h-svh", "max-h-svh", "overflow-hidden");
|
||||
expect(viewerFrame).not.toHaveClass("min-h-0", "flex-1", "overflow-hidden");
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Trans } from "@lingui/react/macro";
|
||||
import { CircleNotchIcon, DownloadSimpleIcon } from "@phosphor-icons/react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getRouteApi } from "@tanstack/react-router";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { BrandIcon } from "@reactive-resume/ui/components/brand-icon";
|
||||
import { Button } from "@reactive-resume/ui/components/button";
|
||||
import { LoadingScreen } from "@/components/layout/loading-screen";
|
||||
@@ -17,30 +17,12 @@ export function PublicResumeRoute() {
|
||||
const { username, slug } = publicResumeRoute.useParams();
|
||||
|
||||
const { data: resume } = useQuery(orpc.resume.getBySlug.queryOptions({ input: { username, slug } }));
|
||||
const projectionQuery = useQuery(
|
||||
orpc.resume.getStyleProjection.queryOptions({ input: { username, slug }, enabled: resume !== undefined }),
|
||||
);
|
||||
const styleProjection =
|
||||
projectionQuery.data && Object.keys(projectionQuery.data.nodes).length > 0 ? projectionQuery.data : undefined;
|
||||
const publicResume = useMemo(() => ({ username, slug }), [slug, username]);
|
||||
const refetchStyleProjection = useCallback(async () => {
|
||||
const result = await projectionQuery.refetch();
|
||||
return result.data;
|
||||
}, [projectionQuery.refetch]);
|
||||
const { onDownloadPDF, isExporting } = useResumeExport(resume, {
|
||||
...(resume
|
||||
? {
|
||||
publicResumePdf: {
|
||||
stylesheetMode: resume.stylesheetMode,
|
||||
publicResume,
|
||||
refetchStyleProjection,
|
||||
...(styleProjection ? { styleProjection } : {}),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(resume ? { publicResumePdf: { publicResume } } : {}),
|
||||
});
|
||||
|
||||
if (!resume || projectionQuery.isPending) return <LoadingScreen />;
|
||||
if (!resume) return <LoadingScreen />;
|
||||
|
||||
const { basics, picture } = resume.data;
|
||||
|
||||
@@ -66,14 +48,7 @@ export function PublicResumeRoute() {
|
||||
</header>
|
||||
|
||||
<main className="w-full max-w-5xl bg-white print:max-w-full">
|
||||
<PdfViewer
|
||||
data={resume.data}
|
||||
className="block w-full"
|
||||
stylesheetMode={resume.stylesheetMode}
|
||||
styleProjection={styleProjection}
|
||||
publicResume={publicResume}
|
||||
refetchStyleProjection={refetchStyleProjection}
|
||||
/>
|
||||
<PdfViewer data={resume.data} className="block w-full" publicResume={publicResume} />
|
||||
</main>
|
||||
|
||||
<footer className="flex justify-center print:hidden">
|
||||
|
||||
Reference in New Issue
Block a user