feat: introduce CardTags component for displaying tags in cards

This commit is contained in:
yessenia 2026-02-06 00:34:59 +08:00
parent 08508b006d
commit 434eb0e924
6 changed files with 42 additions and 134 deletions

View File

@ -5,7 +5,7 @@ type Props = {
tags: string[]
}
const CardMoreInfoComponent = ({
const CardTagsComponent = ({
tags,
}: Props) => {
return (
@ -29,6 +29,6 @@ const CardMoreInfoComponent = ({
}
// Memoize to prevent unnecessary re-renders when tags array hasn't changed
const CardMoreInfo = React.memo(CardMoreInfoComponent)
const CardTags = React.memo(CardTagsComponent)
export default CardMoreInfo
export default CardTags

View File

@ -11,7 +11,7 @@ import DownloadCount from './base/download-count'
import OrgInfo from './base/org-info'
import Placeholder, { LoadingPlaceholder } from './base/placeholder'
import Title from './base/title'
import CardMoreInfo from './card-more-info'
import CardTags from './card-tags'
// ================================
// Import Components Under Test
// ================================
@ -642,9 +642,9 @@ describe('Card', () => {
})
// ================================
// CardMoreInfo Component Tests
// CardTags Component Tests
// ================================
describe('CardMoreInfo', () => {
describe('CardTags', () => {
beforeEach(() => {
vi.clearAllMocks()
})
@ -654,66 +654,24 @@ describe('CardMoreInfo', () => {
// ================================
describe('Rendering', () => {
it('should render without crashing', () => {
render(<CardMoreInfo downloadCount={100} tags={['tag1']} />)
render(<CardTags tags={['tag1']} />)
expect(document.body).toBeInTheDocument()
})
it('should render download count when provided', () => {
render(<CardMoreInfo downloadCount={1000} tags={[]} />)
it('should render tags in uppercase', () => {
render(<CardTags tags={['search', 'image']} />)
expect(screen.getByText('1,000')).toBeInTheDocument()
expect(screen.getByText('SEARCH')).toBeInTheDocument()
expect(screen.getByText('IMAGE')).toBeInTheDocument()
})
it('should render tags when provided', () => {
render(<CardMoreInfo tags={['search', 'image']} />)
it('should render at most two tags', () => {
render(<CardTags tags={['one', 'two', 'three']} />)
expect(screen.getByText('search')).toBeInTheDocument()
expect(screen.getByText('image')).toBeInTheDocument()
})
it('should render both download count and tags with separator', () => {
render(<CardMoreInfo downloadCount={500} tags={['tag1']} />)
expect(screen.getByText('500')).toBeInTheDocument()
expect(screen.getByText('·')).toBeInTheDocument()
expect(screen.getByText('tag1')).toBeInTheDocument()
})
})
// ================================
// Props Testing
// ================================
describe('Props', () => {
it('should not render download count when undefined', () => {
render(<CardMoreInfo tags={['tag1']} />)
expect(screen.queryByTestId('ri-install-line')).not.toBeInTheDocument()
})
it('should not render separator when download count is undefined', () => {
render(<CardMoreInfo tags={['tag1']} />)
expect(screen.queryByText('·')).not.toBeInTheDocument()
})
it('should not render separator when tags are empty', () => {
render(<CardMoreInfo downloadCount={100} tags={[]} />)
expect(screen.queryByText('·')).not.toBeInTheDocument()
})
it('should render hash symbol before each tag', () => {
render(<CardMoreInfo tags={['search']} />)
expect(screen.getByText('#')).toBeInTheDocument()
})
it('should set title attribute with hash prefix for tags', () => {
render(<CardMoreInfo tags={['search']} />)
const tagElement = screen.getByTitle('# search')
expect(tagElement).toBeInTheDocument()
expect(screen.getByText('ONE')).toBeInTheDocument()
expect(screen.getByText('TWO')).toBeInTheDocument()
expect(screen.queryByText('THREE')).not.toBeInTheDocument()
})
})
@ -722,54 +680,8 @@ describe('CardMoreInfo', () => {
// ================================
describe('Memoization', () => {
it('should be memoized with React.memo', () => {
expect(CardMoreInfo).toBeDefined()
expect(typeof CardMoreInfo).toBe('object')
})
})
// ================================
// Edge Cases Tests
// ================================
describe('Edge Cases', () => {
it('should handle zero download count', () => {
render(<CardMoreInfo downloadCount={0} tags={[]} />)
// 0 should still render since downloadCount is defined
expect(screen.getByText('0')).toBeInTheDocument()
})
it('should handle empty tags array', () => {
render(<CardMoreInfo downloadCount={100} tags={[]} />)
expect(screen.queryByText('#')).not.toBeInTheDocument()
})
it('should handle large download count', () => {
render(<CardMoreInfo downloadCount={1234567890} tags={[]} />)
expect(screen.getByText('1,234,567,890')).toBeInTheDocument()
})
it('should handle many tags', () => {
const tags = Array.from({ length: 10 }, (_, i) => `tag${i}`)
render(<CardMoreInfo downloadCount={100} tags={tags} />)
expect(screen.getByText('tag0')).toBeInTheDocument()
expect(screen.getByText('tag9')).toBeInTheDocument()
})
it('should handle tags with special characters', () => {
render(<CardMoreInfo tags={['tag-with-dash', 'tag_with_underscore']} />)
expect(screen.getByText('tag-with-dash')).toBeInTheDocument()
expect(screen.getByText('tag_with_underscore')).toBeInTheDocument()
})
it('should truncate long tag names', () => {
const longTag = 'a'.repeat(200)
const { container } = render(<CardMoreInfo tags={[longTag]} />)
expect(container.querySelector('.truncate')).toBeInTheDocument()
expect(CardTags).toBeDefined()
expect(typeof CardTags).toBe('object')
})
})
})
@ -1688,7 +1600,7 @@ describe('Icon', () => {
render(
<Card
payload={plugin}
footer={<CardMoreInfo downloadCount={5000} tags={['search', 'api']} />}
footer={<CardTags tags={['search', 'api']} />}
/>,
)
@ -1700,9 +1612,8 @@ describe('Icon', () => {
expect(screen.getByText('Tool')).toBeInTheDocument()
expect(screen.getByTestId('partner-badge')).toBeInTheDocument()
expect(screen.getByTestId('verified-badge')).toBeInTheDocument()
expect(screen.getByText('5,000')).toBeInTheDocument()
expect(screen.getByText('search')).toBeInTheDocument()
expect(screen.getByText('api')).toBeInTheDocument()
expect(screen.getByText('SEARCH')).toBeInTheDocument()
expect(screen.getByText('API')).toBeInTheDocument()
})
it('should render loading state correctly', () => {
@ -1728,12 +1639,12 @@ describe('Icon', () => {
<Card
payload={plugin}
installed={true}
footer={<CardMoreInfo downloadCount={100} tags={['tag1']} />}
footer={<CardTags tags={['tag1']} />}
/>,
)
expect(screen.getByTestId('ri-check-line')).toBeInTheDocument()
expect(screen.getByText('100')).toBeInTheDocument()
expect(screen.getByText('TAG1')).toBeInTheDocument()
})
})
@ -1817,9 +1728,9 @@ describe('Icon', () => {
})
it('should have title attribute on tags', () => {
render(<CardMoreInfo downloadCount={100} tags={['search']} />)
render(<CardTags tags={['search']} />)
expect(screen.getByTitle('# search')).toBeInTheDocument()
expect(screen.getByTitle('search')).toBeInTheDocument()
})
it('should have semantic structure', () => {
@ -1864,11 +1775,11 @@ describe('Icon', () => {
expect(endTime - startTime).toBeLessThan(1000)
})
it('should handle CardMoreInfo with many tags', () => {
it('should handle CardTags with many tags', () => {
const tags = Array.from({ length: 20 }, (_, i) => `tag-${i}`)
const startTime = performance.now()
render(<CardMoreInfo downloadCount={1000} tags={tags} />)
render(<CardTags tags={tags} />)
const endTime = performance.now()
expect(endTime - startTime).toBeLessThan(100)

View File

@ -322,11 +322,10 @@ vi.mock('@/app/components/plugins/card', () => ({
),
}))
// Mock CardMoreInfo component
vi.mock('@/app/components/plugins/card/card-more-info', () => ({
default: ({ downloadCount, tags }: { downloadCount: number, tags: string[] }) => (
<div data-testid="card-more-info">
<span data-testid="download-count">{downloadCount}</span>
// Mock CardTags component
vi.mock('@/app/components/plugins/card/card-tags', () => ({
default: ({ tags }: { tags: string[] }) => (
<div data-testid="card-tags">
<span data-testid="tags">{tags.join(',')}</span>
</div>
),

View File

@ -8,7 +8,7 @@ import * as React from 'react'
import { useMemo } from 'react'
import Button from '@/app/components/base/button'
import Card from '@/app/components/plugins/card'
import CardMoreInfo from '@/app/components/plugins/card/card-more-info'
import CardTags from '@/app/components/plugins/card/card-tags'
import { useTags } from '@/app/components/plugins/hooks'
import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace'
import { getPluginDetailLinkInMarketplace, getPluginLinkInMarketplace } from '../utils'
@ -49,7 +49,7 @@ const CardWrapperComponent = ({
key={plugin.name}
payload={plugin}
footer={(
<CardMoreInfo
<CardTags
tags={tagLabels}
/>
)}
@ -95,7 +95,7 @@ const CardWrapperComponent = ({
payload={plugin}
disableOrgLink
footer={(
<CardMoreInfo
<CardTags
tags={tagLabels}
/>
)}

View File

@ -131,11 +131,10 @@ vi.mock('@/app/components/plugins/card', () => ({
),
}))
// Mock CardMoreInfo component
vi.mock('@/app/components/plugins/card/card-more-info', () => ({
default: ({ downloadCount, tags }: { downloadCount: number, tags: string[] }) => (
<div data-testid="card-more-info">
<span data-testid="download-count">{downloadCount}</span>
// Mock CardTags component
vi.mock('@/app/components/plugins/card/card-tags', () => ({
default: ({ tags }: { tags: string[] }) => (
<div data-testid="card-tags">
<span data-testid="tags">{tags.join(',')}</span>
</div>
),
@ -983,7 +982,7 @@ describe('CardWrapper (via List integration)', () => {
expect(screen.getByTestId('card-test-plugin')).toBeInTheDocument()
})
it('should render CardMoreInfo with download count and tags', () => {
it('should render CardTags with tags', () => {
const plugin = createMockPlugin({
name: 'test-plugin',
install_count: 5000,
@ -998,8 +997,7 @@ describe('CardWrapper (via List integration)', () => {
/>,
)
expect(screen.getByTestId('card-more-info')).toBeInTheDocument()
expect(screen.getByTestId('download-count')).toHaveTextContent('5000')
expect(screen.getByTestId('card-tags')).toBeInTheDocument()
})
})

View File

@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next'
import Input from '@/app/components/base/input'
import TabSliderNew from '@/app/components/base/tab-slider-new'
import Card from '@/app/components/plugins/card'
import CardMoreInfo from '@/app/components/plugins/card/card-more-info'
import CardTags from '@/app/components/plugins/card/card-tags'
import { useTags } from '@/app/components/plugins/hooks'
import Empty from '@/app/components/plugins/marketplace/empty'
import PluginDetailPanel from '@/app/components/plugins/plugin-detail-panel'
@ -183,7 +183,7 @@ const ProviderList = () => {
name: collection.plugin_id ? collection.plugin_id.split('/')[1] : collection.name,
} as any}
footer={(
<CardMoreInfo
<CardTags
tags={collection.labels?.map(label => getTagLabel(label)) || []}
/>
)}