From 434eb0e924b0be6defdd90019eb75e836cef052a Mon Sep 17 00:00:00 2001 From: yessenia Date: Fri, 6 Feb 2026 00:34:59 +0800 Subject: [PATCH] feat: introduce CardTags component for displaying tags in cards --- .../{card-more-info.tsx => card-tags.tsx} | 6 +- .../components/plugins/card/index.spec.tsx | 137 +++--------------- .../plugins/marketplace/index.spec.tsx | 9 +- .../plugins/marketplace/list/card-wrapper.tsx | 6 +- .../plugins/marketplace/list/index.spec.tsx | 14 +- web/app/components/tools/provider-list.tsx | 4 +- 6 files changed, 42 insertions(+), 134 deletions(-) rename web/app/components/plugins/card/{card-more-info.tsx => card-tags.tsx} (89%) diff --git a/web/app/components/plugins/card/card-more-info.tsx b/web/app/components/plugins/card/card-tags.tsx similarity index 89% rename from web/app/components/plugins/card/card-more-info.tsx rename to web/app/components/plugins/card/card-tags.tsx index 1f9cde9a22..0a8e1fccfa 100644 --- a/web/app/components/plugins/card/card-more-info.tsx +++ b/web/app/components/plugins/card/card-tags.tsx @@ -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 diff --git a/web/app/components/plugins/card/index.spec.tsx b/web/app/components/plugins/card/index.spec.tsx index 8406d6753d..0266c03740 100644 --- a/web/app/components/plugins/card/index.spec.tsx +++ b/web/app/components/plugins/card/index.spec.tsx @@ -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() + render() expect(document.body).toBeInTheDocument() }) - it('should render download count when provided', () => { - render() + it('should render tags in uppercase', () => { + render() - expect(screen.getByText('1,000')).toBeInTheDocument() + expect(screen.getByText('SEARCH')).toBeInTheDocument() + expect(screen.getByText('IMAGE')).toBeInTheDocument() }) - it('should render tags when provided', () => { - render() + it('should render at most two tags', () => { + render() - expect(screen.getByText('search')).toBeInTheDocument() - expect(screen.getByText('image')).toBeInTheDocument() - }) - - it('should render both download count and tags with separator', () => { - render() - - 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() - - expect(screen.queryByTestId('ri-install-line')).not.toBeInTheDocument() - }) - - it('should not render separator when download count is undefined', () => { - render() - - expect(screen.queryByText('·')).not.toBeInTheDocument() - }) - - it('should not render separator when tags are empty', () => { - render() - - expect(screen.queryByText('·')).not.toBeInTheDocument() - }) - - it('should render hash symbol before each tag', () => { - render() - - expect(screen.getByText('#')).toBeInTheDocument() - }) - - it('should set title attribute with hash prefix for tags', () => { - render() - - 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() - - // 0 should still render since downloadCount is defined - expect(screen.getByText('0')).toBeInTheDocument() - }) - - it('should handle empty tags array', () => { - render() - - expect(screen.queryByText('#')).not.toBeInTheDocument() - }) - - it('should handle large download count', () => { - render() - - expect(screen.getByText('1,234,567,890')).toBeInTheDocument() - }) - - it('should handle many tags', () => { - const tags = Array.from({ length: 10 }, (_, i) => `tag${i}`) - render() - - expect(screen.getByText('tag0')).toBeInTheDocument() - expect(screen.getByText('tag9')).toBeInTheDocument() - }) - - it('should handle tags with special characters', () => { - render() - - 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() - - expect(container.querySelector('.truncate')).toBeInTheDocument() + expect(CardTags).toBeDefined() + expect(typeof CardTags).toBe('object') }) }) }) @@ -1688,7 +1600,7 @@ describe('Icon', () => { render( } + footer={} />, ) @@ -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', () => { } + footer={} />, ) 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() + render() - 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() + render() const endTime = performance.now() expect(endTime - startTime).toBeLessThan(100) diff --git a/web/app/components/plugins/marketplace/index.spec.tsx b/web/app/components/plugins/marketplace/index.spec.tsx index 1c0c700177..9cbe2c0c7d 100644 --- a/web/app/components/plugins/marketplace/index.spec.tsx +++ b/web/app/components/plugins/marketplace/index.spec.tsx @@ -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[] }) => ( -
- {downloadCount} +// Mock CardTags component +vi.mock('@/app/components/plugins/card/card-tags', () => ({ + default: ({ tags }: { tags: string[] }) => ( +
{tags.join(',')}
), diff --git a/web/app/components/plugins/marketplace/list/card-wrapper.tsx b/web/app/components/plugins/marketplace/list/card-wrapper.tsx index 0328d1952e..35895641b1 100644 --- a/web/app/components/plugins/marketplace/list/card-wrapper.tsx +++ b/web/app/components/plugins/marketplace/list/card-wrapper.tsx @@ -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={( - )} @@ -95,7 +95,7 @@ const CardWrapperComponent = ({ payload={plugin} disableOrgLink footer={( - )} diff --git a/web/app/components/plugins/marketplace/list/index.spec.tsx b/web/app/components/plugins/marketplace/list/index.spec.tsx index 31419030a4..01fffc0c66 100644 --- a/web/app/components/plugins/marketplace/list/index.spec.tsx +++ b/web/app/components/plugins/marketplace/list/index.spec.tsx @@ -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[] }) => ( -
- {downloadCount} +// Mock CardTags component +vi.mock('@/app/components/plugins/card/card-tags', () => ({ + default: ({ tags }: { tags: string[] }) => ( +
{tags.join(',')}
), @@ -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() }) }) diff --git a/web/app/components/tools/provider-list.tsx b/web/app/components/tools/provider-list.tsx index 48fd4ef29d..6cc089c96a 100644 --- a/web/app/components/tools/provider-list.tsx +++ b/web/app/components/tools/provider-list.tsx @@ -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={( - getTagLabel(label)) || []} /> )}