diff --git a/frontend/__tests__/unit/components/LogoCarousel.test.tsx b/frontend/__tests__/unit/components/LogoCarousel.test.tsx index 964334de99..496dd0d1ed 100644 --- a/frontend/__tests__/unit/components/LogoCarousel.test.tsx +++ b/frontend/__tests__/unit/components/LogoCarousel.test.tsx @@ -29,16 +29,27 @@ jest.mock('next/image', () => { return function MockImage({ src, alt, - style, - fill, + className, + width, + height, }: { src: string alt: string - style?: React.CSSProperties - fill?: boolean + className?: string + width?: number + height?: number }) { - // eslint-disable-next-line @next/next/no-img-element - return {alt} + return ( + // eslint-disable-next-line @next/next/no-img-element -- mock for unit tests + {alt} + ) } }) @@ -156,20 +167,20 @@ describe('MovingLogos (LogoCarousel)', () => { render() const scroller = document.querySelector('.animate-scroll') - expect(scroller).toHaveStyle('animation-duration: 6s') + expect(scroller).toHaveStyle('animation-duration: 9s') }) it('updates animation duration when sponsors change', () => { const { rerender } = render() let scroller = document.querySelector('.animate-scroll') - expect(scroller).toHaveStyle('animation-duration: 6s') + expect(scroller).toHaveStyle('animation-duration: 9s') const newSponsors = [...mockSponsors, ...mockSponsors] rerender() scroller = document.querySelector('.animate-scroll') - expect(scroller).toHaveStyle('animation-duration: 12s') + expect(scroller).toHaveStyle('animation-duration: 18s') }) }) @@ -260,7 +271,7 @@ describe('MovingLogos (LogoCarousel)', () => { const scroller = document.querySelector('.animate-scroll') expect(scroller).toBeInTheDocument() - expect(scroller).toHaveClass('animate-scroll', 'flex', 'w-full', 'gap-6') + expect(scroller).toHaveClass('animate-scroll', 'flex', 'w-max', 'gap-6') }) }) @@ -284,9 +295,11 @@ describe('MovingLogos (LogoCarousel)', () => { it('provides fallback for empty imageUrl', () => { render() - const imageContainer = document.querySelector('.relative.mb-4') - expect(imageContainer).toBeInTheDocument() - expect(imageContainer?.querySelector('img')).not.toBeInTheDocument() + const images = screen.queryAllByTestId('sponsor-image') + expect(images).toHaveLength(0) + + const fallbackText = screen.getAllByText('No Image Sponsor') + expect(fallbackText.length).toBeGreaterThan(0) }) it('uses generic fallback alt text when sponsor name is missing', () => { @@ -426,7 +439,7 @@ describe('MovingLogos (LogoCarousel)', () => { expect(screen.getAllByTestId('sponsor-image')).toHaveLength(200) const scroller = document.querySelector('.animate-scroll') - expect(scroller).toHaveStyle('animation-duration: 200s') + expect(scroller).toHaveStyle('animation-duration: 300s') }) }) @@ -482,11 +495,8 @@ describe('MovingLogos (LogoCarousel)', () => { const overflowContainer = document.querySelector('.relative.overflow-hidden.py-2') expect(overflowContainer).toBeInTheDocument() - const scroller = document.querySelector('.animate-scroll.flex.w-full.gap-6') + const scroller = document.querySelector('.animate-scroll.flex.w-max.gap-6') expect(scroller).toBeInTheDocument() - - const sponsorContainers = document.querySelectorAll('[class*="min-w-[220px]"]') - expect(sponsorContainers).toHaveLength(6) }) it('applies correct styles to images', () => { @@ -494,8 +504,7 @@ describe('MovingLogos (LogoCarousel)', () => { const images = screen.getAllByTestId('sponsor-image') for (const image of images) { - expect(image).toHaveAttribute('style', 'object-fit: contain;') - expect(image).toHaveAttribute('data-fill', 'true') + expect(image).toHaveClass('h-full', 'w-full', 'object-contain') } }) @@ -508,14 +517,11 @@ describe('MovingLogos (LogoCarousel)', () => { const scroller = overflowContainer?.querySelector('.animate-scroll') expect(scroller).toBeInTheDocument() - const sponsorContainer = scroller?.querySelector('[class*="min-w-[220px]"]') - expect(sponsorContainer).toBeInTheDocument() - - const link = sponsorContainer?.querySelector('a') + const link = scroller?.querySelector('a') expect(link).toBeInTheDocument() - const imageContainer = link?.querySelector('.relative.mb-4') - expect(imageContainer).toBeInTheDocument() + const logoWrapper = link?.querySelector('.bg-white.rounded-lg.shadow-md') + expect(logoWrapper).toBeInTheDocument() }) it('applies correct footer styling', () => { @@ -547,14 +553,14 @@ describe('MovingLogos (LogoCarousel)', () => { expect(donateLink).toHaveClass('text-primary', 'font-medium', 'hover:underline') }) - it('sets correct minimum width for sponsor containers', () => { + it('wraps logos in white containers for dark mode visibility', () => { render() - const sponsorContainers = document.querySelectorAll('[class*="min-w-[220px]"]') - expect(sponsorContainers).toHaveLength(6) + const logoWrappers = document.querySelectorAll('.bg-white.rounded-lg.shadow-md') + expect(logoWrappers).toHaveLength(6) - for (const container of sponsorContainers) { - expect(container).toHaveClass('min-w-[220px]') + for (const wrapper of logoWrappers) { + expect(wrapper).toHaveClass('bg-white', 'rounded-lg', 'shadow-md') } }) }) diff --git a/frontend/src/components/LogoCarousel.tsx b/frontend/src/components/LogoCarousel.tsx index b6490da097..396bcd281c 100644 --- a/frontend/src/components/LogoCarousel.tsx +++ b/frontend/src/components/LogoCarousel.tsx @@ -5,38 +5,43 @@ import type { Sponsor } from 'types/home' interface MovingLogosProps { readonly sponsors: Sponsor[] } - +/** + * Corporate Supporters Carousel Component + * + * Renders an infinite scrolling marquee of corporate supporter logos. + * + * Key Features: + * - **Infinite Loop**: Uses CSS keyframes to scroll continuously. + * - **Dark Mode Support**: Wraps logos in white cards to ensure visibility. + * - **Normalization**: Enforces consistent sizing for all logos. + * + * @returns {JSX.Element} The rendered logo carousel component. + */ export default function MovingLogos({ sponsors }: Readonly) { - // Keep duration behavior stable even when sponsors is empty. - const animationDurationSeconds = Math.max(sponsors.length, 1) * 2 + const animationDuration = `${Math.max(sponsors.length, 1) * 3}s` const renderSponsorCard = (sponsor: Sponsor, index: number, keySuffix: string) => { - // Include `index` to guarantee uniqueness even if `sponsor.id` repeats (e.g. in tests). const keyBase = sponsor.id ? `${sponsor.id}-${index}` : `${sponsor.url || sponsor.name || 'sponsor'}-${index}` return (
- -
+ +
{sponsor.imageUrl ? ( {sponsor.name ) : ( - {sponsor.name} + {sponsor.name || 'Sponsor'} )}
@@ -46,13 +51,13 @@ export default function MovingLogos({ sponsors }: Readonly) { return (
-
+
- {sponsors.map((sponsor, index) => renderSponsorCard(sponsor, index, ''))} - {sponsors.map((sponsor, index) => renderSponsorCard(sponsor, index, 'loop'))} + {sponsors.map((sponsor, index) => renderSponsorCard(sponsor, index, 'a'))} + {sponsors.map((sponsor, index) => renderSponsorCard(sponsor, index, 'b'))}
diff --git a/frontend/tailwind.config.mjs b/frontend/tailwind.config.mjs index b3d807313e..35ff2852c5 100644 --- a/frontend/tailwind.config.mjs +++ b/frontend/tailwind.config.mjs @@ -58,11 +58,11 @@ export default { keyframes: { scroll: { '0%': { transform: 'translateX(0)' }, - '100%': { transform: 'translateX(-500%)' }, + '100%': { transform: 'translateX(-50%)' }, }, }, animation: { - scroll: 'scroll 0.5s linear infinite', + scroll: 'scroll 30s linear infinite', }, }, },