- 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.
247 lines
8.2 KiB
TypeScript
247 lines
8.2 KiB
TypeScript
import { render } from "@testing-library/react";
|
|
import Footer from "./Footer";
|
|
import fc from "fast-check";
|
|
|
|
describe("Property 2: Social Links Open in New Tab", () => {
|
|
it('should have target="_blank" and rel="noopener noreferrer" for all social links', () => {
|
|
/**
|
|
* **Validates: Requirements 2.7**
|
|
*
|
|
* Property: For any social media link in the Footer component, it should have
|
|
* target="_blank" and rel="noopener noreferrer" attributes to open in a new tab
|
|
* securely. This ensures consistent behavior across all social links and prevents
|
|
* security vulnerabilities (tabnabbing).
|
|
*/
|
|
fc.assert(
|
|
fc.property(
|
|
fc.constant(null), // No random input needed, testing static component
|
|
() => {
|
|
const { container } = render(<Footer />);
|
|
|
|
// Find all social media links (they are within the "Connect" section)
|
|
const socialLinks = container.querySelectorAll(
|
|
'a[aria-label*="Visit"]',
|
|
);
|
|
|
|
// Verify we found social links
|
|
if (socialLinks.length === 0) {
|
|
return false;
|
|
}
|
|
|
|
// Check each social link
|
|
let allLinksValid = true;
|
|
|
|
socialLinks.forEach((link) => {
|
|
const target = link.getAttribute("target");
|
|
const rel = link.getAttribute("rel");
|
|
|
|
// Both attributes must be present and correct
|
|
if (target !== "_blank" || rel !== "noopener noreferrer") {
|
|
allLinksValid = false;
|
|
}
|
|
});
|
|
|
|
return allLinksValid;
|
|
},
|
|
),
|
|
{ numRuns: 100 },
|
|
);
|
|
});
|
|
|
|
it("should consistently apply security attributes across all social link variations", () => {
|
|
/**
|
|
* **Validates: Requirements 2.7**
|
|
*
|
|
* Property: For any subset of social links rendered, each link should maintain
|
|
* the security attributes (target="_blank" and rel="noopener noreferrer").
|
|
* This property verifies that the security pattern is consistently applied
|
|
* regardless of which social platforms are included.
|
|
*/
|
|
fc.assert(
|
|
fc.property(
|
|
fc.integer({ min: 0, max: 10 }), // Random seed for variation
|
|
() => {
|
|
const { container } = render(<Footer />);
|
|
|
|
// Get all anchor tags in the footer
|
|
const allLinks = container.querySelectorAll("footer a");
|
|
|
|
// Filter to only social links (those with SVG icons and aria-labels about visiting profiles)
|
|
const socialLinks = Array.from(allLinks).filter((link) => {
|
|
const ariaLabel = link.getAttribute("aria-label");
|
|
const hasSvg = link.querySelector("svg") !== null;
|
|
return ariaLabel && ariaLabel.includes("Visit") && hasSvg;
|
|
});
|
|
|
|
// Verify we found social links
|
|
if (socialLinks.length === 0) {
|
|
return false;
|
|
}
|
|
|
|
// Check that ALL social links have the required attributes
|
|
return socialLinks.every((link) => {
|
|
const target = link.getAttribute("target");
|
|
const rel = link.getAttribute("rel");
|
|
return target === "_blank" && rel === "noopener noreferrer";
|
|
});
|
|
},
|
|
),
|
|
{ numRuns: 100 },
|
|
);
|
|
});
|
|
|
|
it("should not apply new tab attributes to non-social links", () => {
|
|
/**
|
|
* **Validates: Requirements 2.7**
|
|
*
|
|
* Property: Only social media links should have target="_blank". Other links
|
|
* in the footer (like email) should not force new tab behavior. This ensures
|
|
* the new tab behavior is intentionally applied only where appropriate.
|
|
*/
|
|
fc.assert(
|
|
fc.property(fc.constant(null), () => {
|
|
const { container } = render(<Footer />);
|
|
|
|
// Get all anchor tags in the footer
|
|
const allLinks = container.querySelectorAll("footer a");
|
|
|
|
// Separate social links from other links
|
|
const socialLinks: Element[] = [];
|
|
const otherLinks: Element[] = [];
|
|
|
|
allLinks.forEach((link) => {
|
|
const ariaLabel = link.getAttribute("aria-label");
|
|
const hasSvg = link.querySelector("svg") !== null;
|
|
|
|
if (ariaLabel && ariaLabel.includes("Visit") && hasSvg) {
|
|
socialLinks.push(link);
|
|
} else {
|
|
otherLinks.push(link);
|
|
}
|
|
});
|
|
|
|
// All social links should have target="_blank"
|
|
const allSocialLinksHaveTarget = socialLinks.every((link) => {
|
|
return link.getAttribute("target") === "_blank";
|
|
});
|
|
|
|
// Non-social links (like email) should NOT have target="_blank"
|
|
const noOtherLinksHaveTarget = otherLinks.every((link) => {
|
|
return link.getAttribute("target") !== "_blank";
|
|
});
|
|
|
|
return allSocialLinksHaveTarget && noOtherLinksHaveTarget;
|
|
}),
|
|
{ numRuns: 100 },
|
|
);
|
|
});
|
|
|
|
it("should maintain security attributes after multiple renders", () => {
|
|
/**
|
|
* **Validates: Requirements 2.7**
|
|
*
|
|
* Property: For any number of component renders, social links should consistently
|
|
* maintain their security attributes. This verifies that the attributes are not
|
|
* accidentally removed or modified during re-renders.
|
|
*/
|
|
fc.assert(
|
|
fc.property(
|
|
fc.integer({ min: 1, max: 5 }), // Test 1-5 renders
|
|
(numRenders) => {
|
|
let allRendersValid = true;
|
|
|
|
for (let i = 0; i < numRenders; i++) {
|
|
const { container, unmount } = render(<Footer />);
|
|
|
|
// Find social links
|
|
const socialLinks = container.querySelectorAll(
|
|
'a[aria-label*="Visit"]',
|
|
);
|
|
|
|
// Check each link
|
|
socialLinks.forEach((link) => {
|
|
const target = link.getAttribute("target");
|
|
const rel = link.getAttribute("rel");
|
|
|
|
if (target !== "_blank" || rel !== "noopener noreferrer") {
|
|
allRendersValid = false;
|
|
}
|
|
});
|
|
|
|
unmount();
|
|
}
|
|
|
|
return allRendersValid;
|
|
},
|
|
),
|
|
{ numRuns: 100 },
|
|
);
|
|
});
|
|
|
|
it("should have both target and rel attributes present together", () => {
|
|
/**
|
|
* **Validates: Requirements 2.7**
|
|
*
|
|
* Property: For any social link, if target="_blank" is present, then
|
|
* rel="noopener noreferrer" must also be present. These attributes work
|
|
* together to provide secure new tab behavior, and one without the other
|
|
* creates a security vulnerability.
|
|
*/
|
|
fc.assert(
|
|
fc.property(fc.constant(null), () => {
|
|
const { container } = render(<Footer />);
|
|
|
|
// Find all links with target="_blank"
|
|
const linksWithTarget = container.querySelectorAll(
|
|
'footer a[target="_blank"]',
|
|
);
|
|
|
|
// Verify we found links
|
|
if (linksWithTarget.length === 0) {
|
|
return false;
|
|
}
|
|
|
|
// Each link with target="_blank" must have rel="noopener noreferrer"
|
|
return Array.from(linksWithTarget).every((link) => {
|
|
const rel = link.getAttribute("rel");
|
|
return rel === "noopener noreferrer";
|
|
});
|
|
}),
|
|
{ numRuns: 100 },
|
|
);
|
|
});
|
|
|
|
it("should have exactly the expected number of social links with security attributes", () => {
|
|
/**
|
|
* **Validates: Requirements 2.7**
|
|
*
|
|
* Property: The Footer should render a specific number of social links (based on
|
|
* the socialLinks array), and all of them should have the security attributes.
|
|
* This ensures no links are accidentally missing the attributes.
|
|
*/
|
|
fc.assert(
|
|
fc.property(fc.constant(null), () => {
|
|
const { container } = render(<Footer />);
|
|
|
|
// Find all social links with security attributes
|
|
const secureLinks = container.querySelectorAll(
|
|
'footer a[target="_blank"][rel="noopener noreferrer"]',
|
|
);
|
|
|
|
// Find all social links (with aria-label containing "Visit")
|
|
const allSocialLinks = container.querySelectorAll(
|
|
'footer a[aria-label*="Visit"]',
|
|
);
|
|
|
|
// The number of secure links should equal the number of social links
|
|
// This ensures ALL social links have the security attributes
|
|
return (
|
|
secureLinks.length === allSocialLinks.length &&
|
|
allSocialLinks.length > 0
|
|
);
|
|
}),
|
|
{ numRuns: 100 },
|
|
);
|
|
});
|
|
});
|