Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions components/__tests__/BackToTopButton.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { render, screen, fireEvent } from '@testing-library/react'
import '@testing-library/jest-dom'
import { beforeAll, beforeEach, describe, expect, jest, test } from '@jest/globals'
import BackToTopButton from '../buttons/BackToTopButton'
import '@testing-library/jest-dom'

describe('BackToTopButton Test', () => {
beforeAll(() => {
window.scrollTo = jest.fn()

Object.defineProperty(window, 'scrollY', { value: 0, writable: true })
})

test('Not displayed when scrollY < 300', () => {
render(<BackToTopButton />)
fireEvent.scroll(window)
expect(screen.queryByRole('button')).toBeNull()
})

test('Displayed when scrollY > 300', () => {
render(<BackToTopButton />)
Object.defineProperty(window, 'scrollY', { value: 400, writable: true })
fireEvent.scroll(window)
expect(screen.getByRole('button'))
})

test('When clicked, it calls scrollTo with the correct parameters', () => {
render(<BackToTopButton />)
Object.defineProperty(window, 'scrollY', { value: 400, writable: true })
fireEvent.scroll(window)

const button = screen.getByRole('button')
fireEvent.click(button)

expect(window.scrollTo).toHaveBeenCalledWith({
top: 0,
behavior: 'smooth'
})
})
})

37 changes: 37 additions & 0 deletions components/__tests__/Contributors.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { render, screen, fireEvent } from '@testing-library/react'
import '@testing-library/jest-dom'
import { describe, expect, test } from '@jest/globals'
import { Contributors } from '../index/Contributors'

describe('Contributors', () => {
const mockContributors = [
{ id: '1', url: 'https://github.com/alice', avatar: '/alice.png', name: 'Alice' },
{ id: '2', url: 'https://github.com/bob', avatar: '/bob.png', name: 'Bob' }
]

test('renders all contributor images', () => {
render(<Contributors contributors={mockContributors} />)

const aliceImage = screen.getByAltText('Alice')
const bobImage = screen.getByAltText('Bob')

expect(aliceImage).not.toBeNull()
expect(bobImage).not.toBeNull()

const aliceLink = screen.getByRole('link', { name: /alice/i })
const bobLink = screen.getByRole('link', { name: /bob/i })

expect(aliceLink.getAttribute('href')).toBe('https://github.com/alice')
expect(bobLink.getAttribute('href')).toBe('https://github.com/bob')
})

test('renders fallback when image fails to load', () => {
render(<Contributors contributors={mockContributors} />)

const aliceImage = screen.getByAltText('Alice')
fireEvent.error(aliceImage)

const fallback = screen.getByText('A')
expect(fallback).not.toBeNull()
})
})
35 changes: 35 additions & 0 deletions components/__tests__/Footer.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { render, screen, waitFor } from '@testing-library/react'
import React from 'react'
import Footer from '../layout/Footer'

jest.mock('../buttons/Coffee', () => () => <div data-testid="coffee-button" />)
jest.mock('../buttons/Sponsor', () => () => <div data-testid="sponsor-button" />)

describe('Footer', () => {
beforeEach(() => {
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ stargazers_count: 123 })
} as Response)
) as jest.Mock
})

afterEach(() => {
jest.resetAllMocks()
})

test('renders footer links and components', async () => {
render(<Footer />)

expect(screen.getByText('Technology')).toBeInTheDocument()
expect(screen.getByText('Information')).toBeInTheDocument()
expect(screen.getByText('Helpful Links')).toBeInTheDocument()

expect(screen.getByTestId('coffee-button')).toBeInTheDocument()
expect(screen.getByTestId('sponsor-button')).toBeInTheDocument()

await waitFor(() => {
expect(screen.getByText('123')).toBeInTheDocument()
})
})
})
186 changes: 186 additions & 0 deletions components/__tests__/Header.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import { render, screen, fireEvent, waitFor, act } from '@testing-library/react'
import { useUser } from '@clerk/nextjs'
import { useRouter } from 'next/router'
import Header from '../layout/Header'

