Skip to content
Open
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
139 changes: 75 additions & 64 deletions frontend/__tests__/unit/components/MultiSearch.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,45 @@ const defaultProps = {
eventData: [],
}

// Test utility functions
const createUserWithSetup = () => userEvent.setup()

const renderMultiSearchWithDefaults = (overrideProps = {}) => {
const props = { ...defaultProps, ...overrideProps }
return render(<MultiSearchBar {...props} />)
}

const expectSuggestionsVisible = async (text: string) => {
return waitFor(() => {
const suggestions = screen.getAllByText(text)
expect(suggestions.length).toBeGreaterThan(0)
expect(suggestions[0]).toBeInTheDocument()
})
}

const expectListItemHighlighted = async (index: number) => {
return waitFor(() => {
const listItems = screen.getAllByRole('listitem')
expect(listItems[index]).toHaveClass('bg-gray-100')
})
}

const getInputElement = () => screen.getByPlaceholderText('Search...')

const expectNoSuggestions = async () => {
return waitFor(() => {
expect(screen.queryAllByRole('listitem')).toHaveLength(0)
})
}

const expectSuggestionsForEach = async (text: string, expectationCallback: (suggestion: HTMLElement) => void) => {
return waitFor(() => {
const suggestions = screen.getAllByText(text)
expect(suggestions.length).toBeGreaterThan(0)
suggestions.forEach(expectationCallback)
})
}

