- 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.
345 lines
11 KiB
TypeScript
345 lines
11 KiB
TypeScript
import { render } from "@testing-library/react";
|
|
import CompanyLogos from "./CompanyLogos";
|
|
import fc from "fast-check";
|
|
|
|
// 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("Property 4: Reduced Motion Respect", () => {
|
|
afterEach(() => {
|
|
mockReducedMotion = false;
|
|
});
|
|
|
|
it("should render static grid layout when reduced motion is enabled", () => {
|
|
/**
|
|
* **Validates: Requirements 3.6, 6.3**
|
|
*
|
|
* Property: For any reduced motion preference, when useReducedMotion() returns true,
|
|
* the CompanyLogos component should render a static grid layout without motion
|
|
* animations. This ensures accessibility compliance with prefers-reduced-motion.
|
|
*/
|
|
fc.assert(
|
|
fc.property(
|
|
fc.boolean(), // Random reduced motion preference
|
|
(shouldReduce) => {
|
|
mockReducedMotion = shouldReduce;
|
|
|
|
const { container } = render(<CompanyLogos />);
|
|
|
|
// Check for animated motion.div with animation props
|
|
const animatedElements = container.querySelectorAll("[data-animate]");
|
|
|
|
// Check for static grid layout
|
|
const staticGrid = container.querySelector(".grid");
|
|
|
|
if (shouldReduce) {
|
|
// When reduced motion is enabled:
|
|
// - Should NOT have animated elements
|
|
// - Should have static grid layout
|
|
return animatedElements.length === 0 && staticGrid !== null;
|
|
} else {
|
|
// When reduced motion is disabled:
|
|
// - Should have animated elements
|
|
// - Should NOT have static grid layout
|
|
return animatedElements.length > 0 && staticGrid === null;
|
|
}
|
|
},
|
|
),
|
|
{ numRuns: 100 },
|
|
);
|
|
});
|
|
|
|
it("should exclude horizontal scroll animation when reduced motion is enabled", () => {
|
|
/**
|
|
* **Validates: Requirements 3.6, 6.3**
|
|
*
|
|
* Property: For any component render with reduced motion enabled, the horizontal
|
|
* scroll animation (x: [0, '-50%']) should be completely absent. This verifies
|
|
* that motion animations are properly disabled.
|
|
*/
|
|
fc.assert(
|
|
fc.property(
|
|
fc.constant(null), // No random input needed
|
|
() => {
|
|
mockReducedMotion = true;
|
|
|
|
const { container } = render(<CompanyLogos />);
|
|
|
|
// Check for animation props
|
|
const animatedElements = container.querySelectorAll("[data-animate]");
|
|
|
|
let hasScrollAnimation = false;
|
|
|
|
animatedElements.forEach((element) => {
|
|
const animateValue = element.getAttribute("data-animate");
|
|
if (animateValue && animateValue !== "undefined") {
|
|
try {
|
|
const props = JSON.parse(animateValue);
|
|
|
|
// Check for x animation (horizontal scroll)
|
|
if (props.x !== undefined) {
|
|
hasScrollAnimation = true;
|
|
}
|
|
} catch (e) {
|
|
// Skip invalid JSON
|
|
}
|
|
}
|
|
});
|
|
|
|
// Should not have scroll animation when reduced motion is enabled
|
|
return !hasScrollAnimation;
|
|
},
|
|
),
|
|
{ numRuns: 100 },
|
|
);
|
|
});
|
|
|
|
it("should display all company logos in both animated and static modes", () => {
|
|
/**
|
|
* **Validates: Requirements 3.6, 6.3**
|
|
*
|
|
* Property: For any reduced motion preference, all company logos should be
|
|
* displayed. The reduced motion preference should only affect the animation
|
|
* style, not the content visibility.
|
|
*/
|
|
fc.assert(
|
|
fc.property(
|
|
fc.boolean(), // Random reduced motion preference
|
|
(shouldReduce) => {
|
|
mockReducedMotion = shouldReduce;
|
|
|
|
const { container } = render(<CompanyLogos />);
|
|
|
|
// Find all logo images
|
|
const logoImages = container.querySelectorAll('img[alt*="logo"]');
|
|
|
|
// Should have at least 5 logos (the original set)
|
|
// In animated mode, we have duplicates, so it will be 10
|
|
// In static mode, we have exactly 5
|
|
if (shouldReduce) {
|
|
return logoImages.length === 5;
|
|
} else {
|
|
return logoImages.length === 10; // Duplicated for infinite scroll
|
|
}
|
|
},
|
|
),
|
|
{ numRuns: 100 },
|
|
);
|
|
});
|
|
|
|
it("should maintain accessibility attributes regardless of reduced motion preference", () => {
|
|
/**
|
|
* **Validates: Requirements 3.6, 6.3, 6.2**
|
|
*
|
|
* Property: For any reduced motion preference, all company logo images should
|
|
* maintain proper alt text attributes. Accessibility should not be compromised
|
|
* by the animation mode.
|
|
*/
|
|
fc.assert(
|
|
fc.property(
|
|
fc.boolean(), // Random reduced motion preference
|
|
(shouldReduce) => {
|
|
mockReducedMotion = shouldReduce;
|
|
|
|
const { container } = render(<CompanyLogos />);
|
|
|
|
// Find all logo images
|
|
const logoImages = container.querySelectorAll("img");
|
|
|
|
// Verify we found images
|
|
if (logoImages.length === 0) {
|
|
return false;
|
|
}
|
|
|
|
// Check that all images have non-empty alt text
|
|
return Array.from(logoImages).every((img) => {
|
|
const alt = img.getAttribute("alt");
|
|
return alt !== null && alt.trim().length > 0;
|
|
});
|
|
},
|
|
),
|
|
{ numRuns: 100 },
|
|
);
|
|
});
|
|
|
|
it("should consistently apply reduced motion across multiple renders", () => {
|
|
/**
|
|
* **Validates: Requirements 3.6, 6.3**
|
|
*
|
|
* Property: For any number of component renders with reduced motion enabled,
|
|
* the static layout should be consistently rendered without animations.
|
|
* This verifies that the reduced motion behavior is stable across re-renders.
|
|
*/
|
|
fc.assert(
|
|
fc.property(
|
|
fc.integer({ min: 1, max: 5 }), // Test 1-5 renders
|
|
(numRenders) => {
|
|
mockReducedMotion = true;
|
|
|
|
let allRendersValid = true;
|
|
|
|
for (let i = 0; i < numRenders; i++) {
|
|
const { container, unmount } = render(<CompanyLogos />);
|
|
|
|
// Check for animated elements
|
|
const animatedElements =
|
|
container.querySelectorAll("[data-animate]");
|
|
|
|
// Check for static grid
|
|
const staticGrid = container.querySelector(".grid");
|
|
|
|
// Should have no animations and should have static grid
|
|
if (animatedElements.length > 0 || staticGrid === null) {
|
|
allRendersValid = false;
|
|
}
|
|
|
|
unmount();
|
|
}
|
|
|
|
return allRendersValid;
|
|
},
|
|
),
|
|
{ numRuns: 100 },
|
|
);
|
|
});
|
|
|
|
it("should use grid layout classes when reduced motion is enabled", () => {
|
|
/**
|
|
* **Validates: Requirements 3.6, 6.3**
|
|
*
|
|
* Property: When reduced motion is enabled, the component should use CSS grid
|
|
* layout classes (grid, grid-cols-*) to display logos in a static arrangement.
|
|
* This ensures proper visual presentation without animation.
|
|
*/
|
|
fc.assert(
|
|
fc.property(fc.constant(null), () => {
|
|
mockReducedMotion = true;
|
|
|
|
const { container } = render(<CompanyLogos />);
|
|
|
|
// Check for grid layout classes
|
|
const gridContainer = container.querySelector(".grid");
|
|
|
|
if (!gridContainer) {
|
|
return false;
|
|
}
|
|
|
|
// Verify grid has responsive column classes
|
|
const hasGridCols = gridContainer.className.includes("grid-cols-");
|
|
|
|
return hasGridCols;
|
|
}),
|
|
{ numRuns: 100 },
|
|
);
|
|
});
|
|
|
|
it("should not have infinite repeat transition when reduced motion is enabled", () => {
|
|
/**
|
|
* **Validates: Requirements 3.6, 6.3**
|
|
*
|
|
* Property: When reduced motion is enabled, the component should not have
|
|
* transition configurations with infinite repeat. This ensures no continuous
|
|
* motion occurs when users prefer reduced motion.
|
|
*/
|
|
fc.assert(
|
|
fc.property(fc.constant(null), () => {
|
|
mockReducedMotion = true;
|
|
|
|
const { container } = render(<CompanyLogos />);
|
|
|
|
// Check for transition props
|
|
const elementsWithTransition =
|
|
container.querySelectorAll("[data-transition]");
|
|
|
|
let hasInfiniteRepeat = false;
|
|
|
|
elementsWithTransition.forEach((element) => {
|
|
const transitionValue = element.getAttribute("data-transition");
|
|
if (transitionValue && transitionValue !== "undefined") {
|
|
try {
|
|
const props = JSON.parse(transitionValue);
|
|
|
|
// Check for infinite repeat
|
|
if (props.repeat === Infinity || props.repeat === "Infinity") {
|
|
hasInfiniteRepeat = true;
|
|
}
|
|
} catch (e) {
|
|
// Skip invalid JSON
|
|
}
|
|
}
|
|
});
|
|
|
|
// Should not have infinite repeat when reduced motion is enabled
|
|
return !hasInfiniteRepeat;
|
|
}),
|
|
{ numRuns: 100 },
|
|
);
|
|
});
|
|
|
|
it("should switch between animated and static modes based on reduced motion preference", () => {
|
|
/**
|
|
* **Validates: Requirements 3.6, 6.3**
|
|
*
|
|
* Property: The component should render different layouts based on the reduced
|
|
* motion preference. When the preference changes, the component should adapt
|
|
* accordingly. This verifies the conditional rendering logic.
|
|
*/
|
|
fc.assert(
|
|
fc.property(
|
|
fc.array(fc.boolean(), { minLength: 2, maxLength: 5 }), // Sequence of preference changes
|
|
(preferenceSequence) => {
|
|
let allTransitionsValid = true;
|
|
|
|
preferenceSequence.forEach((shouldReduce) => {
|
|
mockReducedMotion = shouldReduce;
|
|
|
|
const { container, unmount } = render(<CompanyLogos />);
|
|
|
|
const hasAnimations =
|
|
container.querySelectorAll("[data-animate]").length > 0;
|
|
const hasStaticGrid = container.querySelector(".grid") !== null;
|
|
|
|
// Verify correct mode is rendered
|
|
if (shouldReduce) {
|
|
if (hasAnimations || !hasStaticGrid) {
|
|
allTransitionsValid = false;
|
|
}
|
|
} else {
|
|
if (!hasAnimations || hasStaticGrid) {
|
|
allTransitionsValid = false;
|
|
}
|
|
}
|
|
|
|
unmount();
|
|
});
|
|
|
|
return allTransitionsValid;
|
|
},
|
|
),
|
|
{ numRuns: 100 },
|
|
);
|
|
});
|
|
});
|