jest.mock('@clerk/nextjs', () => ({
useUser: jest.fn(),
UserButton: jest.fn(() => <div data-testid="user-button" />),
}))

jest.mock('next/router', () => ({
useRouter: jest.fn(),
}))

describe('Header component', () => {
const pushMock = jest.fn()
const onMock = jest.fn()
const offMock = jest.fn()

beforeEach(() => {
(useRouter as jest.Mock).mockReturnValue({
push: pushMock,
events: { on: onMock, off: offMock },
})

global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ stargazers_count: 123 }),
}) as any
)
})

afterEach(() => {
jest.clearAllMocks()
})

const renderHeader = async () => {
await act(async () => {
render(<Header />)
})
}

test('renders logo and main links', async () => {
;(useUser as jest.Mock).mockReturnValue({ user: null, isLoaded: true })

await renderHeader()

expect(screen.getAllByAltText('Logo')[0]).toBeInTheDocument()
expect(screen.getByText('Languages')).toBeInTheDocument()
expect(screen.getByText('Frameworks')).toBeInTheDocument()
expect(screen.getByText('Git')).toBeInTheDocument()
expect(screen.getByText('Roadmap')).toBeInTheDocument()
expect(screen.getAllByText('Sign In')[0]).toBeInTheDocument()
})

test('fetches GitHub stars and displays them', async () => {
;(useUser as jest.Mock).mockReturnValue({ user: null, isLoaded: true })

await renderHeader()

await waitFor(() => {
expect(screen.getAllByText('123')[0]).toBeInTheDocument()
})
})

test('toggles dropdown menus', async () => {
;(useUser as jest.Mock).mockReturnValue({ user: null, isLoaded: true })

await renderHeader()

const languagesButton = screen.getByText('Languages')
fireEvent.click(languagesButton)
expect(screen.getByText('JavaScript')).toBeInTheDocument()

fireEvent.click(languagesButton)
expect(screen.queryByText('JavaScript')).not.toBeInTheDocument()
})

test('navigates when dropdown item clicked', async () => {
;(useUser as jest.Mock).mockReturnValue({ user: null, isLoaded: true })

await renderHeader()

fireEvent.click(screen.getByText('Languages'))
fireEvent.click(screen.getByText('JavaScript'))
expect(pushMock).toHaveBeenCalledWith('/languages/javascript')
})

test('mobile menu toggles', async () => {
;(useUser as jest.Mock).mockReturnValue({ user: null, isLoaded: true })
await renderHeader()

const menuButton = screen.getByTestId('mobile-menu-button')

fireEvent.click(menuButton)
await waitFor(() => {
const lang = screen.getAllByText('Languages')
expect(lang.length).toBeGreaterThan(0)
})

fireEvent.click(menuButton)
await waitFor(() => {
const lang = screen.queryAllByText('Languages')
expect(lang.length).toBeGreaterThan(0)
})
})

test('renders UserButton if user is logged in', async () => {
;(useUser as jest.Mock).mockReturnValue({ user: { id: '1' }, isLoaded: true })

await renderHeader()

expect(screen.getByTestId('user-button')).toBeInTheDocument()
})

test('handles fetch error gracefully', async () => {
;(useUser as jest.Mock).mockReturnValue({ user: null, isLoaded: true })
;(global.fetch as jest.Mock).mockRejectedValueOnce(new Error('Network error'))
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})

await renderHeader()

await waitFor(() => {
expect(errorSpy).toHaveBeenCalled()
})
errorSpy.mockRestore()
})

test('registers and cleans up router.events listeners', async () => {
;(useUser as jest.Mock).mockReturnValue({ user: null, isLoaded: true })

const { unmount } = render(<Header />)

await waitFor(() => {
expect(onMock).toHaveBeenCalledWith('routeChangeStart', expect.any(Function))
})

unmount()
expect(offMock).toHaveBeenCalledWith('routeChangeStart', expect.any(Function))
})

