feat: add semantic CSS stylesheets (#3274)

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
Amruth Pillai
2026-07-30 12:39:15 +02:00
committed by GitHub
co-authored by Cursor Agent
parent 4ac19f81b3
commit d2ffbf9618
320 changed files with 78393 additions and 2915 deletions
+173
View File
@@ -1,6 +1,15 @@
import type { E2EAccount } from "./data";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { Pool } from "pg";
const legacyParityRules = JSON.parse(
readFileSync(
resolve(process.cwd(), "packages/pdf/src/semantic/__fixtures__/legacy/custom-section-type.json"),
"utf8",
),
) as unknown[];
function getDatabaseUrl() {
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) throw new Error("DATABASE_URL is required for E2E cleanup.");
@@ -17,3 +26,167 @@ export async function deleteE2EUser(account: E2EAccount) {
await pool.end();
}
}
type SemanticStylesheetSeed = {
mode: "legacy" | "semantic";
source: { languageVersion: number; text: string };
applied: { languageVersion: number; text: string };
};
export async function updateSemanticCssFixture(
resumeId: string,
update: {
stylesheet?: SemanticStylesheetSeed;
bumpRevision?: boolean;
portableLayout?: "balanced" | "pagination-stress";
experienceItemId?: string;
legacyStyleRule?: boolean;
hidePicture?: boolean;
basicsName?: string;
},
) {
const pool = new Pool({ connectionString: getDatabaseUrl() });
try {
const result = await pool.query<{ data: Record<string, unknown> }>('select data from "resume" where id = $1', [
resumeId,
]);
const data = result.rows[0]?.data;
if (!data) throw new Error(`Resume ${resumeId} was not found.`);
if (update.stylesheet) {
const metadata = data.metadata as Record<string, unknown>;
metadata.stylesheet = update.stylesheet;
}
if (update.experienceItemId) {
const sections = data.sections as Record<string, { items: Array<Record<string, unknown>> }>;
const experience = sections.experience;
if (!experience?.items[0]) throw new Error("The semantic CSS fixture requires an experience item.");
const item = experience.items[1] ?? structuredClone(experience.items[0]);
item.id = update.experienceItemId;
if (!experience.items[1]) experience.items.push(item);
}
if (update.portableLayout) {
const sections = data.sections as Record<string, { items: Array<Record<string, unknown>> }>;
const experience = sections.experience;
const education = sections.education;
const projects = sections.projects;
const profiles = sections.profiles;
const skills = sections.skills;
if (
!experience?.items[0] ||
!experience.items[1] ||
!education?.items[0] ||
!projects?.items[0] ||
!profiles?.items[0] ||
!skills?.items[0]
) {
throw new Error("The portable semantic CSS fixture requires standard sample sections.");
}
experience.items = experience.items.slice(0, 2);
experience.items[0].description = "<ul><li><p>Portable rich text marker one.</p></li></ul>";
experience.items[1].description = "<ul><li><p>Portable rich text marker two.</p></li></ul>";
education.items = education.items.slice(0, 1);
education.items[0].description = Array.from(
{ length: update.portableLayout === "pagination-stress" ? 30 : 12 },
(_, index) =>
`<p>Education pagination spacer ${index + 1}: deterministic content places the next section near the page boundary.</p>`,
).join("");
projects.items = projects.items.slice(0, 1);
projects.items[0].description = Array.from(
{ length: 5 },
(_, index) =>
`<p>Project pagination marker ${index + 1}: this section fits on a fresh page and must stay together.</p>`,
).join("");
profiles.items = profiles.items.slice(0, 1);
skills.items = skills.items.slice(0, 1);
const metadata = data.metadata as {
layout: { pages: Array<{ fullWidth: boolean; main: string[]; sidebar: string[] }> };
};
metadata.layout.pages = [
{
fullWidth: false,
main: ["experience"],
sidebar: ["profiles", "skills"],
},
{
fullWidth: true,
main: ["education", "projects"],
sidebar: [],
},
];
}
if (update.legacyStyleRule) {
const metadata = data.metadata as Record<string, unknown>;
metadata.styleRules = structuredClone(legacyParityRules);
}
if (update.hidePicture) {
const picture = data.picture as Record<string, unknown>;
picture.hidden = true;
picture.url = "";
}
if (update.basicsName) {
const basics = data.basics as Record<string, unknown>;
basics.name = update.basicsName;
}
await pool.query(
`update "resume"
set data = $2,
stylesheet_revision = stylesheet_revision + $3,
render_data_version = render_data_version + $4,
updated_at = now()
where id = $1`,
[
resumeId,
data,
update.bumpRevision || update.stylesheet || update.legacyStyleRule ? 1 : 0,
update.stylesheet ||
update.portableLayout ||
update.experienceItemId ||
update.legacyStyleRule ||
update.hidePicture ||
update.basicsName
? 1
: 0,
],
);
} finally {
await pool.end();
}
}
export async function readSemanticCssFixture(resumeId: string) {
const pool = new Pool({ connectionString: getDatabaseUrl() });
try {
const result = await pool.query<{
data: {
basics?: { headline?: string };
metadata?: { stylesheet?: SemanticStylesheetSeed };
};
slug: string;
stylesheet_revision: number;
username: string;
}>(
`select r.data, r.slug, r.stylesheet_revision, u.username
from "resume" r
inner join "user" u on u.id = r.user_id
where r.id = $1`,
[resumeId],
);
const row = result.rows[0];
if (!row) throw new Error(`Resume ${resumeId} was not found.`);
return {
stylesheet: row.data.metadata?.stylesheet,
headline: row.data.basics?.headline,
revision: row.stylesheet_revision,
slug: row.slug,
username: row.username,
};
} finally {
await pool.end();
}
}
+58
View File
@@ -0,0 +1,58 @@
// @vitest-environment happy-dom
import { afterEach, describe, expect, it } from "vitest";
import { ACTIVE_PREVIEW_PAGE_SELECTOR, activePreviewPageSelector, readPreviewPageDataUrl } from "./preview";
describe("preview helpers", () => {
afterEach(() => {
document.body.replaceChildren();
});
it("reads the active preview while an exiting layer remains mounted", () => {
document.body.innerHTML = `
<div aria-hidden="true">
<canvas aria-label="Resume page 1 of 1"></canvas>
</div>
<div aria-hidden="false">
<canvas aria-label="Resume page 1 of 1"></canvas>
</div>
`;
const [exiting, active] = document.querySelectorAll<HTMLCanvasElement>("canvas");
if (!exiting || !active) throw new Error("Expected both preview layers.");
exiting.toDataURL = () => "exiting";
active.toDataURL = () => "active";
expect(readPreviewPageDataUrl(ACTIVE_PREVIEW_PAGE_SELECTOR)).toBe("active");
});
it("accepts a requested active template with the same bitmap as the previous layer", () => {
document.body.innerHTML = `
<div aria-hidden="true" data-resume-preview-template="gengar">
<canvas aria-label="Resume page 1 of 1"></canvas>
</div>
<div aria-hidden="false" data-resume-preview-template="glalie">
<canvas aria-label="Resume page 1 of 1"></canvas>
</div>
`;
for (const canvas of document.querySelectorAll<HTMLCanvasElement>("canvas")) {
canvas.toDataURL = () => "same-bitmap";
}
expect(readPreviewPageDataUrl(activePreviewPageSelector("glalie"))).toBe("same-bitmap");
});
it("does not accept an unrelated active template", () => {
document.body.innerHTML = `
<div aria-hidden="false" data-resume-preview-template="gengar">
<canvas aria-label="Resume page 1 of 1"></canvas>
</div>
<div aria-hidden="true" data-resume-preview-template="glalie">
<canvas aria-label="Resume page 1 of 1"></canvas>
</div>
`;
expect(() => readPreviewPageDataUrl(activePreviewPageSelector("glalie"))).toThrow(
"Expected an active first-page canvas.",
);
});
});
+10
View File
@@ -0,0 +1,10 @@
export const ACTIVE_PREVIEW_PAGE_SELECTOR = '[aria-hidden="false"] canvas[aria-label^="Resume page 1 of"]';
export const activePreviewPageSelector = (template: string) =>
`[aria-hidden="false"][data-resume-preview-template="${template}"] canvas[aria-label^="Resume page 1 of"]`;
export function readPreviewPageDataUrl(selector: string) {
const canvas = document.querySelector<HTMLCanvasElement>(selector);
if (!canvas) throw new Error("Expected an active first-page canvas.");
return canvas.toDataURL();
}
+46
View File
@@ -0,0 +1,46 @@
import type { Page } from "@playwright/test";
import { describe, expect, it, vi } from "vitest";
import { activePreviewPageSelector } from "./preview";
import { switchTemplate } from "./semantic-css";
vi.mock("@playwright/test", () => {
const expect = Object.assign(() => ({ toBeVisible: async () => {} }), {
poll: (read: () => Promise<unknown>) => ({
toBe: async () => {
await read();
await read();
},
}),
});
return { expect };
});
vi.mock("./resume", () => ({
createSampleResumeFromDashboard: vi.fn(),
openSidebarSection: vi.fn(),
}));
describe("switchTemplate", () => {
it("waits for the requested active preview when the template is already selected", async () => {
let selectedPreview = "";
const locator = {
filter: () => locator,
first: () => locator,
};
const section = {
getByRole: () => ({ isVisible: async () => true }),
};
const page = {
evaluate: async () => "same-bitmap",
getByRole: (role: string) => (role === "region" ? section : locator),
locator: (selector: string) => {
selectedPreview = selector;
return locator;
},
} as unknown as Page;
await switchTemplate(page, "Glalie");
expect(selectedPreview).toBe(activePreviewPageSelector("glalie"));
});
});
+191
View File
@@ -0,0 +1,191 @@
import type { Download, Locator, Page, TestInfo } from "@playwright/test";
import { expect } from "@playwright/test";
import { updateSemanticCssFixture } from "./db";
import { ACTIVE_PREVIEW_PAGE_SELECTOR, activePreviewPageSelector, readPreviewPageDataUrl } from "./preview";
import { createSampleResumeFromDashboard, openSidebarSection } from "./resume";
const EMPTY_SEMANTIC_STYLESHEET = {
languageVersion: 1,
text: "@version 1;\n",
} as const;
type SemanticStylesheetSeed = {
mode: "semantic";
source: { languageVersion: number; text: string };
applied: { languageVersion: number; text: string };
};
export const PORTABLE_STYLESHEET = `@version 1;
:root {
--accent: var(--resume-primary-color);
}
header > name {
color: var(--accent);
}
section:is([type="experience"], [type="education"]) > section-heading {
text-transform: uppercase;
}
section[id="projects"] > section-items > item {
padding: 6pt;
}
section[id="experience"] item[id="experience-item-2"] field[name="period"] {
color: var(--accent);
}
rich-text list-item > list-item-content {
line-height: 1.25;
}
region[placement="sidebar"] section {
background-color: rgba(0, 0, 0, 0.04);
}
section[type="projects"] {
break-inside: avoid;
-resume-min-presence-ahead: 24pt;
}
@media (max-width: 600pt) {
region[placement="sidebar"] section-heading {
font-size: 9pt;
}
}
resume[template="azurill"] template-part[name="timeline-dot"] {
background-color: var(--accent);
}
`;
export const resumeIdFromPage = (page: Page) => {
const resumeId = new URL(page.url()).pathname.match(/^\/builder\/([^/]+)/)?.[1];
if (!resumeId) throw new Error(`Expected a builder URL, received ${page.url()}.`);
return resumeId;
};
export async function createSemanticCssResume(page: Page, testInfo: TestInfo) {
await createSampleResumeFromDashboard(page, testInfo);
const resumeId = resumeIdFromPage(page);
await openSemanticCssEditor(page);
return resumeId;
}
export async function seedSemanticCssResume(
page: Page,
resumeId: string,
{
basicsName,
portableLayout,
experienceItemId,
hidePicture = false,
stylesheet = {
mode: "semantic",
source: EMPTY_SEMANTIC_STYLESHEET,
applied: EMPTY_SEMANTIC_STYLESHEET,
},
}: {
basicsName?: string;
portableLayout?: "balanced" | "pagination-stress";
experienceItemId?: string;
hidePicture?: boolean;
stylesheet?: SemanticStylesheetSeed;
} = {},
) {
await updateSemanticCssFixture(resumeId, {
basicsName,
portableLayout,
experienceItemId,
hidePicture,
stylesheet,
});
await page.reload();
await openSemanticCssEditor(page);
await waitForStylesheetStatus(page, "Applied");
}
export async function openSemanticCssEditor(page: Page) {
await openSidebarSection(page, "Custom Styles");
await expect(page.getByRole("textbox", { name: "Semantic CSS stylesheet" })).toBeVisible();
}
export async function replaceStylesheet(page: Page, source: string) {
const editor = page.getByRole("textbox", { name: "Semantic CSS stylesheet" });
await editor.fill(source);
await expect.poll(() => readStylesheetSource(page)).toBe(source);
}
export function readStylesheetSource(page: Page) {
const editor = page.getByRole("textbox", { name: "Semantic CSS stylesheet" });
return editor.evaluate((element) =>
Array.from(element.querySelectorAll(".cm-line"), (line) => line.textContent ?? "").join("\n"),
);
}
export async function waitForStylesheetStatus(page: Page, status: string) {
await expect(page.getByText(status, { exact: true }).filter({ visible: true }).last()).toBeVisible({
timeout: 30_000,
});
}
export async function activateStylesheet(page: Page) {
const button = page.getByRole("button", { name: "Activate Semantic CSS" });
await expect(button).toBeEnabled({ timeout: 30_000 });
await button.click();
await waitForStylesheetStatus(page, "Applied");
}
async function firstPreviewPage(page: Page, selector = ACTIVE_PREVIEW_PAGE_SELECTOR): Promise<Locator> {
const canvas = page.locator(selector).filter({ visible: true }).first();
await expect(canvas).toBeVisible({ timeout: 30_000 });
return canvas;
}
export async function waitForStablePreview(page: Page, selector = ACTIVE_PREVIEW_PAGE_SELECTOR): Promise<Locator> {
const canvas = await firstPreviewPage(page, selector);
let previous: string | undefined;
let stableSamples = 0;
await expect
.poll(
async () => {
const current = await page.evaluate(readPreviewPageDataUrl, selector);
stableSamples = previous === current ? stableSamples + 1 : 0;
previous = current;
return stableSamples >= 1;
},
{ timeout: 15_000, intervals: [250] },
)
.toBe(true);
return canvas;
}
export async function switchTemplate(page: Page, template: string) {
await openSidebarSection(page, "Template");
const section = page.getByRole("region", { name: "Toggle Template section" });
const targetPreview = activePreviewPageSelector(template.toLowerCase());
if (!(await section.getByRole("heading", { name: template, exact: true }).isVisible())) {
await section.getByRole("button").first().click();
const gallery = page.getByRole("dialog", { name: "Template Gallery" });
await expect(gallery).toBeVisible();
const save = page.waitForResponse((response) => {
const body = response.request().postData() ?? "";
return response.url().includes("/api/rpc") && response.ok() && body.includes(template.toLowerCase());
});
await gallery.getByRole("img", { name: template, exact: true }).click();
await save;
await page.keyboard.press("Escape");
}
await waitForStablePreview(page, targetPreview);
}
export async function downloadPdf(page: Page): Promise<Download> {
await page.getByRole("button", { name: "Download options" }).click();
const dialog = page.getByRole("dialog", { name: "Download" });
await expect(dialog).toBeVisible();
const download = page.waitForEvent("download");
await dialog.getByRole("button", { name: "Download PDF" }).click();
return download;
}