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 6: Chronological Ordering Within Categories", () => { // Counter for unique IDs let idCounter = 0; // Generator for experience entries with specific year const experienceWithYearArbitrary = ( type: "Software" | "Hardware", year: number, ) => fc.record({ id: fc .string({ minLength: 10, maxLength: 20 }) .map((s) => `exp-${type.toLowerCase()}-${year}-${idCounter++}-${s}`), title: fc.string({ minLength: 5, maxLength: 50 }), company: fc.string({ minLength: 3, maxLength: 30 }), period: fc.constant(`Janeiro ${year} - Dezembro ${year}`), 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), }); // Generator for array of experiences with random years const experiencesWithYearsArbitrary = ( type: "Software" | "Hardware", count: number, ) => { return fc .array(fc.integer({ min: 2015, max: 2025 }), { minLength: count, maxLength: count, }) .chain((years) => { return fc.tuple( ...years.map((year) => experienceWithYearArbitrary(type, year)), ); }); }; beforeEach(() => { idCounter = 0; }); afterEach(() => { jest.clearAllMocks(); }); it("should sort Software experiences in descending chronological order by year", () => { /** * **Validates: Requirements 4.3** * * Property: For any set of Software experiences with different years, * they should be rendered in descending chronological order (newest first). * This ensures users see the most recent experiences at the top. */ fc.assert( fc.property( fc .integer({ min: 2, max: 5 }) .chain((count) => experiencesWithYearsArbitrary("Software", count)), (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 section const softwareHeading = Array.from( container.querySelectorAll("h3"), ).find((h3) => h3.textContent?.includes("Software")); if (!softwareHeading) return false; // Get the parent container of software experiences const softwareSection = softwareHeading.closest("div"); if (!softwareSection) return false; // Get all experience cards in the software section const softwareCards = Array.from( softwareSection.querySelectorAll("article"), ); // Extract years from the rendered cards const renderedYears = softwareCards.map((card) => { const periodText = card.querySelector(".ds-kicker")?.textContent || ""; const yearMatch = periodText.match(/\d{4}/); return yearMatch ? Number.parseInt(yearMatch[0], 10) : 0; }); // Check if years are in descending order for (let i = 0; i < renderedYears.length - 1; i++) { if (renderedYears[i] < renderedYears[i + 1]) { return false; } } return true; }, ), { numRuns: 100 }, ); }); it("should sort Hardware experiences in descending chronological order by year", () => { /** * **Validates: Requirements 4.3** * * Property: For any set of Hardware experiences with different years, * they should be rendered in descending chronological order (newest first). * This ensures users see the most recent experiences at the top. */ fc.assert( fc.property( fc .integer({ min: 2, max: 5 }) .chain((count) => experiencesWithYearsArbitrary("Hardware", count)), (hardwareExps) => { mockUsePortfolioData.mockReturnValue({ data: { experiences: hardwareExps, 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) return false; // Get the parent container of hardware experiences const hardwareSection = hardwareHeading.closest("div"); if (!hardwareSection) return false; // Get all experience cards in the hardware section const hardwareCards = Array.from( hardwareSection.querySelectorAll("article"), ); // Extract years from the rendered cards const renderedYears = hardwareCards.map((card) => { const periodText = card.querySelector(".ds-kicker")?.textContent || ""; const yearMatch = periodText.match(/\d{4}/); return yearMatch ? Number.parseInt(yearMatch[0], 10) : 0; }); // Check if years are in descending order for (let i = 0; i < renderedYears.length - 1; i++) { if (renderedYears[i] < renderedYears[i + 1]) { return false; } } return true; }, ), { numRuns: 100 }, ); }); it("should maintain chronological order within each category independently", () => { /** * **Validates: Requirements 4.3** * * Property: For any mix of Software and Hardware experiences with various years, * each category should maintain its own descending chronological order independently. * The ordering in one category should not affect the other. */ fc.assert( fc.property( fc .integer({ min: 2, max: 4 }) .chain((softwareCount) => fc .integer({ min: 2, max: 4 }) .chain((hardwareCount) => fc.tuple( experiencesWithYearsArbitrary("Software", softwareCount), experiencesWithYearsArbitrary("Hardware", hardwareCount), ), ), ), ([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 Software category ordering const softwareHeading = Array.from( container.querySelectorAll("h3"), ).find((h3) => h3.textContent?.includes("Software")); if (!softwareHeading) return false; const softwareSection = softwareHeading.closest("div"); if (!softwareSection) return false; const softwareCards = Array.from( softwareSection.querySelectorAll("article"), ); const softwareYears = softwareCards.map((card) => { const periodText = card.querySelector(".ds-kicker")?.textContent || ""; const yearMatch = periodText.match(/\d{4}/); return yearMatch ? Number.parseInt(yearMatch[0], 10) : 0; }); // Check if software years are in descending order for (let i = 0; i < softwareYears.length - 1; i++) { if (softwareYears[i] < softwareYears[i + 1]) { return false; } } // Check Hardware category ordering const hardwareHeading = Array.from( container.querySelectorAll("h3"), ).find((h3) => h3.textContent?.includes("Hardware")); if (!hardwareHeading) return false; const hardwareSection = hardwareHeading.closest("div"); if (!hardwareSection) return false; const hardwareCards = Array.from( hardwareSection.querySelectorAll("article"), ); const hardwareYears = hardwareCards.map((card) => { const periodText = card.querySelector(".ds-kicker")?.textContent || ""; const yearMatch = periodText.match(/\d{4}/); return yearMatch ? Number.parseInt(yearMatch[0], 10) : 0; }); // Check if hardware years are in descending order for (let i = 0; i < hardwareYears.length - 1; i++) { if (hardwareYears[i] < hardwareYears[i + 1]) { return false; } } return true; }, ), { numRuns: 100 }, ); }); it("should handle experiences with the same year within a category", () => { /** * **Validates: Requirements 4.3** * * Property: For any set of experiences with the same year within a category, * they should all be rendered without errors. The relative order of same-year * experiences is not specified but should be stable. */ fc.assert( fc.property( fc.integer({ min: 2015, max: 2025 }), fc.integer({ min: 2, max: 4 }), (year, count) => { const softwareExps = Array.from({ length: count }, (_, i) => ({ id: `exp-software-${year}-${i}`, title: `Software Title ${i}`, company: `Company ${i}`, period: `Janeiro ${year} - Dezembro ${year}`, location: `Location ${i}`, type: "Software" as const, description: `Description ${i}`, achievements: [`Achievement ${i}`], technologies: [`Tech ${i}`], featured: true, })); 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 section const softwareHeading = Array.from( container.querySelectorAll("h3"), ).find((h3) => h3.textContent?.includes("Software")); if (!softwareHeading) return false; const softwareSection = softwareHeading.closest("div"); if (!softwareSection) return false; const softwareCards = softwareSection.querySelectorAll("article"); // All experiences should be rendered return softwareCards.length === count; }, ), { numRuns: 100 }, ); }); it("should correctly parse years from various period formats", () => { /** * **Validates: Requirements 4.3** * * Property: For any experience with a period string containing a 4-digit year, * the year should be correctly extracted and used for chronological ordering. * This ensures robust parsing across different date formats. */ fc.assert( fc.property( fc.array( fc.record({ year: fc.integer({ min: 2015, max: 2025 }), format: fc.constantFrom( (y: number) => `Janeiro ${y} - Dezembro ${y}`, (y: number) => `${y} - Presente`, (y: number) => `Dezembro ${y} - Fevereiro ${y + 1}`, (y: number) => `Junho ${y} - Presente`, ), }), { minLength: 2, maxLength: 5 }, ), (periodConfigs) => { const softwareExps = periodConfigs.map((config, i) => ({ id: `exp-software-${i}`, title: `Software Title ${i}`, company: `Company ${i}`, period: config.format(config.year), location: `Location ${i}`, type: "Software" as const, description: `Description ${i}`, achievements: [`Achievement ${i}`], technologies: [`Tech ${i}`], featured: true, })); 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 section const softwareHeading = Array.from( container.querySelectorAll("h3"), ).find((h3) => h3.textContent?.includes("Software")); if (!softwareHeading) return false; const softwareSection = softwareHeading.closest("div"); if (!softwareSection) return false; const softwareCards = Array.from( softwareSection.querySelectorAll("article"), ); // Extract years from the rendered cards const renderedYears = softwareCards.map((card) => { const periodText = card.querySelector(".ds-kicker")?.textContent || ""; const yearMatch = periodText.match(/\d{4}/); return yearMatch ? Number.parseInt(yearMatch[0], 10) : 0; }); // Check if years are in descending order for (let i = 0; i < renderedYears.length - 1; i++) { if (renderedYears[i] < renderedYears[i + 1]) { return false; } } return true; }, ), { numRuns: 100 }, ); }); it("should maintain chronological order across component re-renders", () => { /** * **Validates: Requirements 4.3** * * Property: For any set of experiences, the chronological ordering should * remain consistent across multiple component renders. The sorting should * be deterministic and stable. */ fc.assert( fc.property( fc .integer({ min: 3, max: 5 }) .chain((count) => experiencesWithYearsArbitrary("Software", count)), fc.integer({ min: 2, max: 3 }), // Number of renders (softwareExps, numRenders) => { let firstRenderYears: number[] = []; for (let i = 0; i < numRenders; i++) { mockUsePortfolioData.mockReturnValue({ data: { experiences: softwareExps, personal: {} as any, projects: [], skills: { technical: [], soft: [] }, contact: {} as any, social: [], }, loading: false, error: null, }); const { container, unmount } = render(); const softwareHeading = Array.from( container.querySelectorAll("h3"), ).find((h3) => h3.textContent?.includes("Software")); if (!softwareHeading) return false; const softwareSection = softwareHeading.closest("div"); if (!softwareSection) return false; const softwareCards = Array.from( softwareSection.querySelectorAll("article"), ); const renderedYears = softwareCards.map((card) => { const periodText = card.querySelector(".ds-kicker")?.textContent || ""; const yearMatch = periodText.match(/\d{4}/); return yearMatch ? Number.parseInt(yearMatch[0], 10) : 0; }); if (i === 0) { firstRenderYears = renderedYears; } else { // Check if the order matches the first render if (renderedYears.length !== firstRenderYears.length) return false; for (let j = 0; j < renderedYears.length; j++) { if (renderedYears[j] !== firstRenderYears[j]) return false; } } unmount(); } return true; }, ), { numRuns: 100 }, ); }); it("should sort experiences with year ranges by the first year", () => { /** * **Validates: Requirements 4.3** * * Property: For any experience with a period containing multiple years * (e.g., "2023 - 2024"), the sorting should use the first year found. * This ensures consistent ordering for multi-year experiences. */ fc.assert( fc.property( fc.array(fc.integer({ min: 2015, max: 2024 }), { minLength: 2, maxLength: 5, }), (years) => { const softwareExps = years.map((year, i) => ({ id: `exp-software-${i}`, title: `Software Title ${i}`, company: `Company ${i}`, period: `Janeiro ${year} - Dezembro ${year + 1}`, location: `Location ${i}`, type: "Software" as const, description: `Description ${i}`, achievements: [`Achievement ${i}`], technologies: [`Tech ${i}`], featured: true, })); mockUsePortfolioData.mockReturnValue({ data: { experiences: softwareExps, personal: {} as any, projects: [], skills: { technical: [], soft: [] }, contact: {} as any, social: [], }, loading: false, error: null, }); const { container } = render(); const softwareHeading = Array.from( container.querySelectorAll("h3"), ).find((h3) => h3.textContent?.includes("Software")); if (!softwareHeading) return false; const softwareSection = softwareHeading.closest("div"); if (!softwareSection) return false; const softwareCards = Array.from( softwareSection.querySelectorAll("article"), ); // Extract first year from each rendered card const renderedYears = softwareCards.map((card) => { const periodText = card.querySelector(".ds-kicker")?.textContent || ""; const yearMatch = periodText.match(/\d{4}/); return yearMatch ? Number.parseInt(yearMatch[0], 10) : 0; }); // Check if years are in descending order for (let i = 0; i < renderedYears.length - 1; i++) { if (renderedYears[i] < renderedYears[i + 1]) { return false; } } return true; }, ), { numRuns: 100 }, ); }); });