feat: control room hero, SEO/AEO optimization and design system fixes
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).
This commit is contained in:
1 parent
0920370f30
commit
b791b7de9e
94 files changed
+2621
-2889
No files matched your search
@@ -1,641 +1,146 @@
|
||||
import { render } from "@testing-library/react";
|
||||
import Experience from "./Experience";
|
||||
import fc from "fast-check";
|
||||
import { usePortfolioData } from "@/lib/usePortfolioData";
|
||||
import type { HTMLAttributes, ReactNode } from "react";
|
||||
import type { PortfolioExperience } from "@/lib/portfolio-data";
|
||||
import Experience from "./Experience";
|
||||
|
||||
// Mock the portfolio data hook
|
||||
jest.mock("@/lib/usePortfolioData");
|
||||
const mockUsePortfolioData = usePortfolioData as jest.MockedFunction<
|
||||
typeof usePortfolioData
|
||||
>;
|
||||
type MotionProps = HTMLAttributes<HTMLElement> & {
|
||||
initial?: unknown;
|
||||
whileInView?: unknown;
|
||||
viewport?: unknown;
|
||||
variants?: unknown;
|
||||
whileHover?: unknown;
|
||||
whileTap?: unknown;
|
||||
transition?: unknown;
|
||||
};
|
||||
|
||||
type SectionMockProps = {
|
||||
children?: ReactNode;
|
||||
title?: string;
|
||||
eyebrow?: string;
|
||||
description?: string;
|
||||
disableChildrenAnimation?: boolean;
|
||||
id?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
jest.mock("motion/react", () => {
|
||||
const createMotionElement =
|
||||
(Tag: "div" | "article") =>
|
||||
({
|
||||
children,
|
||||
initial: _initial,
|
||||
whileInView: _whileInView,
|
||||
viewport: _viewport,
|
||||
variants: _variants,
|
||||
whileHover: _whileHover,
|
||||
whileTap: _whileTap,
|
||||
transition: _transition,
|
||||
...props
|
||||
}: MotionProps & { children?: ReactNode }) => (
|
||||
<Tag {...props}>{children}</Tag>
|
||||
);
|
||||
|
||||
// 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>
|
||||
),
|
||||
div: createMotionElement("div"),
|
||||
article: createMotionElement("article"),
|
||||
},
|
||||
useReducedMotion: () => false,
|
||||
};
|
||||
});
|
||||
|
||||
// Mock Section component
|
||||
jest.mock("./Section", () => ({
|
||||
Section: ({ children, ...props }: any) => (
|
||||
<section {...props}>{children}</section>
|
||||
Section: ({
|
||||
children,
|
||||
title,
|
||||
eyebrow: _eyebrow,
|
||||
description: _description,
|
||||
disableChildrenAnimation: _disableChildrenAnimation,
|
||||
...props
|
||||
}: SectionMockProps) => (
|
||||
<section {...props}>
|
||||
<h2>{title}</h2>
|
||||
{children}
|
||||
</section>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock animation components
|
||||
jest.mock("./animations/DrawLine", () => ({
|
||||
DrawLine: () => <div data-testid="draw-line" />,
|
||||
DrawLine: () => <span 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)),
|
||||
);
|
||||
});
|
||||
function makeExperience(
|
||||
index: number,
|
||||
year: number,
|
||||
period = `${year} - Present`,
|
||||
): PortfolioExperience {
|
||||
return {
|
||||
id: `experience-${index}`,
|
||||
title: `Experience ${index}`,
|
||||
company: "Company",
|
||||
period,
|
||||
location: "Remote",
|
||||
type: "Software",
|
||||
description: "Description",
|
||||
achievements: [],
|
||||
technologies: [],
|
||||
featured: true,
|
||||
};
|
||||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
describe("Experience chronological ordering", () => {
|
||||
it("sorts featured experiences by the first year in descending order", () => {
|
||||
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,
|
||||
fc.array(fc.integer({ min: 1990, max: 2035 }), {
|
||||
minLength: 1,
|
||||
maxLength: 8,
|
||||
}),
|
||||
(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"),
|
||||
const experiences = years.map((year, index) =>
|
||||
makeExperience(index, year),
|
||||
);
|
||||
const { container, unmount } = render(
|
||||
<Experience experiences={experiences} />,
|
||||
);
|
||||
const renderedYears = Array.from(
|
||||
container.querySelectorAll("article .ds-kicker"),
|
||||
).map((element) => Number.parseInt(element.textContent ?? "0", 10));
|
||||
const expectedExperiences = [...experiences].sort(
|
||||
(a, b) =>
|
||||
Number.parseInt(b.period, 10) - Number.parseInt(a.period, 10),
|
||||
);
|
||||
const expectedYears = expectedExperiences.map((experience) =>
|
||||
Number.parseInt(experience.period, 10),
|
||||
);
|
||||
const renderedTitles = Array.from(
|
||||
container.querySelectorAll("article h3"),
|
||||
).map((element) => element.textContent);
|
||||
|
||||
// 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;
|
||||
expect(renderedYears).toEqual(expectedYears);
|
||||
expect(renderedTitles).toEqual(
|
||||
expectedExperiences.map((experience) => experience.title),
|
||||
);
|
||||
unmount();
|
||||
},
|
||||
),
|
||||
{ numRuns: 100 },
|
||||
{ numRuns: 25 },
|
||||
);
|
||||
});
|
||||
|
||||
it("sorts a period range by its first year", () => {
|
||||
const experiences = [
|
||||
makeExperience(1, 2022, "2022 - 2024"),
|
||||
makeExperience(2, 2024, "2024 - Present"),
|
||||
makeExperience(3, 2021, "2021 - 2023"),
|
||||
];
|
||||
const { container } = render(<Experience experiences={experiences} />);
|
||||
|
||||
expect(
|
||||
Array.from(container.querySelectorAll("article .ds-kicker")).map(
|
||||
(element) => element.textContent,
|
||||
),
|
||||
).toEqual(["2024 - Present", "2022 - 2024", "2021 - 2023"]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user