feat: introduce CardTags component for displaying tags in cards

This commit is contained in:
yessenia 2026-02-06 00:34:59 +08:00
parent 6329647f3b
commit 9a698eaad9
6 changed files with 42 additions and 134 deletions

View File

@ -5,7 +5,7 @@ type Props = {
tags: string[] tags: string[]
} }
const CardMoreInfoComponent = ({ const CardTagsComponent = ({
tags, tags,
}: Props) => { }: Props) => {
return ( return (
@ -29,6 +29,6 @@ const CardMoreInfoComponent = ({
} }
// Memoize to prevent unnecessary re-renders when tags array hasn't changed // 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 OrgInfo from './base/org-info'
import Placeholder, { LoadingPlaceholder } from './base/placeholder' import Placeholder, { LoadingPlaceholder } from './base/placeholder'
import Title from './base/title' import Title from './base/title'
import CardMoreInfo from './card-more-info' import CardTags from './card-tags'
// ================================ // ================================
// Import Components Under Test // Import Components Under Test
// ================================ // ================================
@ -642,9 +642,9 @@ describe('Card', () => {
}) })
// ================================ // ================================
// CardMoreInfo Component Tests // CardTags Component Tests
// ================================ // ================================
describe('CardMoreInfo', () => { describe('CardTags', () => {
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks() vi.clearAllMocks()
}) })
@ -654,66 +654,24 @@ describe('CardMoreInfo', () => {
// ================================ // ================================
describe('Rendering', () => { describe('Rendering', () => {
it('should render without crashing', () => { it('should render without crashing', () => {
render(<CardMoreInfo downloadCount={100} tags={['tag1']} />) render(<CardTags tags={['tag1']} />)
expect(document.body).toBeInTheDocument() expect(document.body).toBeInTheDocument()
}) })
it('should render download count when provided', () => { it('should render tags in uppercase', () => {
render(<CardMoreInfo downloadCount={1000} tags={[]} />) 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', () => { it('should render at most two tags', () => {
render(<CardMoreInfo tags={['search', 'image']} />) render(<CardTags tags={['one', 'two', 'three']} />)
expect(screen.getByText('search')).toBeInTheDocument() expect(screen.getByText('ONE')).toBeInTheDocument()
expect(screen.getByText('image')).toBeInTheDocument() expect(screen.getByText('TWO')).toBeInTheDocument()
}) expect(screen.queryByText('THREE')).not.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()
}) })
}) })
@ -722,54 +680,8 @@ describe('CardMoreInfo', () => {
// ================================ // ================================
describe('Memoization', () => { describe('Memoization', () => {
it('should be memoized with React.memo', () => { it('should be memoized with React.memo', () => {
expect(CardMoreInfo).toBeDefined() expect(CardTags).toBeDefined()
expect(typeof CardMoreInfo).toBe('object') expect(typeof CardTags).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()
}) })
}) })
}) })
@ -1688,7 +1600,7 @@ describe('Icon', () => {
render( render(
<Card <Card
payload={plugin} 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.getByText('Tool')).toBeInTheDocument()
expect(screen.getByTestId('partner-badge')).toBeInTheDocument() expect(screen.getByTestId('partner-badge')).toBeInTheDocument()
expect(screen.getByTestId('verified-badge')).toBeInTheDocument() expect(screen.getByTestId('verified-badge')).toBeInTheDocument()
expect(screen.getByText('5,000')).toBeInTheDocument() expect(screen.getByText('SEARCH')).toBeInTheDocument()
expect(screen.getByText('search')).toBeInTheDocument() expect(screen.getByText('API')).toBeInTheDocument()
expect(screen.getByText('api')).toBeInTheDocument()
}) })
it('should render loading state correctly', () => { it('should render loading state correctly', () => {
@ -1728,12 +1639,12 @@ describe('Icon', () => {
<Card <Card
payload={plugin} payload={plugin}
installed={true} installed={true}
footer={<CardMoreInfo downloadCount={100} tags={['tag1']} />} footer={<CardTags tags={['tag1']} />}
/>, />,
) )
expect(screen.getByTestId('ri-check-line')).toBeInTheDocument() 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', () => { 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', () => { it('should have semantic structure', () => {
@ -1864,11 +1775,11 @@ describe('Icon', () => {
expect(endTime - startTime).toBeLessThan(1000) 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 tags = Array.from({ length: 20 }, (_, i) => `tag-${i}`)
const startTime = performance.now() const startTime = performance.now()
render(<CardMoreInfo downloadCount={1000} tags={tags} />) render(<CardTags tags={tags} />)
const endTime = performance.now() const endTime = performance.now()
expect(endTime - startTime).toBeLessThan(100) expect(endTime - startTime).toBeLessThan(100)

View File

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

View File

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

View File

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

View File

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