beforeEach(() => {
mockUseRouter.mockReturnValue({
push: mockPush,
Expand All @@ -105,15 +144,13 @@ afterEach(() => {

describe('Rendering', () => {
it('renders successfully with minimal required props', () => {
render(<MultiSearchBar {...defaultProps} />)

renderMultiSearchWithDefaults()
expect(screen.getByPlaceholderText('Search...')).toBeInTheDocument()
expect(screen.getByTestId('font-awesome-icon')).toBeInTheDocument()
})

it('renders loading state when not loaded', () => {
render(<MultiSearchBar {...defaultProps} isLoaded={false} />)

renderMultiSearchWithDefaults({ isLoaded: false })
const loadingSkeleton = document.querySelector(
'.animate-pulse.h-12.w-full.rounded-lg.bg-gray-200'
)
Expand All @@ -128,20 +165,18 @@ describe('Rendering', () => {
})

it('renders with initial value', () => {
render(<MultiSearchBar {...defaultProps} initialValue={'initial search'} />)

renderMultiSearchWithDefaults({ initialValue: 'initial search' })
expect(screen.getByDisplayValue('initial search')).toBeInTheDocument()
})

it('renders custom placeholder', () => {
render(<MultiSearchBar {...defaultProps} placeholder={'Custom Placeholder'} />)

renderMultiSearchWithDefaults({ placeholder: 'Custom Placeholder' })
expect(screen.getByPlaceholderText('Custom Placeholder')).toBeInTheDocument()
})

it('applies correct css classes for input', () => {
render(<MultiSearchBar {...defaultProps} />)
const input = screen.getByPlaceholderText('Search...')
renderMultiSearchWithDefaults()
const input = getInputElement()
expect(input).toHaveClass(
'h-12',
'w-full',
Expand Down Expand Up @@ -308,10 +343,10 @@ describe('Rendering', () => {
})

it('hides suggestions when clicking outside', async () => {
const user = userEvent.setup()
render(<MultiSearchBar {...defaultProps} />)
const user = createUserWithSetup()
renderMultiSearchWithDefaults()
const input = getInputElement()

const input = screen.getByPlaceholderText('Search...')
await user.type(input, 'test')

await waitFor(() => {
Expand All @@ -324,7 +359,6 @@ describe('Rendering', () => {
})

await user.click(document.body)

await waitFor(() => {
expect(screen.queryAllByText('Test Chapter')).toHaveLength(0)
})
Expand All @@ -348,102 +382,79 @@ describe('Rendering', () => {
})

it('highlights first suggestion on arrow down', async () => {
const user = userEvent.setup()
render(<MultiSearchBar {...defaultProps} />)
const user = createUserWithSetup()
renderMultiSearchWithDefaults()
const input = getInputElement()

const input = screen.getByPlaceholderText('Search...')
await user.type(input, 'test')
await waitFor(() => {
const suggestionButtons = screen.getAllByRole('button')
expect(suggestionButtons.length).toBeGreaterThan(0)
})

await user.keyboard('{ArrowDown}')
await waitFor(() => {
const listItems = screen.getAllByRole('listitem')
expect(listItems[0]).toHaveClass('bg-gray-100')
})
await expectListItemHighlighted(0)
})

// eslint-disable-next-line jest/expect-expect
it('moves highlight down on subsequent arrow down presses', async () => {
const user = userEvent.setup()
render(<MultiSearchBar {...defaultProps} />)
const user = createUserWithSetup()
renderMultiSearchWithDefaults()
const input = getInputElement()

const input = screen.getByPlaceholderText('Search...')
await user.type(input, 'test')
await waitFor(() => {
const testChapters = screen.getAllByText('Test Chapter')
expect(testChapters.length).toBeGreaterThan(0)
})
await expectSuggestionsVisible('Test Chapter')

await user.keyboard('{ArrowDown}')
await user.keyboard('{ArrowDown}')
await waitFor(() => {
const listItems = screen.getAllByRole('listitem')
expect(listItems[1]).toHaveClass('bg-gray-100')
})
await expectListItemHighlighted(1)
})

// eslint-disable-next-line jest/expect-expect
it('moves highlight up on arrow up', async () => {
const user = userEvent.setup()
render(<MultiSearchBar {...defaultProps} />)
const user = createUserWithSetup()
renderMultiSearchWithDefaults()
const input = getInputElement()

const input = screen.getByPlaceholderText('Search...')
await user.type(input, 'test')
await expectSuggestionsVisible('Test Chapter')

await waitFor(() => {
const testChapters = screen.getAllByText('Test Chapter')
expect(testChapters.length).toBeGreaterThan(0)
})
await user.keyboard('{ArrowDown}')
await user.keyboard('{ArrowDown}')
await waitFor(() => {
const listItems = screen.getAllByRole('listitem')
expect(listItems[1]).toHaveClass('bg-gray-100')
})
await user.keyboard('{ArrowUp}')
await expectListItemHighlighted(1)

await waitFor(() => {
const listItems = screen.getAllByRole('listitem')
expect(listItems[0]).toHaveClass('bg-gray-100')
})
await user.keyboard('{ArrowUp}')
await expectListItemHighlighted(0)
})

it('closes suggestions on Escape key', async () => {
const user = userEvent.setup()
render(<MultiSearchBar {...defaultProps} />)
const user = createUserWithSetup()
renderMultiSearchWithDefaults()
const input = getInputElement()

const input = screen.getByPlaceholderText('Search...')
await user.type(input, 'test')

// Wait for suggestion list items to appear
await waitFor(() => {
const listItems = screen.getAllByRole('listitem')
expect(listItems.length).toBeGreaterThan(0)
})

await user.keyboard('{Escape}')

// Check that no list items remain
await waitFor(() => {
const listItems = screen.queryAllByRole('listitem')
expect(listItems).toHaveLength(0)
})
await expectNoSuggestions()
})

it('selects highlighted suggestion on Enter', async () => {
const user = userEvent.setup()
render(<MultiSearchBar {...defaultProps} />)
const user = createUserWithSetup()
renderMultiSearchWithDefaults()
const input = getInputElement()

const input = screen.getByPlaceholderText('Search...')
await user.type(input, 'test')

await waitFor(() => {
const listItems = screen.getAllByRole('listitem')
expect(listItems.length).toBeGreaterThan(0)
})

await user.keyboard('{ArrowDown}')
await user.keyboard('{Enter}')

expect(mockPush).toHaveBeenCalledWith('/chapters/test-chapter')
})
})
Expand Down
16 changes: 16 additions & 0 deletions frontend/__tests__/unit/components/ProgramCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,21 @@ describe('ProgramCard', () => {

beforeEach(() => {
jest.clearAllMocks()
// Mock toLocaleDateString to ensure consistent formatting
jest.spyOn(Date.prototype, 'toLocaleDateString').mockImplementation(function(this: Date, locale, options) {
if (this.getTime() === new Date('2024-01-01T00:00:00Z').getTime()) {
return 'Jan 1, 2024'
}
if (this.getTime() === new Date('2024-12-31T23:59:59Z').getTime()) {
return 'Dec 31, 2024'
}
// Call the original implementation for other dates
return new Intl.DateTimeFormat(locale, options).format(this)
})
})

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

describe('Basic Rendering', () => {
Expand Down Expand Up @@ -219,6 +234,7 @@ describe('ProgramCard', () => {
it('shows date range when both startedAt and endedAt are provided', () => {
render(<ProgramCard program={baseMockProgram} onView={mockOnView} accessLevel="user" />)

// With mocked date formatting, we can now match the exact string
expect(screen.getByText('Jan 1, 2024 – Dec 31, 2024')).toBeInTheDocument()
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -361,9 +361,7 @@ describe('ProjectTypeDashboardCard', () => {
]

const testTypeValue = (type: 'healthy' | 'needsAttention' | 'unhealthy') => {
expect(() => {
render(<ProjectTypeDashboardCard type={type} count={10} icon={faHeartPulse} />)
}).not.toThrow()
expect(() => render(<ProjectTypeDashboardCard type={type} count={10} icon={faHeartPulse} />)).not.toThrow()
}

for (const type of validTypes) {
Expand Down
1 change: 1 addition & 0 deletions frontend/__tests__/unit/data/mockProjectDetailsData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const mockProjectDetailsData = {
lastCommitDays: 5,
lastReleaseDays: 10,
score: 85,
createdAt: '2024-01-01T00:00:00Z',
},
],
isActive: true,
Expand Down
28 changes: 28 additions & 0 deletions frontend/__tests__/unit/pages/Home.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,24 @@ jest.mock('next/navigation', () => ({
useRouter: jest.fn(() => mockRouter),
}))

jest.mock('components/AnimatedCounter', () => {
return ({ count, suffix }: { count?: number; suffix?: string }) => {
// Handle undefined/null count values
if (count === undefined || count === null) {
return <span>0{suffix || ''}</span>
}

// Simple number formatting that mimics millify for common test values
const formatNumber = (num: number) => {
if (num >= 1000000) return (num / 1000000).toFixed(1).replace(/\.0$/, '') + 'M'
if (num >= 1000) return (num / 1000).toFixed(1).replace(/\.0$/, '') + 'k'
return num.toString()
}

return <span>{formatNumber(count)}{suffix || ''}</span>
}
})

jest.mock('@/components/MarkdownWrapper', () => {
return ({ content, className }: { content: string; className?: string }) => (
<div
Expand Down Expand Up @@ -66,6 +84,14 @@ jest.mock('next/link', () => {
return ({ children }) => children
})

jest.mock('components/ChapterMap', () => {
return () => <div data-testid="mock-chapter-map">Mock Chapter Map</div>
})

jest.mock('components/ContributionHeatmap', () => {
return () => <div data-testid="mock-contribution-heatmap">Mock Contribution Heatmap</div>
})

describe('Home', () => {
let mockRouter: { push: jest.Mock }

Expand Down Expand Up @@ -269,8 +295,10 @@ describe('Home', () => {
'Countries',
'Slack Community',
]

const stats = mockGraphQLData.statsOverview

// Check headers are rendered
await waitFor(() => {
for (const header of headers) {
expect(screen.getByText(header)).toBeInTheDocument()
Expand Down
22 changes: 22 additions & 0 deletions frontend/__tests__/unit/pages/ProjectDetails.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,28 @@ jest.mock('react-apexcharts', () => {
}
})

jest.mock('components/LineChart', () => {
return ({ title }: { title: string }) => (
<div data-testid="mock-line-chart">
<h3>{title}</h3>
<div>Mock Line Chart</div>
</div>
)
})

jest.mock('components/BarChart', () => {
return ({ title }: { title: string }) => (
<div data-testid="mock-bar-chart">
<h3>{title}</h3>
<div>Mock Bar Chart</div>
</div>
)
})

jest.mock('utils/env.client', () => ({
IS_PROJECT_HEALTH_ENABLED: true,
}))

const mockRouter = {
push: jest.fn(),
}
Expand Down
Loading
Loading