Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -309,3 +309,188 @@ describe("the same section rendered for certifications (#884)", () => {
expect(container.textContent).toContain("Add certification");
});
});

/**
* #899 — the certification-only bugs on top of the shared `AchievementsSection`
* component #884 introduced: the achievements-only `AchievementTypePicker`
* leaking onto credential rows, a dangling `·` in front of an empty year, and
* multiple credentials each taking a full vertical row instead of compressing
* onto one wrapped, middot-separated line.
*
* The compact line is where a row loses its own vertical space, so what it may
* and may not take with it is the rest of this block: the year form has to be
* the one the PDF draws (`compactCredentialHeader`), the `·` may mean only one
* thing at a time, and no row may be routed onto the line if the affordance it
* owns lives off it — which is what stranded an added credential with no way to
* ever get a bullet.
*/
describe("compact certifications layout (#899)", () => {
function renderCertsWith(
certs: readonly HeuristicAchievement[],
added: readonly AddedEntry[] = [],
): void {
const all: HeuristicAchievement[] = [
...certs,
...added.map((a) => ({ title: a.title })),
];
act(() =>
root.render(
createElement(AchievementsSection, {
section: "certifications",
fallbackHeading: "Certifications",
entryNoun: "certification",
achievements: all,
groups: all.map((c, i) => ({
experienceIndex: i,
experience: { title: c.title },
bullets: [],
})),
addedAchievements: [...added],
originalCount: certs.length,
parsedIndices: survivingParsedIndices(
"certifications",
new Set(),
certs.length,
),
onAddEntry: () => {},
onEntryField: () => {},
onAddBullet: () => {},
onPruneEmpty: () => {},
onRemoveEntry: () => {},
onRemoveBullet: vi.fn(() => true),
onAchievementField: vi.fn(),
}),
),
);
}

/** Every rendered `·` glyph — the achievement type↔title separator, the
* title↔year separator, and the between-item compact-row separator all
* draw through the same `aria-hidden` span shape. */
function middotSpans(): HTMLElement[] {
return [
...container.querySelectorAll<HTMLElement>('span[aria-hidden="true"]'),
].filter((el) => el.textContent === "·");
}

it("never renders the achievement type picker for a certification row", () => {
renderCertsWith([
{ title: "AWS Certified Solutions Architect", year: "2022" },
]);
expect(container.querySelector('[aria-haspopup="menu"]')).toBeNull();
});

it("omits the dangling separator when a certification has no year", () => {
renderCertsWith([{ title: "Patent Bar Registration" }]);
// A single, dateless certification: no type-picker separator (gated off),
// no title↔year separator (no year to set off), no between-item separator
// (only one item) — zero middots anywhere in the row.
expect(middotSpans()).toHaveLength(0);
});

it("joins two or more certifications with exactly one between-item middot", () => {
renderCertsWith([
{ title: "AWS Certified Solutions Architect" },
{ title: "CKA" },
]);
// Neither carries a year, so the only middot either could draw is the
// compact row's between-item separator — one, joining the pair, none
// trailing the last item.
expect(middotSpans()).toHaveLength(1);
});

it("parenthesises the year, so one glyph never means two things", () => {
renderCertsWith([
{ title: "AWS Certified Solutions Architect", year: "2022" },
{ title: "CKA" },
]);
// The compact line reuses `·` as its item boundary, so a year set off by
// the same glyph would be unreadable ("Solutions Architect·2022·CKA"). The
// exporter already parenthesises for exactly that reason
// (`compactCredentialHeader`); the view has to draw the same line the PDF
// will. One middot survives — the boundary between the two credentials.
expect(container.textContent).toContain("(2022)");
expect(middotSpans()).toHaveLength(1);
});

it("leaves a LONE credential's year on its ordinary separator", () => {
// Nothing to be ambiguous with: a single credential draws no boundary
// middot, and the exporter does not compact one either (its guard starts at
// two), so parenthesising here would invent a divergence from the PDF
// rather than remove one. The "+ year" affordance survives for the same
// reason — the line it would clutter does not exist.
renderCertsWith([{ title: "CKA", year: "2021" }]);
expect(container.textContent).not.toContain("(2021)");
expect(middotSpans()).toHaveLength(1);

renderCertsWith([{ title: "CKA" }]);
expect(container.querySelector('[aria-label^="Add Year"]')).not.toBeNull();
});

it("re-emits a NON-middot source separator verbatim, as the PDF does", () => {
// The parenthesised form replaces only the ambiguous glyph. A résumé that
// wrote its own comma keeps it, matching `compactCredentialHeader`'s same
// carve-out, so #380's punctuation fidelity is not traded away wholesale.
renderCertsWith([
{ title: "CKA", year: "2021", year_separator: "," },
{ title: "Terraform Associate" },
]);
expect(container.textContent).not.toContain("(2021)");
expect(container.textContent).toContain(",");
});

it("hides the '+ year' affordance on a dateless compact credential AT REST, not from the DOM", () => {
// One "+ year" permanently on the shared line is the clutter the compact
// form exists to remove and it competes with the boundary glyph — so it's
// `opacity-0` until the row is hovered/focused, not omitted outright. A
// parsed dateless credential still stays dateable from this line (#899
// AC 5) — see AchievementYearSlot's docblock.
renderCertsWith([{ title: "CKA" }, { title: "Terraform Associate" }]);
const addYear = container.querySelector('[aria-label^="Add Year"]');
expect(addYear).not.toBeNull();
// opacity-at-rest lives on the parenthesised wrapper span, not the field
// itself — the parens hide alongside the field.
const wrapper = addYear?.parentElement;
expect(wrapper?.className).toContain("opacity-0");
expect(wrapper?.className).toContain("group-hover:opacity-100");
expect(wrapper?.className).toContain("group-focus-within:opacity-100");
});

it("keeps the add-bullet affordance on an ADDED certification", () => {
// The bug: an added credential starts with no bullets, so routing rows by
// bullets alone put it on the compact line — which carries no
// `InlineBulletAdd` — and it could never get one. It takes the full-width
// branch instead, where the affordance lives.
renderCertsWith([{ title: "CKA" }, { title: "Terraform Associate" }]);
expect(container.textContent).not.toContain("Add bullet");
renderCertsWith([{ title: "CKA" }], [
{ id: "added-1", section: "certifications", title: "Terraform Associate" },
]);
expect(container.textContent).toContain("Add bullet");
// …and the parsed credential before it draws no trailing separator into
// the line break that full-width row forces.
expect(middotSpans()).toHaveLength(0);
});

it("stays individually editable and removable inside the compact row", () => {
renderCertsWith([
{ title: "AWS Certified Solutions Architect", year: "2022" },
{ title: "CKA" },
]);
// Title + year fields for both entries, and one remove control each. The
// dateless CKA row still renders its year field (hidden at rest, revealed
// on hover/focus — see AchievementYearSlot) rather than omitting it, so a
// parsed-but-undated credential stays dateable from the compact line.
expect(
container.querySelectorAll('[aria-label^="Edit Certification title"], [aria-label^="Add Certification title"]')
.length,
).toBe(2);
expect(
container.querySelectorAll('[aria-label^="Edit Year"], [aria-label^="Add Year"]')
.length,
).toBe(2);
expect(
container.querySelectorAll('[aria-label="Remove certification"]').length,
).toBe(2);
});
});
Loading
Loading