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,320 @@
|
||||
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) => {
|
||||
return <div {...props}>{children}</div>;
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe("Property 10: Alt Text for Images", () => {
|
||||
afterEach(() => {
|
||||
mockReducedMotion = false;
|
||||
});
|
||||
|
||||
it("should have non-empty alt attributes for all company logo images", () => {
|
||||
/**
|
||||
* **Validates: Requirements 6.2**
|
||||
*
|
||||
* Property: For any company logo image in the CompanyLogos component,
|
||||
* it should have a non-empty alt attribute describing the company.
|
||||
* This ensures accessibility for screen readers and assistive technologies.
|
||||
*/
|
||||
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 have descriptive alt text that includes company name", () => {
|
||||
/**
|
||||
* **Validates: Requirements 6.2**
|
||||
*
|
||||
* Property: For any company logo image, the alt text should be descriptive
|
||||
* and include identifying information about the company. This ensures
|
||||
* meaningful accessibility information is provided.
|
||||
*/
|
||||
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 alt text contains "logo" or is descriptive
|
||||
return Array.from(logoImages).every((img) => {
|
||||
const alt = img.getAttribute("alt");
|
||||
if (!alt) return false;
|
||||
|
||||
const altLower = alt.toLowerCase();
|
||||
// Alt text should contain "logo" or be at least 5 characters (descriptive)
|
||||
return altLower.includes("logo") || alt.trim().length >= 5;
|
||||
});
|
||||
},
|
||||
),
|
||||
{ numRuns: 100 },
|
||||
);
|
||||
});
|
||||
|
||||
it("should maintain alt text consistency across multiple renders", () => {
|
||||
/**
|
||||
* **Validates: Requirements 6.2**
|
||||
*
|
||||
* Property: For any number of component renders, the alt text for each
|
||||
* company logo should remain consistent. This ensures stable accessibility
|
||||
* information across re-renders.
|
||||
*/
|
||||
fc.assert(
|
||||
fc.property(
|
||||
fc.integer({ min: 2, max: 5 }), // Test 2-5 renders
|
||||
fc.boolean(), // Random reduced motion preference
|
||||
(numRenders, shouldReduce) => {
|
||||
mockReducedMotion = shouldReduce;
|
||||
|
||||
let firstRenderAltTexts: string[] = [];
|
||||
|
||||
for (let i = 0; i < numRenders; i++) {
|
||||
const { container, unmount } = render(<CompanyLogos />);
|
||||
|
||||
const logoImages = container.querySelectorAll("img");
|
||||
const currentAltTexts = Array.from(logoImages).map(
|
||||
(img) => img.getAttribute("alt") || "",
|
||||
);
|
||||
|
||||
if (i === 0) {
|
||||
firstRenderAltTexts = currentAltTexts;
|
||||
} else {
|
||||
// Compare with first render
|
||||
if (currentAltTexts.length !== firstRenderAltTexts.length) {
|
||||
unmount();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check that alt texts match (accounting for duplicates in animated mode)
|
||||
const expectedCount = shouldReduce ? 5 : 10;
|
||||
if (currentAltTexts.length !== expectedCount) {
|
||||
unmount();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
unmount();
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
),
|
||||
{ numRuns: 100 },
|
||||
);
|
||||
});
|
||||
|
||||
it("should have unique alt text for each distinct company", () => {
|
||||
/**
|
||||
* **Validates: Requirements 6.2**
|
||||
*
|
||||
* Property: For any set of company logos, each distinct company should have
|
||||
* its own unique alt text. This ensures each company is properly identified
|
||||
* for accessibility purposes.
|
||||
*/
|
||||
fc.assert(
|
||||
fc.property(fc.constant(null), () => {
|
||||
mockReducedMotion = true; // Use static mode to get unique logos
|
||||
|
||||
const { container } = render(<CompanyLogos />);
|
||||
|
||||
const logoImages = container.querySelectorAll("img");
|
||||
const altTexts = Array.from(logoImages).map(
|
||||
(img) => img.getAttribute("alt") || "",
|
||||
);
|
||||
|
||||
// In static mode, we should have 5 unique alt texts
|
||||
const uniqueAltTexts = new Set(altTexts);
|
||||
|
||||
// Should have 5 unique alt texts (one per company)
|
||||
return uniqueAltTexts.size === 5 && altTexts.length === 5;
|
||||
}),
|
||||
{ numRuns: 100 },
|
||||
);
|
||||
});
|
||||
|
||||
it("should preserve alt text in both animated and static modes", () => {
|
||||
/**
|
||||
* **Validates: Requirements 6.2**
|
||||
*
|
||||
* Property: For any reduced motion preference, all company logo images
|
||||
* should maintain their alt text attributes. The animation mode should
|
||||
* not affect accessibility attributes.
|
||||
*/
|
||||
fc.assert(
|
||||
fc.property(
|
||||
fc.boolean(), // Random reduced motion preference
|
||||
(shouldReduce) => {
|
||||
mockReducedMotion = shouldReduce;
|
||||
|
||||
const { container } = render(<CompanyLogos />);
|
||||
|
||||
const logoImages = container.querySelectorAll("img");
|
||||
|
||||
// Verify all images have alt attributes
|
||||
const allHaveAlt = Array.from(logoImages).every((img) => {
|
||||
const alt = img.getAttribute("alt");
|
||||
return alt !== null && alt.trim().length > 0;
|
||||
});
|
||||
|
||||
// Verify correct number of images based on mode
|
||||
const expectedCount = shouldReduce ? 5 : 10;
|
||||
const hasCorrectCount = logoImages.length === expectedCount;
|
||||
|
||||
return allHaveAlt && hasCorrectCount;
|
||||
},
|
||||
),
|
||||
{ numRuns: 100 },
|
||||
);
|
||||
});
|
||||
|
||||
it("should not have empty or whitespace-only alt text", () => {
|
||||
/**
|
||||
* **Validates: Requirements 6.2**
|
||||
*
|
||||
* Property: For any company logo image, the alt text should not be empty
|
||||
* or consist only of whitespace. This ensures meaningful accessibility
|
||||
* information is always provided.
|
||||
*/
|
||||
fc.assert(
|
||||
fc.property(
|
||||
fc.boolean(), // Random reduced motion preference
|
||||
(shouldReduce) => {
|
||||
mockReducedMotion = shouldReduce;
|
||||
|
||||
const { container } = render(<CompanyLogos />);
|
||||
|
||||
const logoImages = container.querySelectorAll("img");
|
||||
|
||||
// Check that no image has empty or whitespace-only alt text
|
||||
return Array.from(logoImages).every((img) => {
|
||||
const alt = img.getAttribute("alt");
|
||||
|
||||
// Alt must exist and not be empty after trimming
|
||||
if (!alt || alt.trim().length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Alt should not be just whitespace
|
||||
return alt !== alt.trim() ? false : true;
|
||||
});
|
||||
},
|
||||
),
|
||||
{ numRuns: 100 },
|
||||
);
|
||||
});
|
||||
|
||||
it("should have alt text that matches the expected company names", () => {
|
||||
/**
|
||||
* **Validates: Requirements 6.2**
|
||||
*
|
||||
* Property: For any company logo image, the alt text should correspond to
|
||||
* one of the expected company names (Apolo Energia, Julia Beauty Lashes,
|
||||
* Innovo, TeraByteShop, Pichau). This ensures correct identification.
|
||||
*/
|
||||
fc.assert(
|
||||
fc.property(fc.constant(null), () => {
|
||||
mockReducedMotion = true; // Use static mode for clarity
|
||||
|
||||
const { container } = render(<CompanyLogos />);
|
||||
|
||||
const logoImages = container.querySelectorAll("img");
|
||||
const altTexts = Array.from(logoImages).map(
|
||||
(img) => img.getAttribute("alt") || "",
|
||||
);
|
||||
|
||||
// Expected company names in alt text
|
||||
const expectedCompanies = [
|
||||
"Apolo Energia",
|
||||
"Julia Beauty Lashes",
|
||||
"Innovo",
|
||||
"TeraByteShop",
|
||||
"Pichau",
|
||||
];
|
||||
|
||||
// Check that each alt text contains one of the expected company names
|
||||
return altTexts.every((alt) =>
|
||||
expectedCompanies.some((company) => alt.includes(company)),
|
||||
);
|
||||
}),
|
||||
{ numRuns: 100 },
|
||||
);
|
||||
});
|
||||
|
||||
it("should maintain alt text when images fail to load", () => {
|
||||
/**
|
||||
* **Validates: Requirements 6.2**
|
||||
*
|
||||
* Property: For any company logo image that fails to load, the alt text
|
||||
* should still be present in the DOM. This ensures accessibility is
|
||||
* maintained even when images are unavailable.
|
||||
*/
|
||||
fc.assert(
|
||||
fc.property(
|
||||
fc.boolean(), // Random reduced motion preference
|
||||
(shouldReduce) => {
|
||||
mockReducedMotion = shouldReduce;
|
||||
|
||||
const { container } = render(<CompanyLogos />);
|
||||
|
||||
const logoImages = container.querySelectorAll("img");
|
||||
|
||||
// Verify all images have alt text before any error handling
|
||||
const allHaveAltBeforeError = Array.from(logoImages).every((img) => {
|
||||
const alt = img.getAttribute("alt");
|
||||
return alt !== null && alt.trim().length > 0;
|
||||
});
|
||||
|
||||
// The alt attribute should be present regardless of load state
|
||||
return allHaveAltBeforeError;
|
||||
},
|
||||
),
|
||||
{ numRuns: 100 },
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user