From 2bfc0b9fd0eb6e40d7c8d80454b12e79a8991305 Mon Sep 17 00:00:00 2001 From: RizWaaN3024 Date: Sun, 14 Sep 2025 01:42:02 +0530 Subject: [PATCH 1/2] test: add comprehensive LogoCarousel unit tests Add 33 unit tests covering rendering, props, events, accessibility, and edge cases. Handle component's scrolling duplication behavior and clean up code formatting. --- .../unit/components/LogoCarousel.test.tsx | 522 ++++++++++++++++++ 1 file changed, 522 insertions(+) create mode 100644 frontend/__tests__/unit/components/LogoCarousel.test.tsx diff --git a/frontend/__tests__/unit/components/LogoCarousel.test.tsx b/frontend/__tests__/unit/components/LogoCarousel.test.tsx new file mode 100644 index 0000000000..3c4be7a065 --- /dev/null +++ b/frontend/__tests__/unit/components/LogoCarousel.test.tsx @@ -0,0 +1,522 @@ +import { render, screen, fireEvent } from '@testing-library/react' +import React from 'react' +import { Sponsor } from 'types/home' +import MovingLogos from 'components/LogoCarousel' + +jest.mock('next/image', () => { + return function MockImage({ + src, + alt, + style, + fill, + }: { + src: string + alt: string + style?: React.CSSProperties + fill?: boolean + }) { + // eslint-disable-next-line @next/next/no-img-element + return {alt} + } +}) + +jest.mock('next/link', () => { + return function MockLink({ + href, + children, + target, + rel, + className, + }: { + href: string + children: React.ReactNode + target?: string + rel?: string + className?: string + }) { + return ( + + {children} + + ) + } +}) +const mockSponsors: Sponsor[] = [ + { + name: 'Test Sponsor 1', + imageUrl: 'https://example.com/logo1.png', + url: 'https://sponsor1.com', + sponsorType: 'Gold', + }, + { + name: 'Test Sponsor 2', + imageUrl: 'https://example.com/logo2.png', + url: 'https://sponsor2.com', + sponsorType: 'Silver', + }, + { + name: 'Test Sponsor 3', + imageUrl: '', + url: 'https://sponsor3.com', + sponsorType: 'Bronze', + }, +] + +const mockSponsorsWithoutImages: Sponsor[] = [ + { + name: 'No Image Sponsor', + imageUrl: '', + url: 'https://noimage.com', + sponsorType: 'Bronze', + }, +] + +const mockEmptySponsors: Sponsor[] = [] + +describe('MovingLogos (LogoCarousel)', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + describe('Basic Rendering', () => { + it('renders successfully with minimal required props', () => { + render() + + expect(screen.getAllByTestId('sponsor-link')).toHaveLength(8) + expect(screen.getAllByTestId('sponsor-image')).toHaveLength(4) + }) + + it('renders with empty sponsors array', () => { + render() + + expect(screen.getByText(/These logos represent the corporate supporters/)).toBeInTheDocument() + expect(screen.getByText(/If you're interested in sponsoring/)).toBeInTheDocument() + }) + }) + + describe('Conditional Rendering Logic', () => { + it('renders images when imageUrl is provided', () => { + render() + + const images = screen.getAllByTestId('sponsor-image') + expect(images).toHaveLength(4) + + expect(images[0]).toHaveAttribute('src', 'https://example.com/logo1.png') + expect(images[0]).toHaveAttribute('alt', 'Test Sponsor 1 logo') + expect(images[1]).toHaveAttribute('src', 'https://example.com/logo2.png') + expect(images[1]).toHaveAttribute('alt', 'Test Sponsor 2 logo') + }) + + it('renders empty div when imageUrl is not provided', () => { + render() + + const images = screen.queryAllByTestId('sponsor-image') + expect(images).toHaveLength(0) + expect(screen.getAllByTestId('sponsor-link')).toHaveLength(4) + }) + + it('handles mixed sponsors with and without images', () => { + render() + + const images = screen.getAllByTestId('sponsor-image') + expect(images).toHaveLength(4) + expect(screen.getAllByTestId('sponsor-link')).toHaveLength(8) + }) + }) + + describe('Prop-based Behavior', () => { + it('renders different sponsors based on props', () => { + const customSponsors: Sponsor[] = [ + { + name: 'Custom Sponsor', + imageUrl: 'https://custom.com/logo.png', + url: 'https://custom.com', + sponsorType: 'Platinum', + }, + ] + + render() + + const images = screen.getAllByTestId('sponsor-image') + expect(images[0]).toHaveAttribute('src', 'https://custom.com/logo.png') + + const sponsorLinks = screen + .getAllByTestId('sponsor-link') + .filter((link) => link.getAttribute('href') === 'https://custom.com') + expect(sponsorLinks).toHaveLength(2) + }) + + it('calculates animation duration based on sponsors length', () => { + render() + + const scroller = document.querySelector('.animate-scroll') + expect(scroller).toHaveStyle('animation-duration: 6s') + }) + + it('updates animation duration when sponsors change', () => { + const { rerender } = render() + + let scroller = document.querySelector('.animate-scroll') + expect(scroller).toHaveStyle('animation-duration: 6s') + + const newSponsors = [...mockSponsors, ...mockSponsors] + rerender() + + scroller = document.querySelector('.animate-scroll') + expect(scroller).toHaveStyle('animation-duration: 12s') + }) + }) + + describe('Event Handling', () => { + it('handles link clicks correctly', () => { + render() + + const links = screen.getAllByTestId('sponsor-link') + expect(links).toHaveLength(8) + + expect(links[0]).toHaveAttribute('href', 'https://sponsor1.com') + expect(links[0]).toHaveAttribute('target', '_blank') + expect(links[0]).toHaveAttribute('rel', 'noopener noreferrer') + + expect(links[1]).toHaveAttribute('href', 'https://sponsor2.com') + expect(links[1]).toHaveAttribute('target', '_blank') + expect(links[1]).toHaveAttribute('rel', 'noopener noreferrer') + }) + + it('handles footer link clicks', () => { + render() + + const supportersLink = screen.getByText('this page') + const donateLink = screen.getByText('click here') + + expect(supportersLink.closest('a')).toHaveAttribute('href', 'https://owasp.org/supporters/') + expect(supportersLink.closest('a')).toHaveAttribute('target', '_blank') + expect(supportersLink.closest('a')).toHaveAttribute('rel', 'noopener noreferrer') + + expect(donateLink.closest('a')).toHaveAttribute( + 'href', + 'https://owasp.org/donate/?reponame=www-project-nest&title=OWASP+Nest' + ) + expect(donateLink.closest('a')).toHaveAttribute('target', '_blank') + expect(donateLink.closest('a')).toHaveAttribute('rel', 'noopener noreferrer') + }) + + it('simulates click events without errors', () => { + render() + + const links = screen.getAllByTestId('sponsor-link') + + expect(() => { + fireEvent.click(links[0]) + fireEvent.click(links[1]) + }).not.toThrow() + }) + }) + + describe('State Changes and Internal Logic', () => { + it('duplicates innerHTML on mount and when sponsors change', () => { + const { rerender } = render() + + const sponsorLinks = screen + .getAllByTestId('sponsor-link') + .filter( + (link) => + link.getAttribute('href')?.includes('sponsor1.com') || + link.getAttribute('href')?.includes('sponsor2.com') || + link.getAttribute('href')?.includes('sponsor3.com') + ) + expect(sponsorLinks).toHaveLength(6) + + const newSponsors = [ + ...mockSponsors, + { name: 'New Sponsor', imageUrl: '', url: 'https://new.com', sponsorType: 'Bronze' }, + ] + rerender() + + const newSponsorLinks = screen + .getAllByTestId('sponsor-link') + .filter((link) => link.getAttribute('href')?.includes('new.com')) + expect(newSponsorLinks).toHaveLength(2) + + expect(newSponsorLinks[0]).toHaveAttribute('href', 'https://new.com') + expect(newSponsorLinks[0]).toHaveAttribute('target', '_blank') + expect(newSponsorLinks[0]).toHaveAttribute('rel', 'noopener noreferrer') + }) + + it('uses ref correctly for DOM manipulation', () => { + render() + + const scroller = document.querySelector('.animate-scroll') + expect(scroller).toBeInTheDocument() + expect(scroller).toHaveClass('animate-scroll', 'flex', 'w-full', 'gap-6') + }) + }) + + describe('Default Values and Fallbacks', () => { + it('handles sponsors with missing optional properties gracefully', () => { + const incompleteSponsors: Partial[] = [ + { + name: 'Incomplete Sponsor', + url: 'https://incomplete.com', + }, + ] + + render() + + const incompleteLinks = screen + .getAllByTestId('sponsor-link') + .filter((link) => link.getAttribute('href') === 'https://incomplete.com') + expect(incompleteLinks).toHaveLength(2) + }) + + it('provides fallback for empty imageUrl', () => { + render() + + const imageContainer = document.querySelector('.relative.mb-4') + expect(imageContainer).toBeInTheDocument() + expect(imageContainer?.querySelector('img')).not.toBeInTheDocument() + }) + }) + + describe('Text and Content Rendering', () => { + it('renders all required text content', () => { + render() + + expect(screen.getByText(/These logos represent the corporate supporters/)).toBeInTheDocument() + expect( + screen.getByText(/whose contributions fuel OWASP Foundation security initiatives/) + ).toBeInTheDocument() + expect(screen.getByText(/Visit/)).toBeInTheDocument() + expect(screen.getByText(/this page/)).toBeInTheDocument() + expect(screen.getByText(/to become a corporate supporter/)).toBeInTheDocument() + expect( + screen.getByText(/If you're interested in sponsoring the OWASP Nest project/) + ).toBeInTheDocument() + expect(screen.getByText(/❤️/)).toBeInTheDocument() + expect(screen.getByText(/click here/)).toBeInTheDocument() + }) + + it('renders sponsor names correctly in alt attributes', () => { + render() + + const images = screen.getAllByTestId('sponsor-image') + expect(images[0]).toHaveAttribute('alt', 'Test Sponsor 1 logo') + expect(images[1]).toHaveAttribute('alt', 'Test Sponsor 2 logo') + }) + + it('renders image alt text correctly', () => { + render() + + const images = screen.getAllByTestId('sponsor-image') + expect(images[0]).toHaveAttribute('alt', 'Test Sponsor 1 logo') + expect(images[1]).toHaveAttribute('alt', 'Test Sponsor 2 logo') + }) + }) + + describe('Edge Cases and Invalid Inputs', () => { + it('handles null/undefined sponsors gracefully', () => { + expect(() => render()).toThrow() + }) + + it('handles sponsors with very long names', () => { + const longNameSponsors: Sponsor[] = [ + { + name: 'A'.repeat(1000), + imageUrl: 'https://example.com/logo.png', + url: 'https://example.com', + sponsorType: 'Gold', + }, + ] + + render() + + const images = screen.getAllByTestId('sponsor-image') + expect(images[0]).toHaveAttribute('alt', 'A'.repeat(1000) + ' logo') + expect(images[1]).toHaveAttribute('alt', 'A'.repeat(1000) + ' logo') + }) + + it('handles sponsors with special characters in names', () => { + const specialCharSponsors: Sponsor[] = [ + { + name: 'Sponsor & Co. (Ltd.) - "Special" ', + imageUrl: 'https://example.com/logo.png', + url: 'https://example.com', + sponsorType: 'Gold', + }, + ] + + render() + + const images = screen.getAllByTestId('sponsor-image') + expect(images[0]).toHaveAttribute('alt', 'Sponsor & Co. (Ltd.) - "Special" logo') + expect(images[1]).toHaveAttribute('alt', 'Sponsor & Co. (Ltd.) - "Special" logo') + }) + + it('handles invalid URLs gracefully', () => { + const invalidUrlSponsors: Sponsor[] = [ + { + name: 'Invalid URL Sponsor', + imageUrl: 'not-a-valid-url', + url: 'also-not-valid', + sponsorType: 'Gold', + }, + ] + + render() + + const images = screen.getAllByTestId('sponsor-image') + expect(images[0]).toHaveAttribute('src', 'not-a-valid-url') + + const invalidLinks = screen + .getAllByTestId('sponsor-link') + .filter((link) => link.getAttribute('href') === 'also-not-valid') + expect(invalidLinks).toHaveLength(2) + }) + + it('handles very large number of sponsors', () => { + const manySponsors: Sponsor[] = Array.from({ length: 100 }, (_, i) => ({ + name: `Sponsor ${i}`, + imageUrl: `https://example.com/logo${i}.png`, + url: `https://sponsor${i}.com`, + sponsorType: 'Gold', + })) + + render() + + expect(screen.getAllByTestId('sponsor-link')).toHaveLength(202) + expect(screen.getAllByTestId('sponsor-image')).toHaveLength(200) + + const scroller = document.querySelector('.animate-scroll') + expect(scroller).toHaveStyle('animation-duration: 200s') + }) + }) + + describe('Accessibility Roles and Labels', () => { + it('provides proper alt text for images', () => { + render() + + const images = screen.getAllByTestId('sponsor-image') + expect(images[0]).toHaveAttribute('alt', 'Test Sponsor 1 logo') + expect(images[1]).toHaveAttribute('alt', 'Test Sponsor 2 logo') + expect(images[2]).toHaveAttribute('alt', 'Test Sponsor 1 logo') + expect(images[3]).toHaveAttribute('alt', 'Test Sponsor 2 logo') + }) + + it('uses proper link attributes for accessibility', () => { + render() + + const links = screen.getAllByTestId('sponsor-link') + links.forEach((link) => { + expect(link).toHaveAttribute('target', '_blank') + expect(link).toHaveAttribute('rel', 'noopener noreferrer') + }) + }) + + it('maintains semantic structure for screen readers', () => { + render() + + const sponsorLinks = screen.getAllByTestId('sponsor-link') + sponsorLinks.forEach((link) => { + expect(link).toBeInTheDocument() + expect(link.tagName).toBe('A') + }) + }) + + it('provides descriptive text for external links', () => { + render() + + const supportersLink = screen.getByText('this page') + const donateLink = screen.getByText('click here') + + expect(supportersLink.closest('a')).toHaveAttribute('href', 'https://owasp.org/supporters/') + expect(donateLink.closest('a')).toHaveAttribute( + 'href', + 'https://owasp.org/donate/?reponame=www-project-nest&title=OWASP+Nest' + ) + }) + }) + + describe('DOM Structure, ClassNames, and Styles', () => { + it('applies correct CSS classes', () => { + render() + + const overflowContainer = document.querySelector('.relative.overflow-hidden.py-2') + expect(overflowContainer).toBeInTheDocument() + + const scroller = document.querySelector('.animate-scroll.flex.w-full.gap-6') + expect(scroller).toBeInTheDocument() + + const sponsorContainers = document.querySelectorAll('[class*="min-w-[220px]"]') + expect(sponsorContainers).toHaveLength(6) + }) + + it('applies correct styles to images', () => { + render() + + const images = screen.getAllByTestId('sponsor-image') + images.forEach((image) => { + expect(image).toHaveAttribute('style', 'object-fit: contain;') + expect(image).toHaveAttribute('data-fill', 'true') + }) + }) + + it('maintains proper DOM hierarchy', () => { + render() + + const overflowContainer = document.querySelector('.relative.overflow-hidden') + expect(overflowContainer).toBeInTheDocument() + + 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') + expect(link).toBeInTheDocument() + + const imageContainer = link?.querySelector('.relative.mb-4') + expect(imageContainer).toBeInTheDocument() + }) + + it('applies correct footer styling', () => { + render() + + const footer = screen + .getByText(/These logos represent the corporate supporters/) + .closest('div') + expect(footer).toHaveClass( + 'text-muted-foreground', + 'mt-4', + 'flex', + 'w-full', + 'flex-col', + 'items-center', + 'justify-center', + 'text-center', + 'text-sm' + ) + }) + + it('applies correct link styling in footer', () => { + render() + + const supportersLink = screen.getByText('this page').closest('a') + const donateLink = screen.getByText('click here').closest('a') + + expect(supportersLink).toHaveClass('text-primary', 'font-medium', 'hover:underline') + expect(donateLink).toHaveClass('text-primary', 'font-medium', 'hover:underline') + }) + + it('sets correct minimum width for sponsor containers', () => { + render() + + const sponsorContainers = document.querySelectorAll('[class*="min-w-[220px]"]') + expect(sponsorContainers).toHaveLength(6) + + sponsorContainers.forEach((container) => { + expect(container).toHaveClass('min-w-[220px]') + }) + }) + }) +}) From e17435527165cd317c94667e6bf937017f1de164 Mon Sep 17 00:00:00 2001 From: RizWaaN3024 Date: Sun, 14 Sep 2025 17:30:59 +0530 Subject: [PATCH 2/2] Fixed Issues flagged by the bot --- frontend/__tests__/unit/components/LogoCarousel.test.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/__tests__/unit/components/LogoCarousel.test.tsx b/frontend/__tests__/unit/components/LogoCarousel.test.tsx index 3c4be7a065..a50487cbdf 100644 --- a/frontend/__tests__/unit/components/LogoCarousel.test.tsx +++ b/frontend/__tests__/unit/components/LogoCarousel.test.tsx @@ -221,9 +221,9 @@ describe('MovingLogos (LogoCarousel)', () => { .getAllByTestId('sponsor-link') .filter( (link) => - link.getAttribute('href')?.includes('sponsor1.com') || - link.getAttribute('href')?.includes('sponsor2.com') || - link.getAttribute('href')?.includes('sponsor3.com') + link.getAttribute('href') === 'https://sponsor1.com' || + link.getAttribute('href') === 'https://sponsor2.com' || + link.getAttribute('href') === 'https://sponsor3.com' ) expect(sponsorLinks).toHaveLength(6) @@ -235,7 +235,7 @@ describe('MovingLogos (LogoCarousel)', () => { const newSponsorLinks = screen .getAllByTestId('sponsor-link') - .filter((link) => link.getAttribute('href')?.includes('new.com')) + .filter((link) => link.getAttribute('href') === 'https://new.com') expect(newSponsorLinks).toHaveLength(2) expect(newSponsorLinks[0]).toHaveAttribute('href', 'https://new.com')