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
112 changes: 112 additions & 0 deletions frontend/__tests__/unit/components/IssuesTable.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { render, screen, fireEvent, within } from '@testing-library/react'
import '@testing-library/jest-dom'
import React from 'react'
import IssuesTable, { type IssueRow } from 'components/IssuesTable'
import { LabelList } from 'components/LabelList'

jest.mock('next/navigation', () => ({
useRouter: () => ({
Expand Down Expand Up @@ -47,6 +48,36 @@ jest.mock('@heroui/tooltip', () => ({
},
}))

interface MockLabelListProps {
entityKey: string
labels: string[]
maxVisible?: number
className?: string
}

const MockLabelList = (props: MockLabelListProps) => {
const { entityKey, labels, maxVisible = 5, className } = props
if (!labels || labels.length === 0) return null
const visibleLabels = labels.slice(0, maxVisible)
const remainingCount = labels.length - maxVisible
return (
<div data-testid="label-list" className={className}>
{visibleLabels.map((label) => (
<span key={`${entityKey}-${label}`} data-testid="label">
{label}
</span>
))}
{remainingCount > 0 && <span data-testid="label-more">+{remainingCount} more</span>}
</div>
)
}

jest.mock('components/LabelList', () => ({
// Must match the module export name for the mock to be used by IssuesTable
// eslint-disable-next-line @typescript-eslint/naming-convention -- component export name
LabelList: jest.fn((props: MockLabelListProps) => <MockLabelList {...props} />),
}))

const mockIssues: IssueRow[] = [
{
objectID: '1',
Expand Down Expand Up @@ -102,6 +133,10 @@ describe('<IssuesTable />', () => {
issues: mockIssues,
}

beforeEach(() => {
jest.mocked(LabelList).mockClear()
})

describe('Rendering', () => {
it('renders table view', () => {
render(<IssuesTable {...defaultProps} />)
Expand Down Expand Up @@ -200,6 +235,83 @@ describe('<IssuesTable />', () => {
render(<IssuesTable issues={[manyLabelsIssue]} maxVisibleLabels={3} />)
expect(screen.getByText('+2 more')).toBeInTheDocument()
})

it('uses LabelList with entityKey derived from issue objectID', () => {
render(<IssuesTable issues={[mockIssues[0]]} />)
expect(LabelList).toHaveBeenCalledTimes(1)
expect(LabelList).toHaveBeenCalledWith(
expect.objectContaining({
entityKey: 'issue-1',
labels: ['bug', 'enhancement'],
maxVisible: 5,
}),
undefined
)
})

it('passes maxVisibleLabels to LabelList as maxVisible', () => {
render(<IssuesTable issues={[mockIssues[0]]} maxVisibleLabels={3} />)
expect(LabelList).toHaveBeenCalledTimes(1)
expect(LabelList).toHaveBeenCalledWith(
expect.objectContaining({
entityKey: 'issue-1',
labels: ['bug', 'enhancement'],
maxVisible: 3,
}),
undefined
)
})

it('passes empty array to LabelList when issue has no labels', () => {
render(<IssuesTable issues={[mockIssues[2]]} />)
expect(LabelList).toHaveBeenCalledTimes(1)
expect(LabelList).toHaveBeenCalledWith(
expect.objectContaining({
entityKey: 'issue-3',
labels: [],
maxVisible: 5,
}),
undefined
)
})

it('passes empty array to LabelList when issue.labels is undefined', () => {
const issueWithUndefinedLabels = {
...mockIssues[0],
objectID: 'undefined-labels',
labels: undefined,
} as IssueRow
render(<IssuesTable issues={[issueWithUndefinedLabels]} />)
expect(LabelList).toHaveBeenCalledTimes(1)
expect(LabelList).toHaveBeenCalledWith(
expect.objectContaining({
entityKey: 'issue-undefined-labels',
labels: [],
maxVisible: 5,
}),
undefined
)
})

it('calls LabelList once per issue row with correct labels', () => {
render(<IssuesTable issues={mockIssues} />)
expect(LabelList).toHaveBeenCalledTimes(3)
expect(LabelList).toHaveBeenNthCalledWith(
1,
expect.objectContaining({ entityKey: 'issue-1', labels: ['bug', 'enhancement'] }),
undefined
)
expect(LabelList).toHaveBeenNthCalledWith(
2,
expect.objectContaining({ entityKey: 'issue-2', labels: ['documentation'] }),
undefined
)
expect(LabelList).toHaveBeenNthCalledWith(
3,
expect.objectContaining({ entityKey: 'issue-3', labels: [] }),
undefined
)
})
})

describe('Assignee Column', () => {
Expand Down
25 changes: 8 additions & 17 deletions frontend/src/components/IssuesTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import Image from 'next/image'
import { useRouter } from 'next/navigation'
import type React from 'react'

import { LabelList } from 'components/LabelList'

export type IssueRow = {
objectID: string
number: number
Expand Down Expand Up @@ -141,23 +143,12 @@ const IssuesTable: React.FC<IssuesTableProps> = ({

{/* Labels */}
<td className="block pb-3 lg:table-cell lg:px-6 lg:py-4">
{issue.labels && issue.labels.length > 0 ? (
<div className="flex flex-wrap gap-1 lg:gap-2">
{issue.labels.slice(0, maxVisibleLabels).map((label) => (
<span
key={label}
className="inline-flex items-center rounded-md bg-gray-100 px-2 py-0.5 text-xs text-gray-700 lg:rounded-lg lg:border lg:border-gray-400 lg:bg-transparent lg:hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:lg:border-gray-300 dark:lg:hover:bg-gray-700"
>
{label}
</span>
))}
{issue.labels.length > maxVisibleLabels && (
<span className="inline-flex items-center rounded-md bg-gray-100 px-2 py-0.5 text-xs text-gray-500 lg:rounded-lg lg:border lg:border-gray-400 lg:bg-transparent lg:text-gray-700 lg:hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-400 dark:lg:border-gray-300 dark:lg:text-gray-300 dark:lg:hover:bg-gray-700">
+{issue.labels.length - maxVisibleLabels} more
</span>
)}
</div>
) : null}
<LabelList
entityKey={`issue-${issue.objectID}`}
labels={issue.labels ?? []}
maxVisible={maxVisibleLabels}
className="gap-1 lg:gap-2"
/>
</td>

{/* Assignee */}
Expand Down