mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-10-03 02:04:31 +10:00
fix(web): close accessibility outline gaps
This commit is contained in:
@@ -180,3 +180,41 @@ it("exposes entry and subordinate-role headings with safe nested rich-text lists
|
||||
expect(screen.queryByText("alert(1)")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("Acme Company")).toHaveTextContent("Acme Company");
|
||||
});
|
||||
|
||||
it.each([" ", "<script>alert(1)</script>", "<style>.hidden { display: none; }</style>", "<iframe>ignored</iframe>"])(
|
||||
"omits summary section when content has no renderable text: %s",
|
||||
(content) => {
|
||||
const data = structuredClone(sampleResumeData);
|
||||
data.summary.content = content;
|
||||
|
||||
renderAccessibleText(data);
|
||||
|
||||
const summaryTitle = data.summary.title?.trim() || "Summary";
|
||||
expect(screen.queryByRole("heading", { level: 2, name: summaryTitle })).not.toBeInTheDocument();
|
||||
},
|
||||
);
|
||||
|
||||
it("promotes role heading to H3 when experience company is blank", () => {
|
||||
const data = structuredClone(sampleResumeData);
|
||||
data.sections.experience.items = [
|
||||
{
|
||||
...data.sections.experience.items[0],
|
||||
id: "blank-company-item",
|
||||
company: "",
|
||||
position: "",
|
||||
roles: [
|
||||
{
|
||||
id: "blank-company-role",
|
||||
position: "Role Without Company",
|
||||
period: "2020 - 2022",
|
||||
description: "<p>Role detail.</p>",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
renderAccessibleText(data);
|
||||
|
||||
expect(screen.getByRole("heading", { level: 3, name: "Role Without Company" })).toBeInTheDocument();
|
||||
expect(screen.queryByRole("heading", { level: 4, name: "Role Without Company" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -128,6 +128,23 @@ function RichText({ html }: { html: string }) {
|
||||
return Array.from(body.childNodes).map((node, index) => renderRichTextNode(node, `rich-text-${index}`));
|
||||
}
|
||||
|
||||
function hasRenderableRichText(html: string): boolean {
|
||||
if (!html.trim()) return false;
|
||||
if (typeof DOMParser === "undefined") return stripHtml(html).trim().length > 0;
|
||||
|
||||
const body = new DOMParser().parseFromString(html, "text/html").body;
|
||||
const getText = (node: ChildNode): string => {
|
||||
if (node.nodeType === 3) return node.textContent ?? "";
|
||||
if (node.nodeType !== 1) return "";
|
||||
|
||||
const element = node as Element;
|
||||
if (RICH_TEXT_OMIT_TAGS.has(element.tagName.toLowerCase())) return "";
|
||||
return Array.from(element.childNodes).map(getText).join("");
|
||||
};
|
||||
|
||||
return Array.from(body.childNodes).some((node) => getText(node).trim().length > 0);
|
||||
}
|
||||
|
||||
type ItemWebsite = { url?: string; label?: string };
|
||||
|
||||
type ItemBodyProps = {
|
||||
@@ -174,7 +191,7 @@ function renderItem(type: CustomSectionType, item: CustomSectionItem, keywordLay
|
||||
<li key={role.id}>
|
||||
<ItemBody
|
||||
heading={role.position}
|
||||
headingLevel={4}
|
||||
headingLevel={it.company.trim() ? 4 : 3}
|
||||
details={role.period}
|
||||
description={role.description}
|
||||
/>
|
||||
@@ -384,7 +401,7 @@ export function ResumeAccessibleText({ data }: ResumeAccessibleTextProps) {
|
||||
) : null}
|
||||
</header>
|
||||
|
||||
{summaryText ? (
|
||||
{hasRenderableRichText(summaryText) ? (
|
||||
<section>
|
||||
<h2>{summary.title?.trim() || getSectionTitle("summary")}</h2>
|
||||
<RichText html={summaryText} />
|
||||
|
||||
Reference in New Issue
Block a user