Hero: replace particle field with cobe globe (Capricorn view, multi-axis drag, reduced-motion safe), telemetry HUD with pointer parallax, particle text build effect, Space Grotesk display font. SEO: short titles with template, PT-BR accents, twitter cards on dynamic cases, escaped JSON-LD via lib/jsonld.ts, CSP report-only, 25 service FAQs answer-first. Design: fix TW4 layered reset killing spacing utilities, literal @theme tokens, mobile hero overflow, header overlap, empty case cards. Remove framer-motion (unified on motion/react), header version badge, phone number from hero CTA. Production build verified (24 static pages).
301 lines
10 KiB
TypeScript
301 lines
10 KiB
TypeScript
import { render } from "@testing-library/react";
|
|
import fc from "fast-check";
|
|
import Footer from "./Footer";
|
|
|
|
describe("Property 9: ARIA Labels for Links", () => {
|
|
it("should have descriptive aria-label attributes for all social media links", () => {
|
|
/**
|
|
* **Validates: Requirements 6.1**
|
|
*
|
|
* Property: For any social media link in the Footer component, it should have
|
|
* a descriptive aria-label attribute that identifies the platform and action.
|
|
* This ensures accessibility for users with assistive technologies.
|
|
*/
|
|
fc.assert(
|
|
fc.property(
|
|
fc.constant(null), // No random input needed, testing static component
|
|
() => {
|
|
const { container } = render(<Footer />);
|
|
|
|
// Find all social media links (they have SVG icons and are in the Connect section)
|
|
const socialLinks = container.querySelectorAll(
|
|
"footer a[aria-label]",
|
|
);
|
|
|
|
// Filter to only social links (those with SVG children)
|
|
const socialLinksWithSvg = Array.from(socialLinks).filter((link) => {
|
|
return link.querySelector("svg") !== null;
|
|
});
|
|
|
|
// Verify we found social links
|
|
if (socialLinksWithSvg.length === 0) {
|
|
return false;
|
|
}
|
|
|
|
// Check each social link has a non-empty, descriptive aria-label
|
|
return socialLinksWithSvg.every((link) => {
|
|
const ariaLabel = link.getAttribute("aria-label");
|
|
|
|
// aria-label must exist and be non-empty
|
|
if (!ariaLabel || ariaLabel.trim().length === 0) {
|
|
return false;
|
|
}
|
|
|
|
// aria-label should be descriptive (at least 5 characters)
|
|
// and should describe an action (contain words like "Visit", "View", "Go to", etc.)
|
|
return ariaLabel.length >= 5;
|
|
});
|
|
},
|
|
),
|
|
{ numRuns: 100 },
|
|
);
|
|
});
|
|
|
|
it("should have unique aria-label for each social platform", () => {
|
|
/**
|
|
* **Validates: Requirements 6.1**
|
|
*
|
|
* Property: For any set of social media links, each should have a unique
|
|
* aria-label that identifies the specific platform. This ensures users can
|
|
* distinguish between different social links when using screen readers.
|
|
*/
|
|
fc.assert(
|
|
fc.property(fc.constant(null), () => {
|
|
const { container } = render(<Footer />);
|
|
|
|
// Find all social media links with aria-labels
|
|
const socialLinks = container.querySelectorAll("footer a[aria-label]");
|
|
|
|
// Filter to only social links (those with SVG children)
|
|
const socialLinksWithSvg = Array.from(socialLinks).filter((link) => {
|
|
return link.querySelector("svg") !== null;
|
|
});
|
|
|
|
// Collect all aria-labels
|
|
const ariaLabels = socialLinksWithSvg.map((link) =>
|
|
link.getAttribute("aria-label"),
|
|
);
|
|
|
|
// Verify we have aria-labels
|
|
if (ariaLabels.length === 0) {
|
|
return false;
|
|
}
|
|
|
|
// Check that all aria-labels are unique
|
|
const uniqueLabels = new Set(ariaLabels);
|
|
return uniqueLabels.size === ariaLabels.length;
|
|
}),
|
|
{ numRuns: 100 },
|
|
);
|
|
});
|
|
|
|
it("should have aria-label that identifies both platform and action", () => {
|
|
/**
|
|
* **Validates: Requirements 6.1**
|
|
*
|
|
* Property: For any social media link, the aria-label should contain both
|
|
* the platform name (GitHub, LinkedIn, etc.) and an action verb (Visit, View, etc.).
|
|
* This provides complete context for assistive technology users.
|
|
*/
|
|
fc.assert(
|
|
fc.property(fc.constant(null), () => {
|
|
const { container } = render(<Footer />);
|
|
|
|
// Find all social media links
|
|
const socialLinks = container.querySelectorAll("footer a[aria-label]");
|
|
|
|
// Filter to only social links (those with SVG children)
|
|
const socialLinksWithSvg = Array.from(socialLinks).filter((link) => {
|
|
return link.querySelector("svg") !== null;
|
|
});
|
|
|
|
// Verify we found social links
|
|
if (socialLinksWithSvg.length === 0) {
|
|
return false;
|
|
}
|
|
|
|
// Common action verbs for social links
|
|
const actionVerbs = ["visit", "view", "go to", "open", "see", "check"];
|
|
|
|
// Check each link has an action verb in its aria-label
|
|
return socialLinksWithSvg.every((link) => {
|
|
const ariaLabel = link.getAttribute("aria-label");
|
|
|
|
if (!ariaLabel) {
|
|
return false;
|
|
}
|
|
|
|
const lowerLabel = ariaLabel.toLowerCase();
|
|
|
|
// Check if aria-label contains at least one action verb
|
|
return actionVerbs.some((verb) => lowerLabel.includes(verb));
|
|
});
|
|
}),
|
|
{ numRuns: 100 },
|
|
);
|
|
});
|
|
|
|
it("should maintain aria-labels across multiple renders", () => {
|
|
/**
|
|
* **Validates: Requirements 6.1**
|
|
*
|
|
* Property: For any number of component renders, social links should consistently
|
|
* maintain their aria-label attributes. This verifies that accessibility attributes
|
|
* are not accidentally removed during re-renders.
|
|
*/
|
|
fc.assert(
|
|
fc.property(
|
|
fc.integer({ min: 1, max: 5 }), // Test 1-5 renders
|
|
(numRenders) => {
|
|
let allRendersValid = true;
|
|
|
|
for (let i = 0; i < numRenders; i++) {
|
|
const { container, unmount } = render(<Footer />);
|
|
|
|
// Find social links
|
|
const socialLinks = container.querySelectorAll(
|
|
"footer a[aria-label]",
|
|
);
|
|
const socialLinksWithSvg = Array.from(socialLinks).filter(
|
|
(link) => {
|
|
return link.querySelector("svg") !== null;
|
|
},
|
|
);
|
|
|
|
// Check each link has a valid aria-label
|
|
socialLinksWithSvg.forEach((link) => {
|
|
const ariaLabel = link.getAttribute("aria-label");
|
|
|
|
if (!ariaLabel || ariaLabel.trim().length < 5) {
|
|
allRendersValid = false;
|
|
}
|
|
});
|
|
|
|
unmount();
|
|
}
|
|
|
|
return allRendersValid;
|
|
},
|
|
),
|
|
{ numRuns: 100 },
|
|
);
|
|
});
|
|
|
|
it('should have aria-label for every social link with target="_blank"', () => {
|
|
/**
|
|
* **Validates: Requirements 6.1**
|
|
*
|
|
* Property: For any social link that opens in a new tab (target="_blank"),
|
|
* it must have an aria-label. This ensures users with assistive technologies
|
|
* are informed about where the link leads before activating it.
|
|
*/
|
|
fc.assert(
|
|
fc.property(fc.constant(null), () => {
|
|
const { container } = render(<Footer />);
|
|
|
|
// Find all links that open in new tab
|
|
const newTabLinks = container.querySelectorAll(
|
|
'footer a[target="_blank"]',
|
|
);
|
|
|
|
// Verify we found links
|
|
if (newTabLinks.length === 0) {
|
|
return false;
|
|
}
|
|
|
|
// Each new tab link should have an aria-label
|
|
return Array.from(newTabLinks).every((link) => {
|
|
const ariaLabel = link.getAttribute("aria-label");
|
|
return ariaLabel !== null && ariaLabel.trim().length > 0;
|
|
});
|
|
}),
|
|
{ numRuns: 100 },
|
|
);
|
|
});
|
|
|
|
it("should not have empty or whitespace-only aria-labels", () => {
|
|
/**
|
|
* **Validates: Requirements 6.1**
|
|
*
|
|
* Property: For any social media link with an aria-label attribute,
|
|
* the value should not be empty or contain only whitespace. Empty
|
|
* aria-labels provide no accessibility benefit and can confuse screen readers.
|
|
*/
|
|
fc.assert(
|
|
fc.property(fc.constant(null), () => {
|
|
const { container } = render(<Footer />);
|
|
|
|
// Find all social media links
|
|
const socialLinks = container.querySelectorAll("footer a[aria-label]");
|
|
|
|
// Filter to only social links (those with SVG children)
|
|
const socialLinksWithSvg = Array.from(socialLinks).filter((link) => {
|
|
return link.querySelector("svg") !== null;
|
|
});
|
|
|
|
// Verify we found social links
|
|
if (socialLinksWithSvg.length === 0) {
|
|
return false;
|
|
}
|
|
|
|
// Check that no aria-label is empty or whitespace-only
|
|
return socialLinksWithSvg.every((link) => {
|
|
const ariaLabel = link.getAttribute("aria-label");
|
|
return ariaLabel !== null && ariaLabel.trim().length > 0;
|
|
});
|
|
}),
|
|
{ numRuns: 100 },
|
|
);
|
|
});
|
|
|
|
it("should have aria-label that matches or relates to the link title", () => {
|
|
/**
|
|
* **Validates: Requirements 6.1**
|
|
*
|
|
* Property: For any social media link with both aria-label and title attributes,
|
|
* the aria-label should provide context that relates to the title (platform name).
|
|
* This ensures consistency between visual tooltips and screen reader announcements.
|
|
*/
|
|
fc.assert(
|
|
fc.property(fc.constant(null), () => {
|
|
const { container } = render(<Footer />);
|
|
|
|
// Find all social media links with both aria-label and title
|
|
const socialLinks = container.querySelectorAll(
|
|
"footer a[aria-label][title]",
|
|
);
|
|
|
|
// Filter to only social links (those with SVG children)
|
|
const socialLinksWithSvg = Array.from(socialLinks).filter((link) => {
|
|
return link.querySelector("svg") !== null;
|
|
});
|
|
|
|
// If no links have both attributes, that's okay (not all implementations require title)
|
|
if (socialLinksWithSvg.length === 0) {
|
|
return true;
|
|
}
|
|
|
|
// Check that aria-label relates to title
|
|
return socialLinksWithSvg.every((link) => {
|
|
const ariaLabel = link.getAttribute("aria-label");
|
|
const title = link.getAttribute("title");
|
|
|
|
if (!ariaLabel || !title) {
|
|
return false;
|
|
}
|
|
|
|
// aria-label should contain the platform name from title
|
|
// or title should be contained in aria-label
|
|
const lowerLabel = ariaLabel.toLowerCase();
|
|
const lowerTitle = title.toLowerCase();
|
|
|
|
return (
|
|
lowerLabel.includes(lowerTitle) || lowerTitle.includes(lowerLabel)
|
|
);
|
|
});
|
|
}),
|
|
{ numRuns: 100 },
|
|
);
|
|
});
|
|
});
|