test('toggleSideNav opens and closes side navigation', async () => {
;(useUser as jest.Mock).mockReturnValue({ user: null, isLoaded: true })
await renderHeader()

const menuButton = screen.getByTestId('mobile-menu-button')
fireEvent.click(menuButton)
fireEvent.click(menuButton)
expect(menuButton).toBeInTheDocument()
})

test('dispatches custom event when dropdowns open', async () => {
;(useUser as jest.Mock).mockReturnValue({ user: null, isLoaded: true })
const eventSpy = jest.fn()
window.addEventListener('dropdownOpened', eventSpy)

await renderHeader()

const langBtn = screen.getAllByText('Languages')[0]
const fwBtn = screen.getAllByText('Frameworks')[0]
const gitBtn = screen.getAllByText('Git')[0]
const roadmapBtn = screen.getAllByText('Roadmap')[0]

fireEvent.click(langBtn)
fireEvent.click(fwBtn)
fireEvent.click(gitBtn)
fireEvent.click(roadmapBtn)

expect(eventSpy).toHaveBeenCalledTimes(4)

window.removeEventListener('dropdownOpened', eventSpy)
})

test('navigates for framework', async () => {
;(useUser as jest.Mock).mockReturnValue({ user: null, isLoaded: true })

await renderHeader()

const fwBtn = screen.getAllByText('Frameworks')[0]
fireEvent.click(fwBtn)
fireEvent.click(await screen.findByText('React'))
expect(pushMock).toHaveBeenCalledWith('/frameworks/react')


})
})
39 changes: 39 additions & 0 deletions components/__tests__/LazyImage.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { render, screen } from '@testing-library/react'
import '@testing-library/jest-dom'
import { useInView } from 'react-intersection-observer'
import { describe, expect, test } from '@jest/globals'
import React from 'react'
import LazyImage from '../index/LazyImage'

jest.mock('react-intersection-observer', () => ({
useInView: jest.fn()
}))

const mockedUseInView = useInView as unknown as jest.Mock

describe('LazyImage', () => {
const mockProps = {
src: '/test.png',
alt: 'Test Image',
width: 100,
height: 100
}

test('does not render image when not in view', () => {
mockedUseInView.mockReturnValue({ ref: jest.fn(), inView: false })

render(<LazyImage {...mockProps} />)

expect(screen.queryByAltText('Test Image')).toBeNull()
})

test('renders image when in view', () => {
mockedUseInView.mockReturnValue({ ref: jest.fn(), inView: true })

render(<LazyImage {...mockProps} />)

const img = screen.getByAltText('Test Image')
expect(img).not.toBeNull()
expect(img?.getAttribute('src')).toContain('test.png')
})
})
48 changes: 48 additions & 0 deletions components/__tests__/LoadingPage.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { render, screen } from '@testing-library/react'
import LoadingPage from '../layout/LoadingPage'
import { useRouter } from 'next/router'
import { act } from 'react-dom/test-utils'

jest.mock('next/router', () => ({
useRouter: jest.fn(),
}))

describe('LoadingPage', () => {
let pushMock: jest.Mock

beforeEach(() => {
jest.useFakeTimers()
pushMock = jest.fn()
;(useRouter as jest.Mock).mockReturnValue({
push: pushMock,
})
})

afterEach(() => {
jest.clearAllMocks()
jest.useRealTimers()
})

test('renders logo, title, bouncing dots and redirect text', () => {
render(<LoadingPage />)

expect(screen.getByAltText('FCM Logo')).toBeInTheDocument()

expect(screen.getByText('Welcome to Fork, Commit, Merge')).toBeInTheDocument()

expect(screen.getByText('Redirecting you to the homepage...')).toBeInTheDocument()

const dots = document.querySelectorAll('.animate-bounce')
expect(dots.length).toBe(3)
})

test('redirects to home after 3 seconds', () => {
render(<LoadingPage />)

act(() => {
jest.advanceTimersByTime(3000)
})

expect(pushMock).toHaveBeenCalledWith('/')
})
})
Loading
Loading