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 5: Experience Categorization", () => { // Counter for unique IDs let idCounter = 0; // Generator for experience entries const experienceArbitrary = (type: "Software" | "Hardware") => fc.record({ id: fc .string({ minLength: 10, maxLength: 20 }) .map((s) => `exp-${type.toLowerCase()}-${idCounter++}-${s}`), title: fc.string({ minLength: 5, maxLength: 50 }), company: fc.string({ minLength: 3, maxLength: 30 }), period: fc.string({ minLength: 10, maxLength: 30 }), location: fc.string({ minLength: 5, maxLength: 40 }), type: fc.constant(type), description: fc.string({ minLength: 20, maxLength: 200 }), achievements: fc.array(fc.string({ minLength: 10, maxLength: 100 }), { minLength: 1, maxLength: 5, }), technologies: fc.array(fc.string({ minLength: 2, maxLength: 20 }), { minLength: 1, maxLength: 10, }), featured: fc.constant(true), }); beforeEach(() => { idCounter = 0; }); afterEach(() => { jest.clearAllMocks(); }); it("should filter and render Software experiences in the Software category", () => { /** * **Validates: Requirements 4.1** * * Property: For any experience entry with type="Software", it should be * filtered and rendered within the Software category group. This ensures * correct categorization of software-related work experiences. */ fc.assert( fc.property( fc.array(experienceArbitrary("Software"), { minLength: 1, maxLength: 5, }), fc.array(experienceArbitrary("Hardware"), { minLength: 0, maxLength: 5, }), (softwareExps, hardwareExps) => { const allExperiences = [...softwareExps, ...hardwareExps]; mockUsePortfolioData.mockReturnValue({ data: { experiences: allExperiences, personal: {} as any, projects: [], skills: { technical: [], soft: [] }, contact: {} as any, social: [], }, loading: false, error: null, }); const { container } = render(); // Find the Software category section const softwareHeading = Array.from( container.querySelectorAll("h3"), ).find((h3) => h3.textContent?.includes("Software")); if (!softwareHeading) { // If no software heading, there should be no software experiences return softwareExps.length === 0; } // Get the parent container of software experiences const softwareSection = softwareHeading.closest("div"); if (!softwareSection) return false; // Count experience cards in the software section const softwareCards = softwareSection.querySelectorAll("article"); // The number of software cards should match the number of software experiences return softwareCards.length === softwareExps.length; }, ), { numRuns: 100 }, ); }); it("should filter and render Hardware experiences in the Hardware category", () => { /** * **Validates: Requirements 4.1** * * Property: For any experience entry with type="Hardware", it should be * filtered and rendered within the Hardware category group. This ensures * correct categorization of hardware-related work experiences. */ fc.assert( fc.property( fc.array(experienceArbitrary("Software"), { minLength: 0, maxLength: 5, }), fc.array(experienceArbitrary("Hardware"), { minLength: 1, maxLength: 5, }), (softwareExps, hardwareExps) => { const allExperiences = [...softwareExps, ...hardwareExps]; mockUsePortfolioData.mockReturnValue({ data: { experiences: allExperiences, personal: {} as any, projects: [], skills: { technical: [], soft: [] }, contact: {} as any, social: [], }, loading: false, error: null, }); const { container } = render(); // Find the Hardware category section const hardwareHeading = Array.from( container.querySelectorAll("h3"), ).find((h3) => h3.textContent?.includes("Hardware")); if (!hardwareHeading) { // If no hardware heading, there should be no hardware experiences return hardwareExps.length === 0; } // Get the parent container of hardware experiences const hardwareSection = hardwareHeading.closest("div"); if (!hardwareSection) return false; // Count experience cards in the hardware section const hardwareCards = hardwareSection.querySelectorAll("article"); // The number of hardware cards should match the number of hardware experiences return hardwareCards.length === hardwareExps.length; }, ), { numRuns: 100 }, ); }); it("should separate Software and Hardware experiences into distinct category groups", () => { /** * **Validates: Requirements 4.1** * * Property: For any mix of Software and Hardware experiences, they should be * rendered in separate category groups with distinct headings. No experience * should appear in both categories. */ fc.assert( fc.property( fc.array(experienceArbitrary("Software"), { minLength: 1, maxLength: 3, }), fc.array(experienceArbitrary("Hardware"), { minLength: 1, maxLength: 3, }), (softwareExps, hardwareExps) => { const allExperiences = [...softwareExps, ...hardwareExps]; mockUsePortfolioData.mockReturnValue({ data: { experiences: allExperiences, personal: {} as any, projects: [], skills: { technical: [], soft: [] }, contact: {} as any, social: [], }, loading: false, error: null, }); const { container } = render(); // Find both category headings const headings = Array.from(container.querySelectorAll("h3")); const softwareHeading = headings.find((h3) => h3.textContent?.includes("Software"), ); const hardwareHeading = headings.find((h3) => h3.textContent?.includes("Hardware"), ); // Both headings should exist if (!softwareHeading || !hardwareHeading) return false; // Get all experience cards const allCards = container.querySelectorAll("article"); // Total cards should equal total experiences return allCards.length === allExperiences.length; }, ), { numRuns: 100 }, ); }); it("should not render category heading when no experiences exist for that type", () => { /** * **Validates: Requirements 4.1** * * Property: For any category (Software or Hardware) with zero experiences, * the category heading should not be rendered. This ensures clean UI without * empty category sections. */ fc.assert( fc.property( fc.oneof( fc.record({ software: fc.array(experienceArbitrary("Software"), { minLength: 1, maxLength: 5, }), hardware: fc.constant([]), }), fc.record({ software: fc.constant([]), hardware: fc.array(experienceArbitrary("Hardware"), { minLength: 1, maxLength: 5, }), }), ), ({ software, hardware }) => { const allExperiences = [...software, ...hardware]; mockUsePortfolioData.mockReturnValue({ data: { experiences: allExperiences, personal: {} as any, projects: [], skills: { technical: [], soft: [] }, contact: {} as any, social: [], }, loading: false, error: null, }); const { container } = render(); const headings = Array.from(container.querySelectorAll("h3")); const softwareHeading = headings.find((h3) => h3.textContent?.includes("Software"), ); const hardwareHeading = headings.find((h3) => h3.textContent?.includes("Hardware"), ); // If software array is empty, no software heading should exist const softwareCheck = software.length === 0 ? !softwareHeading : !!softwareHeading; // If hardware array is empty, no hardware heading should exist const hardwareCheck = hardware.length === 0 ? !hardwareHeading : !!hardwareHeading; return softwareCheck && hardwareCheck; }, ), { numRuns: 100 }, ); }); it("should maintain experience data integrity within each category", () => { /** * **Validates: Requirements 4.1** * * Property: For any experience entry, its data (title, company, description) * should be rendered correctly within its assigned category. The categorization * process should not modify or lose experience data. */ fc.assert( fc.property( fc.array(experienceArbitrary("Software"), { minLength: 1, maxLength: 3, }), fc.array(experienceArbitrary("Hardware"), { minLength: 1, maxLength: 3, }), (softwareExps, hardwareExps) => { const allExperiences = [...softwareExps, ...hardwareExps]; mockUsePortfolioData.mockReturnValue({ data: { experiences: allExperiences, personal: {} as any, projects: [], skills: { technical: [], soft: [] }, contact: {} as any, social: [], }, loading: false, error: null, }); const { container } = render(); // Check that all experience titles are present in the rendered output const allTitlesPresent = allExperiences.every((exp) => { const titleElements = Array.from(container.querySelectorAll("h3")); return titleElements.some((el) => el.textContent?.includes(exp.title), ); }); // Check that all company names are present const allCompaniesPresent = allExperiences.every((exp) => { return container.textContent?.includes(exp.company); }); return allTitlesPresent && allCompaniesPresent; }, ), { numRuns: 100 }, ); }); it("should handle mixed case and variations in type field values", () => { /** * **Validates: Requirements 4.1** * * Property: For any experience entry, the type field should be matched * case-sensitively to ensure correct categorization. Only exact matches * to "Software" or "Hardware" should be categorized. */ fc.assert( fc.property( fc.array(experienceArbitrary("Software"), { minLength: 1, maxLength: 3, }), (softwareExps) => { mockUsePortfolioData.mockReturnValue({ data: { experiences: softwareExps, personal: {} as any, projects: [], skills: { technical: [], soft: [] }, contact: {} as any, social: [], }, loading: false, error: null, }); const { container } = render(); // Find the Software category heading const headings = Array.from(container.querySelectorAll("h3")); const softwareHeading = headings.find((h3) => h3.textContent?.includes("Software"), ); // Software heading should exist if (!softwareHeading) return false; // All experiences should be rendered const cards = container.querySelectorAll("article"); return cards.length === softwareExps.length; }, ), { numRuns: 100 }, ); }); it("should render all experiences when both categories have entries", () => { /** * **Validates: Requirements 4.1** * * Property: For any non-empty set of Software and Hardware experiences, * all experiences should be rendered across both categories. No experiences * should be lost during the categorization process. */ fc.assert( fc.property( fc.array(experienceArbitrary("Software"), { minLength: 1, maxLength: 4, }), fc.array(experienceArbitrary("Hardware"), { minLength: 1, maxLength: 4, }), (softwareExps, hardwareExps) => { const allExperiences = [...softwareExps, ...hardwareExps]; mockUsePortfolioData.mockReturnValue({ data: { experiences: allExperiences, personal: {} as any, projects: [], skills: { technical: [], soft: [] }, contact: {} as any, social: [], }, loading: false, error: null, }); const { container } = render(); // Count all rendered experience cards const allCards = container.querySelectorAll("article"); // Total cards should equal total experiences return allCards.length === allExperiences.length; }, ), { numRuns: 100 }, ); }); it("should maintain category separation across multiple renders", () => { /** * **Validates: Requirements 4.1** * * Property: For any number of component renders with the same data, * the categorization should remain consistent. Software experiences * should always appear in the Software category and Hardware experiences * in the Hardware category. */ fc.assert( fc.property( fc.integer({ min: 2, max: 4 }), // Number of renders fc.array(experienceArbitrary("Software"), { minLength: 1, maxLength: 2, }), fc.array(experienceArbitrary("Hardware"), { minLength: 1, maxLength: 2, }), (numRenders, softwareExps, hardwareExps) => { const allExperiences = [...softwareExps, ...hardwareExps]; let allRendersValid = true; for (let i = 0; i < numRenders; i++) { mockUsePortfolioData.mockReturnValue({ data: { experiences: allExperiences, personal: {} as any, projects: [], skills: { technical: [], soft: [] }, contact: {} as any, social: [], }, loading: false, error: null, }); const { container, unmount } = render(); // Check that both categories exist const headings = Array.from(container.querySelectorAll("h3")); const softwareHeading = headings.find((h3) => h3.textContent?.includes("Software"), ); const hardwareHeading = headings.find((h3) => h3.textContent?.includes("Hardware"), ); if (!softwareHeading || !hardwareHeading) { allRendersValid = false; } // Check total card count const allCards = container.querySelectorAll("article"); if (allCards.length !== allExperiences.length) { allRendersValid = false; } unmount(); } return allRendersValid; }, ), { numRuns: 100 }, ); }); });