Files
portifolio-evandrodevbr/app/components/Experience.chronologicalOrder.pbt.test.tsx
Evandro Fonseca Junior 08dd046887 feat: add animated components for enhanced UI/UX
- Introduced DrawLine component for drawing animations.
- Added LazyRevealImage component for lazy loading images with reveal animations.
- Implemented TextDiffuseReveal for animated text reveals with staggered effects.
- Created TopProgressBar for a dynamic loading indicator.
- Added PointCloud mock for testing animations.
- Developed useReducedMotion hook to handle user preferences for reduced motion.
- Centralized animation configurations in config.ts for easier management.
- Defined animation variants in variants.ts for consistent animation effects.
- Established AnimationProvider to supply global animation settings.
- Created tests for AnimationProvider and useReducedMotion hook.
- Added SVG assets for branding and UI elements.
- Documented frontend review plan and hero circuit board animation plan.
2026-03-15 21:06:43 -03:00

642 lines
20 KiB
TypeScript

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) => <div {...props}>{children}</div>,
article: ({ children, ...props }: any) => (
<article {...props}>{children}</article>
),
},
};
});
// Mock Section component
jest.mock("./Section", () => ({
Section: ({ children, ...props }: any) => (
<section {...props}>{children}</section>
),
}));
// Mock animation components
jest.mock("./animations/DrawLine", () => ({
DrawLine: () => <div data-testid="draw-line" />,
}));
jest.mock("./animations/CounterValue", () => ({
CounterValue: ({ value, suffix }: any) => (
<span>
{value}
{suffix}
</span>
),
}));
// Mock SectionLoading
jest.mock("./SectionLoading", () => ({
SectionLoading: ({ message }: any) => <div>{message}</div>,
}));
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(<Experience />);
// 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(<Experience />);
// 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(<Experience />);
// 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(<Experience />);
// 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(<Experience />);
// 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(<Experience />);
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(<Experience />);
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 },
);
});
});