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:
Evandro Fonseca Junior committed 2026-08-27 10:55:21 -03:00
1 parent 0920370f30
commit b791b7de9e
94 files changed
+2621 -2889

No files matched your search

@@ -1,556 +1,127 @@
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>
),
}));
function makeExperience(index: number, featured: boolean): PortfolioExperience {
return {
id: `experience-${index}`,
title: `Experience ${index}`,
company: "Company",
period: `${2020 + index} - Present`,
location: "Remote",
type: index % 2 === 0 ? "Software" : "Hardware",
description: "Description",
achievements: [],
technologies: [],
featured,
};
}
// Mock SectionLoading
jest.mock("./SectionLoading", () => ({
SectionLoading: ({ message }: any) => <div>{message}</div>,
}));
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.
*/
describe("Experience featured filtering", () => {
it("renders exactly the featured 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(<Experience />);
// 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(<Experience />);
// 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(<Experience />);
// Find both category headings
const headings = Array.from(container.querySelectorAll("h3"));
const softwareHeading = headings.find((h3) =>
h3.textContent?.includes("Software"),
fc.array(fc.boolean(), { minLength: 1, maxLength: 8 }),
(featuredFlags) => {
const experiences = featuredFlags.map((featured, index) =>
makeExperience(index, featured),
);
const hardwareHeading = headings.find((h3) =>
h3.textContent?.includes("Hardware"),
const { container, unmount } = render(
<Experience experiences={experiences} />,
);
const articles = Array.from(container.querySelectorAll("article"));
const expectedCount = featuredFlags.filter(Boolean).length;
// 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(<Experience />);
const headings = Array.from(container.querySelectorAll("h3"));
const softwareHeading = headings.find((h3) =>
h3.textContent?.includes("Software"),
expect(articles).toHaveLength(expectedCount);
const articleText = articles.map(
(article) => article.textContent ?? "",
);
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(<Experience />);
// 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),
featuredFlags.forEach((featured, index) => {
const title = `Experience ${index}`;
expect(articleText.some((text) => text.includes(title))).toBe(
featured,
);
});
// Check that all company names are present
const allCompaniesPresent = allExperiences.every((exp) => {
return container.textContent?.includes(exp.company);
});
return allTitlesPresent && allCompaniesPresent;
unmount();
},
),
{ numRuns: 100 },
{ numRuns: 25 },
);
});
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,
});
it("renders an empty experience list safely", () => {
const { container } = render(<Experience experiences={[]} />);
const { container } = render(<Experience />);
// 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(<Experience />);
// 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(<Experience />);
// 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 },
);
expect(container.querySelector("section")).toBeInTheDocument();
expect(container.querySelectorAll("article")).toHaveLength(0);
});
});