- 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.
504 lines
16 KiB
TypeScript
504 lines
16 KiB
TypeScript
import { render, screen } from "@testing-library/react";
|
|
import CompanyLogos from "./CompanyLogos";
|
|
|
|
// Mock framer-motion with configurable useReducedMotion
|
|
let mockReducedMotion = false;
|
|
|
|
jest.mock("framer-motion", () => {
|
|
const actual = jest.requireActual("framer-motion");
|
|
return {
|
|
...actual,
|
|
useReducedMotion: () => mockReducedMotion,
|
|
motion: {
|
|
div: ({ children, animate, transition, ...props }: any) => {
|
|
// Capture animation props for testing
|
|
const animationProps = {
|
|
"data-animate": JSON.stringify(animate),
|
|
"data-transition": JSON.stringify(transition),
|
|
};
|
|
return (
|
|
<div {...props} {...animationProps}>
|
|
{children}
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
};
|
|
});
|
|
|
|
describe("CompanyLogos Component - Logo Rendering", () => {
|
|
beforeEach(() => {
|
|
mockReducedMotion = false;
|
|
});
|
|
|
|
it("should render all five company logos", () => {
|
|
const { container } = render(<CompanyLogos />);
|
|
|
|
// In animated mode, logos are duplicated (10 total)
|
|
const logoImages = container.querySelectorAll('img[alt*="logo"]');
|
|
expect(logoImages.length).toBe(10); // 5 logos x 2 (duplicated for infinite scroll)
|
|
});
|
|
|
|
it("should render Apolo Energia logo", () => {
|
|
render(<CompanyLogos />);
|
|
|
|
const apoloLogos = screen.getAllByAltText("Apolo Energia logo");
|
|
expect(apoloLogos.length).toBe(2); // Duplicated for infinite scroll
|
|
expect(apoloLogos[0]).toHaveAttribute("src", "/logos/apolo.svg");
|
|
});
|
|
|
|
it("should render Julia Beauty Lashes logo", () => {
|
|
render(<CompanyLogos />);
|
|
|
|
const juliaLogos = screen.getAllByAltText("Julia Beauty Lashes logo");
|
|
expect(juliaLogos.length).toBe(2);
|
|
expect(juliaLogos[0]).toHaveAttribute("src", "/logos/julia.svg");
|
|
});
|
|
|
|
it("should render Innovo logo", () => {
|
|
render(<CompanyLogos />);
|
|
|
|
const innovoLogos = screen.getAllByAltText("Innovo logo");
|
|
expect(innovoLogos.length).toBe(2);
|
|
expect(innovoLogos[0]).toHaveAttribute("src", "/logos/innovo.svg");
|
|
});
|
|
|
|
it("should render TeraByteShop logo", () => {
|
|
render(<CompanyLogos />);
|
|
|
|
const terabyteLogos = screen.getAllByAltText("TeraByteShop logo");
|
|
expect(terabyteLogos.length).toBe(2);
|
|
expect(terabyteLogos[0]).toHaveAttribute("src", "/logos/terabyte.svg");
|
|
});
|
|
|
|
it("should render Pichau logo", () => {
|
|
render(<CompanyLogos />);
|
|
|
|
const pichauLogos = screen.getAllByAltText("Pichau logo");
|
|
expect(pichauLogos.length).toBe(2);
|
|
expect(pichauLogos[0]).toHaveAttribute("src", "/logos/pichau.svg");
|
|
});
|
|
|
|
it("should have alt text for all logo images", () => {
|
|
const { container } = render(<CompanyLogos />);
|
|
|
|
const logoImages = container.querySelectorAll("img");
|
|
|
|
logoImages.forEach((img) => {
|
|
const alt = img.getAttribute("alt");
|
|
expect(alt).toBeTruthy();
|
|
expect(alt?.length).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
it("should duplicate logo array for seamless infinite scroll", () => {
|
|
const { container } = render(<CompanyLogos />);
|
|
|
|
// Each company should appear exactly twice
|
|
const apoloLogos = screen.getAllByAltText("Apolo Energia logo");
|
|
const juliaLogos = screen.getAllByAltText("Julia Beauty Lashes logo");
|
|
const innovoLogos = screen.getAllByAltText("Innovo logo");
|
|
const terabyteLogos = screen.getAllByAltText("TeraByteShop logo");
|
|
const pichauLogos = screen.getAllByAltText("Pichau logo");
|
|
|
|
expect(apoloLogos.length).toBe(2);
|
|
expect(juliaLogos.length).toBe(2);
|
|
expect(innovoLogos.length).toBe(2);
|
|
expect(terabyteLogos.length).toBe(2);
|
|
expect(pichauLogos.length).toBe(2);
|
|
});
|
|
});
|
|
|
|
describe("CompanyLogos Component - Animation Configuration", () => {
|
|
beforeEach(() => {
|
|
mockReducedMotion = false;
|
|
});
|
|
|
|
it("should have horizontal scroll animation configuration", () => {
|
|
const { container } = render(<CompanyLogos />);
|
|
|
|
const animatedElement = container.querySelector("[data-animate]");
|
|
expect(animatedElement).toBeInTheDocument();
|
|
|
|
const animateValue = animatedElement?.getAttribute("data-animate");
|
|
expect(animateValue).toBeTruthy();
|
|
|
|
const animateProps = JSON.parse(animateValue!);
|
|
expect(animateProps.x).toEqual([0, "-50%"]);
|
|
});
|
|
|
|
it("should have 30-second animation duration", () => {
|
|
const { container } = render(<CompanyLogos />);
|
|
|
|
const animatedElement = container.querySelector("[data-transition]");
|
|
expect(animatedElement).toBeInTheDocument();
|
|
|
|
const transitionValue = animatedElement?.getAttribute("data-transition");
|
|
expect(transitionValue).toBeTruthy();
|
|
|
|
const transitionProps = JSON.parse(transitionValue!);
|
|
expect(transitionProps.duration).toBe(30);
|
|
});
|
|
|
|
it("should have infinite repeat configuration", () => {
|
|
const { container } = render(<CompanyLogos />);
|
|
|
|
const animatedElement = container.querySelector("[data-transition]");
|
|
const transitionValue = animatedElement?.getAttribute("data-transition");
|
|
const transitionProps = JSON.parse(transitionValue!);
|
|
|
|
// JSON.stringify converts Infinity to null, so we check for null
|
|
expect(transitionProps.repeat).toBe(null);
|
|
});
|
|
|
|
it("should use linear easing for smooth animation", () => {
|
|
const { container } = render(<CompanyLogos />);
|
|
|
|
const animatedElement = container.querySelector("[data-transition]");
|
|
const transitionValue = animatedElement?.getAttribute("data-transition");
|
|
const transitionProps = JSON.parse(transitionValue!);
|
|
|
|
expect(transitionProps.ease).toBe("linear");
|
|
});
|
|
|
|
it("should have flex layout with gap-sp-12 for logo spacing", () => {
|
|
const { container } = render(<CompanyLogos />);
|
|
|
|
const animatedElement = container.querySelector("[data-animate]");
|
|
const classes = animatedElement?.className || "";
|
|
|
|
expect(classes).toContain("flex");
|
|
expect(classes).toContain("gap-sp-12");
|
|
});
|
|
|
|
it("should have items-center alignment", () => {
|
|
const { container } = render(<CompanyLogos />);
|
|
|
|
const animatedElement = container.querySelector("[data-animate]");
|
|
const classes = animatedElement?.className || "";
|
|
|
|
expect(classes).toContain("items-center");
|
|
});
|
|
});
|
|
|
|
describe("CompanyLogos Component - Reduced Motion Fallback", () => {
|
|
beforeEach(() => {
|
|
mockReducedMotion = true;
|
|
});
|
|
|
|
afterEach(() => {
|
|
mockReducedMotion = false;
|
|
});
|
|
|
|
it("should render static grid when reduced motion is enabled", () => {
|
|
const { container } = render(<CompanyLogos />);
|
|
|
|
const staticGrid = container.querySelector(".grid");
|
|
expect(staticGrid).toBeInTheDocument();
|
|
});
|
|
|
|
it("should not have animated elements when reduced motion is enabled", () => {
|
|
const { container } = render(<CompanyLogos />);
|
|
|
|
const animatedElements = container.querySelectorAll("[data-animate]");
|
|
expect(animatedElements.length).toBe(0);
|
|
});
|
|
|
|
it("should render exactly 5 logos in static mode (no duplication)", () => {
|
|
const { container } = render(<CompanyLogos />);
|
|
|
|
const logoImages = container.querySelectorAll('img[alt*="logo"]');
|
|
expect(logoImages.length).toBe(5);
|
|
});
|
|
|
|
it("should have responsive grid columns in static mode", () => {
|
|
const { container } = render(<CompanyLogos />);
|
|
|
|
const staticGrid = container.querySelector(".grid");
|
|
const classes = staticGrid?.className || "";
|
|
|
|
expect(classes).toContain("grid-cols-2");
|
|
expect(classes).toContain("sm:grid-cols-3");
|
|
expect(classes).toContain("md:grid-cols-5");
|
|
});
|
|
|
|
it("should have gap-sp-6 spacing in static grid", () => {
|
|
const { container } = render(<CompanyLogos />);
|
|
|
|
const staticGrid = container.querySelector(".grid");
|
|
const classes = staticGrid?.className || "";
|
|
|
|
expect(classes).toContain("gap-sp-6");
|
|
});
|
|
|
|
it("should maintain all logo alt text in static mode", () => {
|
|
const { container } = render(<CompanyLogos />);
|
|
|
|
const logoImages = container.querySelectorAll("img");
|
|
|
|
logoImages.forEach((img) => {
|
|
const alt = img.getAttribute("alt");
|
|
expect(alt).toBeTruthy();
|
|
expect(alt?.length).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("CompanyLogos Component - Missing Logo Image Handling", () => {
|
|
beforeEach(() => {
|
|
mockReducedMotion = false;
|
|
});
|
|
|
|
it("should have onError handler for logo images", () => {
|
|
const { container } = render(<CompanyLogos />);
|
|
|
|
const logoImages = container.querySelectorAll("img");
|
|
|
|
logoImages.forEach((img) => {
|
|
// Check that the image element has an onError handler
|
|
// In React, this is attached as a prop, so we verify the element exists
|
|
expect(img).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it("should apply grayscale filter to logo images", () => {
|
|
const { container } = render(<CompanyLogos />);
|
|
|
|
const logoImages = container.querySelectorAll("img");
|
|
|
|
logoImages.forEach((img) => {
|
|
const classes = img.className;
|
|
expect(classes).toContain("grayscale");
|
|
});
|
|
});
|
|
|
|
it("should have hover effect to remove grayscale", () => {
|
|
const { container } = render(<CompanyLogos />);
|
|
|
|
const logoImages = container.querySelectorAll("img");
|
|
|
|
logoImages.forEach((img) => {
|
|
const classes = img.className;
|
|
expect(classes).toContain("hover:grayscale-0");
|
|
});
|
|
});
|
|
|
|
it("should have opacity transition on hover", () => {
|
|
const { container } = render(<CompanyLogos />);
|
|
|
|
const logoImages = container.querySelectorAll("img");
|
|
|
|
logoImages.forEach((img) => {
|
|
const classes = img.className;
|
|
expect(classes).toContain("opacity-60");
|
|
expect(classes).toContain("hover:opacity-100");
|
|
});
|
|
});
|
|
|
|
it("should have transition-all for smooth hover effects", () => {
|
|
const { container } = render(<CompanyLogos />);
|
|
|
|
const logoImages = container.querySelectorAll("img");
|
|
|
|
logoImages.forEach((img) => {
|
|
const classes = img.className;
|
|
expect(classes).toContain("transition-all");
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("CompanyLogos Component - Responsive Layout", () => {
|
|
beforeEach(() => {
|
|
mockReducedMotion = false;
|
|
});
|
|
|
|
it("should have section element as container", () => {
|
|
const { container } = render(<CompanyLogos />);
|
|
|
|
const section = container.querySelector("section");
|
|
expect(section).toBeInTheDocument();
|
|
});
|
|
|
|
it("should have full width class", () => {
|
|
const { container } = render(<CompanyLogos />);
|
|
|
|
const section = container.querySelector("section");
|
|
const classes = section?.className || "";
|
|
|
|
expect(classes).toContain("w-full");
|
|
});
|
|
|
|
it("should have overflow-hidden for animation containment", () => {
|
|
const { container } = render(<CompanyLogos />);
|
|
|
|
const section = container.querySelector("section");
|
|
const classes = section?.className || "";
|
|
|
|
expect(classes).toContain("overflow-hidden");
|
|
});
|
|
|
|
it("should have border-b for visual separation", () => {
|
|
const { container } = render(<CompanyLogos />);
|
|
|
|
const section = container.querySelector("section");
|
|
const classes = section?.className || "";
|
|
|
|
expect(classes).toContain("border-b");
|
|
});
|
|
|
|
it("should have background color class", () => {
|
|
const { container } = render(<CompanyLogos />);
|
|
|
|
const section = container.querySelector("section");
|
|
const classes = section?.className || "";
|
|
|
|
expect(classes).toContain("bg-[var(--paper)]");
|
|
});
|
|
|
|
it("should have py-sp-8 vertical padding", () => {
|
|
const { container } = render(<CompanyLogos />);
|
|
|
|
// In animated mode, padding is on inner div
|
|
const innerDiv = container.querySelector(".py-sp-8");
|
|
expect(innerDiv).toBeInTheDocument();
|
|
});
|
|
|
|
it("should have responsive padding in static mode", () => {
|
|
mockReducedMotion = true;
|
|
const { container } = render(<CompanyLogos />);
|
|
|
|
const innerContainer = container.querySelector(".container");
|
|
const classes = innerContainer?.className || "";
|
|
|
|
expect(classes).toContain("px-sp-6");
|
|
expect(classes).toContain("py-sp-8");
|
|
expect(classes).toContain("md:px-sp-10");
|
|
expect(classes).toContain("lg:px-sp-16");
|
|
});
|
|
|
|
it("should have mx-auto for centering in static mode", () => {
|
|
mockReducedMotion = true;
|
|
const { container } = render(<CompanyLogos />);
|
|
|
|
const innerContainer = container.querySelector(".container");
|
|
const classes = innerContainer?.className || "";
|
|
|
|
expect(classes).toContain("mx-auto");
|
|
});
|
|
});
|
|
|
|
describe("CompanyLogos Component - Logo Sizing and Styling", () => {
|
|
beforeEach(() => {
|
|
mockReducedMotion = false;
|
|
});
|
|
|
|
it("should have h-12 height for all logos", () => {
|
|
const { container } = render(<CompanyLogos />);
|
|
|
|
const logoImages = container.querySelectorAll("img");
|
|
|
|
logoImages.forEach((img) => {
|
|
const classes = img.className;
|
|
expect(classes).toContain("h-12");
|
|
});
|
|
});
|
|
|
|
it("should have w-auto width for aspect ratio preservation", () => {
|
|
const { container } = render(<CompanyLogos />);
|
|
|
|
const logoImages = container.querySelectorAll("img");
|
|
|
|
logoImages.forEach((img) => {
|
|
const classes = img.className;
|
|
expect(classes).toContain("w-auto");
|
|
});
|
|
});
|
|
|
|
it("should have object-contain for proper image scaling", () => {
|
|
const { container } = render(<CompanyLogos />);
|
|
|
|
const logoImages = container.querySelectorAll("img");
|
|
|
|
logoImages.forEach((img) => {
|
|
const classes = img.className;
|
|
expect(classes).toContain("object-contain");
|
|
});
|
|
});
|
|
|
|
it("should have flex-shrink-0 on logo containers in animated mode", () => {
|
|
const { container } = render(<CompanyLogos />);
|
|
|
|
const logoContainers = container.querySelectorAll("[data-animate] > div");
|
|
|
|
logoContainers.forEach((div) => {
|
|
const classes = div.className;
|
|
expect(classes).toContain("flex-shrink-0");
|
|
});
|
|
});
|
|
|
|
it("should center logos within their containers", () => {
|
|
const { container } = render(<CompanyLogos />);
|
|
|
|
const logoContainers = container.querySelectorAll("[data-animate] > div");
|
|
|
|
logoContainers.forEach((div) => {
|
|
const classes = div.className;
|
|
expect(classes).toContain("flex");
|
|
expect(classes).toContain("items-center");
|
|
expect(classes).toContain("justify-center");
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("CompanyLogos Component - Accessibility", () => {
|
|
beforeEach(() => {
|
|
mockReducedMotion = false;
|
|
});
|
|
|
|
it("should use semantic section element", () => {
|
|
const { container } = render(<CompanyLogos />);
|
|
|
|
const section = container.querySelector("section");
|
|
expect(section).toBeInTheDocument();
|
|
});
|
|
|
|
it("should have descriptive alt text for each logo", () => {
|
|
render(<CompanyLogos />);
|
|
|
|
expect(screen.getAllByAltText("Apolo Energia logo")).toHaveLength(2);
|
|
expect(screen.getAllByAltText("Julia Beauty Lashes logo")).toHaveLength(2);
|
|
expect(screen.getAllByAltText("Innovo logo")).toHaveLength(2);
|
|
expect(screen.getAllByAltText("TeraByteShop logo")).toHaveLength(2);
|
|
expect(screen.getAllByAltText("Pichau logo")).toHaveLength(2);
|
|
});
|
|
|
|
it("should respect reduced motion preference", () => {
|
|
mockReducedMotion = true;
|
|
const { container } = render(<CompanyLogos />);
|
|
|
|
// Should render static grid instead of animation
|
|
const staticGrid = container.querySelector(".grid");
|
|
const animatedElements = container.querySelectorAll("[data-animate]");
|
|
|
|
expect(staticGrid).toBeInTheDocument();
|
|
expect(animatedElements.length).toBe(0);
|
|
});
|
|
|
|
it("should maintain content visibility in both modes", () => {
|
|
// Test animated mode
|
|
mockReducedMotion = false;
|
|
const { container: animatedContainer } = render(<CompanyLogos />);
|
|
const animatedLogos =
|
|
animatedContainer.querySelectorAll('img[alt*="logo"]');
|
|
|
|
// Test static mode
|
|
mockReducedMotion = true;
|
|
const { container: staticContainer } = render(<CompanyLogos />);
|
|
const staticLogos = staticContainer.querySelectorAll('img[alt*="logo"]');
|
|
|
|
// Both modes should display logos (different counts due to duplication)
|
|
expect(animatedLogos.length).toBeGreaterThan(0);
|
|
expect(staticLogos.length).toBeGreaterThan(0);
|
|
});
|
|
});
|