- 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.
321 lines
10 KiB
TypeScript
321 lines
10 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) => {
|
|
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 },
|
|
);
|
|
});
|
|
});
|