import { render } from "@testing-library/react"; import Experience from "./Experience"; import fc from "fast-check"; import { usePortfolioData } from "@/lib/usePortfolioData"; // Mock the portfolio data hook jest.mock("@/lib/usePortfolioData"); const mockUsePortfolioData = usePortfolioData as jest.MockedFunction< typeof usePortfolioData >; // Mock framer-motion jest.mock("framer-motion", () => { const actual = jest.requireActual("framer-motion"); return { ...actual, useReducedMotion: () => false, motion: { div: ({ children, ...props }: any) =>
{children}
, article: ({ children, ...props }: any) => (
{children}
), }, }; }); // Mock Section component jest.mock("./Section", () => ({ Section: ({ children, ...props }: any) => (
{children}
), })); // Mock animation components jest.mock("./animations/DrawLine", () => ({ DrawLine: () =>
, })); jest.mock("./animations/CounterValue", () => ({ CounterValue: ({ value, suffix }: any) => ( {value} {suffix} ), })); // Mock SectionLoading jest.mock("./SectionLoading", () => ({ SectionLoading: ({ message }: any) =>
{message}
, })); describe("Property 11: Semantic Heading Hierarchy", () => { afterEach(() => { jest.clearAllMocks(); }); it("should maintain proper heading hierarchy (h2 for section, h3 for cards)", () => { /** * **Validates: Requirements 6.4** * * Property: For any sequence of heading elements in the Experience section, * the heading levels should follow proper semantic hierarchy (h2 for section * title, h3 for experience card titles, no skipped levels). This ensures * screen readers can properly navigate the document structure. */ fc.assert( fc.property( fc.array( fc.record({ id: fc.string(), title: fc.string({ minLength: 1 }), company: fc.string({ minLength: 1 }), type: fc.constantFrom("Software", "Hardware"), period: fc.string({ minLength: 1 }), location: fc.string({ minLength: 1 }), description: fc.string({ minLength: 1 }), achievements: fc.array(fc.string({ minLength: 1 })), technologies: fc.array(fc.string({ minLength: 1 })), featured: fc.constant(true), }), { minLength: 1, maxLength: 10 }, ), (experiences) => { mockUsePortfolioData.mockReturnValue({ data: { experiences, personal: {} as any, projects: [], skills: { technical: [], soft: [] }, contact: {} as any, social: [], }, loading: false, error: null, }); const { container } = render(); // Get all heading elements in order const headings = Array.from( container.querySelectorAll("h1, h2, h3, h4, h5, h6"), ); if (headings.length === 0) { return false; } // Extract heading levels const levels = headings.map((h) => parseInt(h.tagName.substring(1))); // Check for proper hierarchy: // 1. Should start with h2 (section title) // 2. Experience card titles should be h3 // 3. No skipped levels (difference should not be > 1) // First heading should be h2 (section title) if (levels[0] !== 2) { return false; } // Check for skipped levels for (let i = 1; i < levels.length; i++) { const diff = levels[i] - levels[i - 1]; // Difference should be -1, 0, or 1 (no skipping levels) if (Math.abs(diff) > 1 && diff > 0) { return false; } } // All experience card titles should be h3 const cardTitles = container.querySelectorAll("article h3"); if (cardTitles.length === 0) { return false; } return true; }, ), { numRuns: 100 }, ); }); it("should not skip heading levels in the hierarchy", () => { /** * **Validates: Requirements 6.4** * * Property: For any generated set of experiences, the heading hierarchy * should never skip levels (e.g., h2 directly to h4). This is critical * for accessibility as screen reader users rely on heading levels to * understand document structure. */ fc.assert( fc.property( fc.array( fc.record({ id: fc.uuid(), title: fc.lorem({ maxCount: 5 }), company: fc.lorem({ maxCount: 2 }), type: fc.constantFrom("Software", "Hardware"), period: fc.string({ minLength: 1 }), location: fc.string({ minLength: 1 }), description: fc.lorem({ maxCount: 10 }), achievements: fc.array(fc.lorem({ maxCount: 5 }), { maxLength: 3 }), technologies: fc.array(fc.lorem({ maxCount: 1 }), { maxLength: 5 }), featured: fc.constant(true), }), { minLength: 2, maxLength: 8 }, ), (experiences) => { mockUsePortfolioData.mockReturnValue({ data: { experiences, personal: {} as any, projects: [], skills: { technical: [], soft: [] }, contact: {} as any, social: [], }, loading: false, error: null, }); const { container } = render(); // Get all headings const headings = Array.from( container.querySelectorAll("h1, h2, h3, h4, h5, h6"), ); const levels = headings.map((h) => parseInt(h.tagName.substring(1))); // Check that we never skip levels when going deeper for (let i = 1; i < levels.length; i++) { const currentLevel = levels[i]; const previousLevel = levels[i - 1]; // When going deeper (increasing level number), should only increase by 1 if ( currentLevel > previousLevel && currentLevel - previousLevel > 1 ) { return false; } } return true; }, ), { numRuns: 100 }, ); }); it("should use h2 for section title consistently", () => { /** * **Validates: Requirements 6.4** * * Property: For any Experience section render, the main section title * should always be an h2 element. This maintains consistency with the * overall page structure where main sections use h2. */ fc.assert( fc.property( fc.array( fc.record({ id: fc.string(), title: fc.string({ minLength: 1 }), company: fc.string({ minLength: 1 }), type: fc.constantFrom("Software", "Hardware"), period: fc.string({ minLength: 1 }), location: fc.string({ minLength: 1 }), description: fc.string({ minLength: 1 }), achievements: fc.array(fc.string({ minLength: 1 })), technologies: fc.array(fc.string({ minLength: 1 })), featured: fc.constant(true), }), { minLength: 1, maxLength: 5 }, ), (experiences) => { mockUsePortfolioData.mockReturnValue({ data: { experiences, personal: {} as any, projects: [], skills: { technical: [], soft: [] }, contact: {} as any, social: [], }, loading: false, error: null, }); const { container } = render(); // Find the section element const section = container.querySelector("section"); if (!section) return false; // The first heading in the section should be h2 const firstHeading = section.querySelector("h1, h2, h3, h4, h5, h6"); if (!firstHeading) return false; return firstHeading.tagName === "H2"; }, ), { numRuns: 100 }, ); }); it("should use h3 for all experience card titles", () => { /** * **Validates: Requirements 6.4** * * Property: For any set of experience cards rendered, each card's title * should be an h3 element. This maintains proper hierarchy under the * section h2 and ensures consistent navigation for assistive technology. */ fc.assert( fc.property( fc.array( fc.record({ id: fc.uuid(), title: fc.lorem({ maxCount: 5 }), company: fc.lorem({ maxCount: 2 }), type: fc.constantFrom("Software", "Hardware"), period: fc.string({ minLength: 1 }), location: fc.string({ minLength: 1 }), description: fc.lorem({ maxCount: 10 }), achievements: fc.array(fc.lorem({ maxCount: 5 })), technologies: fc.array(fc.lorem({ maxCount: 1 })), featured: fc.constant(true), }), { minLength: 1, maxLength: 10 }, ), (experiences) => { mockUsePortfolioData.mockReturnValue({ data: { experiences, personal: {} as any, projects: [], skills: { technical: [], soft: [] }, contact: {} as any, social: [], }, loading: false, error: null, }); const { container } = render(); // Find all article elements (experience cards) const articles = container.querySelectorAll("article"); if (articles.length === 0) { return false; } // Each article should have at least one h3 for the title let allArticlesHaveH3 = true; articles.forEach((article) => { const h3Elements = article.querySelectorAll("h3"); // Should have at least one h3 (the title) if (h3Elements.length === 0) { allArticlesHaveH3 = false; } }); return allArticlesHaveH3; }, ), { numRuns: 100 }, ); }); it("should maintain hierarchy across multiple renders", () => { /** * **Validates: Requirements 6.4** * * Property: For any number of component re-renders with the same data, * the heading hierarchy should remain consistent. This ensures the * semantic structure is stable and reliable for assistive technology. */ fc.assert( fc.property( fc.array( fc.record({ id: fc.string(), title: fc.string({ minLength: 1 }), company: fc.string({ minLength: 1 }), type: fc.constantFrom("Software", "Hardware"), period: fc.string({ minLength: 1 }), location: fc.string({ minLength: 1 }), description: fc.string({ minLength: 1 }), achievements: fc.array(fc.string({ minLength: 1 })), technologies: fc.array(fc.string({ minLength: 1 })), featured: fc.constant(true), }), { minLength: 1, maxLength: 5 }, ), fc.integer({ min: 2, max: 5 }), (experiences, numRenders) => { let previousHierarchy: number[] | null = null; for (let i = 0; i < numRenders; i++) { mockUsePortfolioData.mockReturnValue({ data: { experiences, personal: {} as any, projects: [], skills: { technical: [], soft: [] }, contact: {} as any, social: [], }, loading: false, error: null, }); const { container, unmount } = render(); const headings = Array.from( container.querySelectorAll("h1, h2, h3, h4, h5, h6"), ); const currentHierarchy = headings.map((h) => parseInt(h.tagName.substring(1)), ); if (previousHierarchy !== null) { // Hierarchy should be identical across renders if ( JSON.stringify(currentHierarchy) !== JSON.stringify(previousHierarchy) ) { return false; } } previousHierarchy = currentHierarchy; unmount(); } return true; }, ), { numRuns: 100 }, ); }); });