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.
This commit is contained in:
1 parent
0ab6930f92
commit
08dd046887
76 files changed
+21682
-1197
No files matched your search
@@ -0,0 +1,344 @@
|
||||
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 },
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user