diff --git a/frontend/__tests__/unit/components/SkeletonBase.test.tsx b/frontend/__tests__/unit/components/SkeletonBase.test.tsx new file mode 100644 index 0000000000..1c214c340c --- /dev/null +++ b/frontend/__tests__/unit/components/SkeletonBase.test.tsx @@ -0,0 +1,473 @@ +import { render, screen } from '@testing-library/react' +import React from 'react' +import SkeletonBase from 'components/SkeletonsBase' + +jest.mock('@heroui/skeleton', () => ({ + Skeleton: ({ className, children }: { className?: string; children?: React.ReactNode }) => ( +
+ {children} +
+ ), +})) + +jest.mock('components/LoadingSpinner', () => { + return function MockLoadingSpinner({ imageUrl }: { imageUrl: string }) { + return
+ } +}) + +jest.mock('components/skeletons/Card', () => { + return function MockCardSkeleton({ + showLevel = true, + showIcons = true, + showLink = true, + numIcons = 1, + showContributors = true, + showSocial = true, + }: { + showLevel?: boolean + showIcons?: boolean + showLink?: boolean + numIcons?: number + showContributors?: boolean + showSocial?: boolean + }) { + return ( +
+ ) + } +}) + +jest.mock('components/skeletons/UserCard', () => { + return function MockUserCardSkeleton() { + return
+ } +}) + +describe('SkeletonBase', () => { + const defaultProps = { + indexName: 'projects', + loadingImageUrl: 'https://example.com/loading.gif', + } + + describe('Basic Rendering', () => { + it('renders successfully with minimal required props', () => { + render() + expect(screen.getAllByTestId('card-skeleton')[0]).toBeInTheDocument() + }) + + it('renders without crashing when props are provided', () => { + const { container } = render() + expect(container.firstChild).toBeInTheDocument() + }) + + it('renders different components based on indexName prop', () => { + const { rerender } = render() + expect(screen.getAllByTestId('card-skeleton')).toHaveLength(4) + + rerender() + expect(screen.getAllByTestId('user-card-skeleton')).toHaveLength(12) + }) + }) + + describe('Conditional Rendering Logic', () => { + it('renders LoadingSpinner for unknown indexName', () => { + render() + + const spinner = screen.getByTestId('loading-spinner') + expect(spinner).toBeInTheDocument() + expect(spinner).toHaveAttribute('data-image-url', 'test-image.jpg') + }) + + it('renders LoadingSpinner for empty indexName', () => { + render() + + expect(screen.getByTestId('loading-spinner')).toBeInTheDocument() + }) + + it('renders user cards grid for users indexName', () => { + render() + + const userCards = screen.getAllByTestId('user-card-skeleton') + expect(userCards).toHaveLength(12) + + const gridContainer = userCards[0].parentElement + expect(gridContainer).toHaveClass( + 'grid', + 'grid-cols-1', + 'gap-6', + 'sm:grid-cols-2', + 'lg:grid-cols-3', + 'xl:grid-cols-4' + ) + }) + + it('renders skeleton components for non-users indexName', () => { + render() + + const skeletonComponents = screen.getAllByTestId('card-skeleton') + expect(skeletonComponents).toHaveLength(4) + }) + }) + + describe('Prop-based Behaviour', () => { + it('configures chapters skeleton correctly', () => { + render() + + expect(screen.getByTestId('hero-skeleton')).toBeInTheDocument() + expect(screen.getByTestId('hero-skeleton')).toHaveClass('mb-2', 'h-96', 'w-full', 'max-w-6xl') + + const cardSkeletons = screen.getAllByTestId('card-skeleton') + cardSkeletons.forEach((skeleton) => { + expect(skeleton).toHaveAttribute('data-show-level', 'false') + expect(skeleton).toHaveAttribute('data-show-icons', 'false') + expect(skeleton).toHaveAttribute('data-show-link', 'false') + }) + }) + + it('configures issues skeleton correctly', () => { + render() + + const cardSkeletons = screen.getAllByTestId('card-skeleton') + cardSkeletons.forEach((skeleton) => { + expect(skeleton).toHaveAttribute('data-show-level', 'false') + expect(skeleton).toHaveAttribute('data-show-icons', 'true') + expect(skeleton).toHaveAttribute('data-num-icons', '2') + expect(skeleton).toHaveAttribute('data-show-contributors', 'false') + expect(skeleton).toHaveAttribute('data-show-social', 'false') + }) + }) + + it('configures projects skeleton correctly', () => { + render() + + const cardSkeletons = screen.getAllByTestId('card-skeleton') + cardSkeletons.forEach((skeleton) => { + expect(skeleton).toHaveAttribute('data-show-link', 'false') + expect(skeleton).toHaveAttribute('data-show-social', 'false') + expect(skeleton).toHaveAttribute('data-show-icons', 'true') + expect(skeleton).toHaveAttribute('data-num-icons', '3') + }) + }) + + it('configures committees skeleton correctly', () => { + render() + + const cardSkeletons = screen.getAllByTestId('card-skeleton') + cardSkeletons.forEach((skeleton) => { + expect(skeleton).toHaveAttribute('data-show-link', 'false') + expect(skeleton).toHaveAttribute('data-show-level', 'false') + expect(skeleton).toHaveAttribute('data-show-icons', 'true') + expect(skeleton).toHaveAttribute('data-num-icons', '1') + }) + }) + + it('passes loadingImageUrl to LoadingSpinner correctly', () => { + const testImageUrl = 'https://example.com/custom-loading.gif' + render() + + const spinner = screen.getByTestId('loading-spinner') + expect(spinner).toHaveAttribute('data-image-url', testImageUrl) + }) + }) + + describe('State Changers / Internal Logic', () => { + it('switches between different skeleton configurations based on indexName', () => { + const { rerender } = render() + + let cardSkeletons = screen.getAllByTestId('card-skeleton') + expect(cardSkeletons[0]).toHaveAttribute('data-num-icons', '2') + + rerender() + cardSkeletons = screen.getAllByTestId('card-skeleton') + expect(cardSkeletons[0]).toHaveAttribute('data-num-icons', '3') + + rerender() + cardSkeletons = screen.getAllByTestId('card-skeleton') + expect(cardSkeletons[0]).toHaveAttribute('data-num-icons', '1') + }) + + it('correctly determines component type based on switch logic', () => { + const { unmount: unmountChapters } = render( + + ) + expect(screen.getAllByTestId('card-skeleton')[0]).toBeInTheDocument() + unmountChapters() + + const { unmount: unmountIssues } = render( + + ) + expect(screen.getAllByTestId('card-skeleton')[0]).toBeInTheDocument() + unmountIssues() + + const { unmount: unmountProjects } = render( + + ) + expect(screen.getAllByTestId('card-skeleton')[0]).toBeInTheDocument() + unmountProjects() + + const { unmount: unmountCommittees } = render( + + ) + expect(screen.getAllByTestId('card-skeleton')[0]).toBeInTheDocument() + unmountCommittees() + + const { unmount: unmountUsers } = render( + + ) + expect(screen.getAllByTestId('user-card-skeleton')).toHaveLength(12) + unmountUsers() + + const { unmount: unmountUnknown } = render( + + ) + expect(screen.getByTestId('loading-spinner')).toBeInTheDocument() + unmountUnknown() + }) + }) + + describe('Default Values and Fallbacks', () => { + it('falls back to LoadingSpinner for unhandled indexName values', () => { + const unhandledValues = ['random', 'test', 'invalid', '123', 'null'] + + unhandledValues.forEach((value) => { + const { container } = render( + + ) + + expect(screen.getByTestId('loading-spinner')).toBeInTheDocument() + container.remove() + }) + }) + + it('handles case-sensitive indexName correctly', () => { + const { rerender } = render() + expect(screen.getByTestId('loading-spinner')).toBeInTheDocument() + + rerender() + expect(screen.getAllByTestId('user-card-skeleton')).toHaveLength(12) + expect(screen.queryByTestId('loading-spinner')).not.toBeInTheDocument() + }) + + it('uses default CardSkeleton props when not explicitly set', () => { + render() + + const cardSkeletons = screen.getAllByTestId('card-skeleton') + cardSkeletons.forEach((skeleton) => { + expect(skeleton).toHaveAttribute('data-show-level', 'true') + expect(skeleton).toHaveAttribute('data-show-contributors', 'true') + }) + }) + }) + + describe('Text and Content Rendering', () => { + it('renders correct number of skeleton components', () => { + const { rerender } = render() + expect(screen.getAllByTestId('card-skeleton')).toHaveLength(4) + + rerender() + expect(screen.getAllByTestId('user-card-skeleton')).toHaveLength(12) + + expect(screen.queryByTestId('card-skeleton')).not.toBeInTheDocument() + }) + + it('renders hero skeleton only for chapters', () => { + const { rerender } = render() + expect(screen.getByTestId('hero-skeleton')).toBeInTheDocument() + + rerender() + expect(screen.queryByTestId('hero-skeleton')).not.toBeInTheDocument() + }) + + it('does not render extra components for non-chapters types', () => { + render() + + expect(screen.queryByTestId('hero-skeleton')).not.toBeInTheDocument() + expect(screen.getAllByTestId('card-skeleton')).toHaveLength(4) + }) + }) + + describe('Edge Cases and Invalid Inputs', () => { + it('handles null indexName', () => { + render() + expect(screen.getByTestId('loading-spinner')).toBeInTheDocument() + }) + + it('handles undefined indexName', () => { + render() + expect(screen.getByTestId('loading-spinner')).toBeInTheDocument() + }) + + it('handles empty string loadingImageUrl', () => { + render() + + const spinner = screen.getByTestId('loading-spinner') + expect(spinner).toHaveAttribute('data-image-url', '') + }) + + it('handles null loadingImageUrl', () => { + render() + + const spinner = screen.getByTestId('loading-spinner') + expect(spinner).toHaveAttribute('data-image-url', '') + }) + + it('handles special characters in indexName', () => { + const specialNames = ['test-name', 'test_name', 'test.name', 'test name', '!@#$%'] + + specialNames.forEach((name) => { + const { container } = render() + + expect(screen.getByTestId('loading-spinner')).toBeInTheDocument() + container.remove() + }) + }) + + it('handles very long indexName strings', () => { + const longName = 'a'.repeat(1000) + render() + + expect(screen.getByTestId('loading-spinner')).toBeInTheDocument() + }) + + it('handles very long loadingImageUrl strings', () => { + const longUrl = 'https://example.com/' + 'a'.repeat(1000) + '.jpg' + render() + + const spinner = screen.getByTestId('loading-spinner') + expect(spinner).toHaveAttribute('data-image-url', longUrl) + }) + }) + + describe('Accessibility', () => { + it('maintains proper component structure for screen readers', () => { + const { container } = render() + + const mainContainer = container.querySelector('div') + expect(mainContainer).toHaveClass( + 'flex', + 'w-full', + 'flex-col', + 'items-center', + 'justify-center' + ) + }) + + it('provides accessible grid structure for users', () => { + render() + + const userCards = screen.getAllByTestId('user-card-skeleton') + const gridContainer = userCards[0].parentElement + + expect(gridContainer).toHaveClass('grid') + expect(gridContainer).toHaveAttribute('class') + }) + + it('ensures skeleton components are properly nested', () => { + render() + + const heroSkeleton = screen.getByTestId('hero-skeleton') + const cardSkeletons = screen.getAllByTestId('card-skeleton') + + expect(heroSkeleton.parentElement).toHaveClass('flex', 'w-full', 'flex-col') + cardSkeletons.forEach((skeleton) => { + expect(skeleton.parentElement).toHaveClass('flex', 'w-full', 'flex-col') + }) + }) + }) + + describe('DOM Structure / ClassNames / Styles', () => { + it('applies correct container classes for non-users components', () => { + const { container } = render() + + const mainContainer = container.firstChild as HTMLElement + expect(mainContainer).toHaveClass( + 'flex', + 'w-full', + 'flex-col', + 'items-center', + 'justify-center' + ) + }) + + it('applies correct hero skeleton classes for chapters', () => { + render() + + const heroSkeleton = screen.getByTestId('hero-skeleton') + expect(heroSkeleton).toHaveClass('mb-2', 'h-96', 'w-full', 'max-w-6xl') + }) + + it('applies correct grid classes for users', () => { + render() + + const userCards = screen.getAllByTestId('user-card-skeleton') + const gridContainer = userCards[0].parentElement + + expect(gridContainer).toHaveClass( + 'grid', + 'grid-cols-1', + 'gap-6', + 'sm:grid-cols-2', + 'lg:grid-cols-3', + 'xl:grid-cols-4' + ) + }) + + it('maintains consistent DOM structure across different skeleton types', () => { + const skeletonTypes = ['chapters', 'issues', 'projects', 'committees'] + + skeletonTypes.forEach((type) => { + const { container } = render() + + const mainContainer = container.querySelector('div') + expect(mainContainer).toHaveClass('flex', 'w-full', 'flex-col') + + container.remove() + }) + }) + }) + + describe('Component Integration', () => { + it('properly integrates with mocked HeroUI Skeleton component', () => { + render() + + const heroSkeleton = screen.getByTestId('hero-skeleton') + expect(heroSkeleton).toBeInTheDocument() + expect(heroSkeleton).toHaveClass('mb-2', 'h-96', 'w-full', 'max-w-6xl') + }) + + it('properly integrates with mocked CardSkeleton component', () => { + render() + + const cardSkeletons = screen.getAllByTestId('card-skeleton') + expect(cardSkeletons).toHaveLength(4) + + cardSkeletons.forEach((skeleton) => { + expect(skeleton).toHaveAttribute('data-show-link', 'false') + expect(skeleton).toHaveAttribute('data-show-social', 'false') + }) + }) + + it('properly integrates with mocked UserCardSkeleton component', () => { + render() + + const userCards = screen.getAllByTestId('user-card-skeleton') + expect(userCards).toHaveLength(12) + }) + + it('properly integrates with mocked LoadingSpinner component', () => { + render() + + const spinner = screen.getByTestId('loading-spinner') + expect(spinner).toHaveAttribute('data-image-url', 'test-spinner.gif') + }) + }) +})