import type { Plugin } from '../../types' import { render, screen } from '@testing-library/react' import * as React from 'react' import { beforeEach, describe, expect, it, vi } from 'vitest' import { API_PREFIX, MARKETPLACE_API_PREFIX } from '@/config' import { PluginCategoryEnum } from '../../types' import Card from '../index' let mockTheme = 'light' vi.mock('@/hooks/use-theme', () => ({ default: () => ({ theme: mockTheme }), })) vi.mock('@/i18n-config', () => ({ renderI18nObject: (obj: Record, locale: string) => { return obj?.[locale] || obj?.['en-US'] || '' }, })) vi.mock('@/i18n-config/language', () => ({ getLanguage: (locale: string) => locale || 'en-US', })) const mockCategoriesMap: Record = { 'tool': { label: 'Tool' }, 'model': { label: 'Model' }, 'extension': { label: 'Extension' }, 'agent-strategy': { label: 'Agent' }, 'datasource': { label: 'Datasource' }, 'trigger': { label: 'Trigger' }, 'bundle': { label: 'Bundle' }, } vi.mock('../../hooks', () => ({ useCategories: () => ({ categoriesMap: mockCategoriesMap, }), })) vi.mock('@/utils/format', () => ({ formatNumber: (num: number) => num.toLocaleString(), })) vi.mock('@/context/app-context', () => ({ useSelector: (selector: (value: { currentWorkspace: { id: string } }) => string) => selector({ currentWorkspace: { id: 'workspace-123' }, }), })) vi.mock('@/utils/mcp', () => ({ shouldUseMcpIcon: (src: unknown) => typeof src === 'object' && src !== null && (src as { content?: string })?.content === '🔗', })) vi.mock('@/app/components/base/app-icon', () => ({ default: ({ icon, background, innerIcon, size, iconType }: { icon?: string background?: string innerIcon?: React.ReactNode size?: string iconType?: string }) => (
{!!innerIcon &&
{innerIcon}
}
), })) vi.mock('@/app/components/base/icons/src/vender/other', () => ({ Mcp: ({ className }: { className?: string }) => (
MCP
), Group: ({ className }: { className?: string }) => (
Group
), })) vi.mock('../../../base/icons/src/vender/plugin', () => ({ LeftCorner: ({ className }: { className?: string }) => (
LeftCorner
), })) vi.mock('../../base/badges/partner', () => ({ default: ({ className, text }: { className?: string, text?: string }) => (
Partner
), })) vi.mock('../../base/badges/verified', () => ({ default: ({ className, text }: { className?: string, text?: string }) => (
Verified
), })) vi.mock('@/app/components/base/skeleton', () => ({ SkeletonContainer: ({ children }: { children: React.ReactNode }) => (
{children}
), SkeletonPoint: () =>
, SkeletonRectangle: ({ className }: { className?: string }) => (
), SkeletonRow: ({ children, className }: { children: React.ReactNode, className?: string }) => (
{children}
), })) const createMockPlugin = (overrides?: Partial): Plugin => ({ type: 'plugin', org: 'test-org', name: 'test-plugin', plugin_id: 'plugin-123', version: '1.0.0', latest_version: '1.0.0', latest_package_identifier: 'test-org/test-plugin:1.0.0', icon: '/test-icon.png', verified: false, label: { 'en-US': 'Test Plugin' }, brief: { 'en-US': 'Test plugin description' }, description: { 'en-US': 'Full test plugin description' }, introduction: 'Test plugin introduction', repository: 'https://github.com/test/plugin', category: PluginCategoryEnum.tool, install_count: 1000, endpoint: { settings: [] }, tags: [{ name: 'search' }], badges: [], verification: { authorized_category: 'community' }, from: 'marketplace', ...overrides, }) describe('Card', () => { beforeEach(() => { vi.clearAllMocks() }) // ================================ // Rendering Tests // ================================ describe('Rendering', () => { it('should render without crashing', () => { const plugin = createMockPlugin() render() expect(document.body).toBeInTheDocument() }) it('should render plugin title from label', () => { const plugin = createMockPlugin({ label: { 'en-US': 'My Plugin Title' }, }) render() expect(screen.getByText('My Plugin Title')).toBeInTheDocument() }) it('should render plugin description from brief', () => { const plugin = createMockPlugin({ brief: { 'en-US': 'This is a brief description' }, }) render() expect(screen.getByText('This is a brief description')).toBeInTheDocument() }) it('should render organization info with org name and package name', () => { const plugin = createMockPlugin({ org: 'my-org', name: 'my-plugin', }) render() expect(screen.getByText('my-org')).toBeInTheDocument() expect(screen.getByText('my-plugin')).toBeInTheDocument() }) it('should render plugin icon', () => { const plugin = createMockPlugin({ icon: '/custom-icon.png', }) const { container } = render() // Check for background image style on icon element const iconElement = container.querySelector('[style*="background-image"]') expect(iconElement).toBeInTheDocument() }) it('should normalize package icon filenames to workspace icon urls', () => { const plugin = createMockPlugin({ from: 'package', icon: 'custom-icon.png', }) const { container } = render() const iconElement = container.querySelector('[style*="background-image"]') expect(iconElement).toBeInTheDocument() expect(iconElement).toHaveStyle({ backgroundImage: `url(${API_PREFIX}/workspaces/current/plugin/icon?tenant_id=workspace-123&filename=custom-icon.png)`, }) }) it('should normalize marketplace icon filenames to marketplace icon urls', () => { const plugin = createMockPlugin({ from: 'marketplace', icon: 'custom-icon.png', }) const { container } = render() const iconElement = container.querySelector('[style*="background-image"]') expect(iconElement).toBeInTheDocument() expect(iconElement).toHaveStyle({ backgroundImage: `url(${MARKETPLACE_API_PREFIX}/plugins/${plugin.org}/${plugin.name}/icon)`, }) }) it('should use icon_dark when theme is dark and icon_dark is provided', () => { // Set theme to dark mockTheme = 'dark' const plugin = createMockPlugin({ icon: '/light-icon.png', icon_dark: '/dark-icon.png', }) const { container } = render() // Check that icon uses dark icon const iconElement = container.querySelector('[style*="background-image"]') expect(iconElement).toBeInTheDocument() expect(iconElement).toHaveStyle({ backgroundImage: 'url(/dark-icon.png)' }) // Reset theme mockTheme = 'light' }) it('should use icon when theme is dark but icon_dark is not provided', () => { mockTheme = 'dark' const plugin = createMockPlugin({ icon: '/light-icon.png', }) const { container } = render() // Should fallback to light icon const iconElement = container.querySelector('[style*="background-image"]') expect(iconElement).toBeInTheDocument() expect(iconElement).toHaveStyle({ backgroundImage: 'url(/light-icon.png)' }) mockTheme = 'light' }) it('should render corner mark with category label', () => { const plugin = createMockPlugin({ category: PluginCategoryEnum.tool, }) render() expect(screen.getByText('Tool')).toBeInTheDocument() }) }) // ================================ // Props Testing // ================================ describe('Props', () => { it('should apply custom className', () => { const plugin = createMockPlugin() const { container } = render( , ) expect(container.querySelector('.custom-class')).toBeInTheDocument() }) it('should hide corner mark when hideCornerMark is true', () => { const plugin = createMockPlugin({ category: PluginCategoryEnum.tool, }) render() expect(screen.queryByTestId('left-corner')).not.toBeInTheDocument() }) it('should show corner mark by default', () => { const plugin = createMockPlugin() render() expect(screen.getByTestId('left-corner')).toBeInTheDocument() }) it('should pass installed prop to Icon component', () => { const plugin = createMockPlugin() const { container } = render() expect(container.querySelector('.bg-state-success-solid')).toBeInTheDocument() }) it('should pass installFailed prop to Icon component', () => { const plugin = createMockPlugin() const { container } = render() expect(container.querySelector('.bg-state-destructive-solid')).toBeInTheDocument() }) it('should render footer when provided', () => { const plugin = createMockPlugin() render( Footer Content
} />, ) expect(screen.getByTestId('custom-footer')).toBeInTheDocument() expect(screen.getByText('Footer Content')).toBeInTheDocument() }) it('should render titleLeft when provided', () => { const plugin = createMockPlugin() render( v1.0} />, ) expect(screen.getByTestId('title-left')).toBeInTheDocument() }) it('should use custom descriptionLineRows', () => { const plugin = createMockPlugin() const { container } = render( , ) // Check for h-4 truncate class when descriptionLineRows is 1 expect(container.querySelector('.h-4.truncate')).toBeInTheDocument() }) it('should use default descriptionLineRows of 2', () => { const plugin = createMockPlugin() const { container } = render() // Check for h-8 line-clamp-2 class when descriptionLineRows is 2 (default) expect(container.querySelector('.h-8.line-clamp-2')).toBeInTheDocument() }) }) // ================================ // Loading State Tests // ================================ describe('Loading State', () => { it('should render Placeholder when isLoading is true', () => { const plugin = createMockPlugin() render() // Should render skeleton elements expect(screen.getByTestId('skeleton-container')).toBeInTheDocument() }) it('should render loadingFileName in Placeholder', () => { const plugin = createMockPlugin() render() expect(screen.getByText('my-plugin.zip')).toBeInTheDocument() }) it('should not render card content when loading', () => { const plugin = createMockPlugin({ label: { 'en-US': 'Plugin Title' }, }) render() // Plugin content should not be visible during loading expect(screen.queryByText('Plugin Title')).not.toBeInTheDocument() }) it('should not render loading state by default', () => { const plugin = createMockPlugin() render() expect(screen.queryByTestId('skeleton-container')).not.toBeInTheDocument() }) }) // ================================ // Badges Tests // ================================ describe('Badges', () => { it('should render Partner badge when badges includes partner', () => { const plugin = createMockPlugin({ badges: ['partner'], }) render() expect(screen.getByTestId('partner-badge')).toBeInTheDocument() }) it('should render Verified badge when verified is true', () => { const plugin = createMockPlugin({ verified: true, }) render() expect(screen.getByTestId('verified-badge')).toBeInTheDocument() }) it('should render both Partner and Verified badges', () => { const plugin = createMockPlugin({ badges: ['partner'], verified: true, }) render() expect(screen.getByTestId('partner-badge')).toBeInTheDocument() expect(screen.getByTestId('verified-badge')).toBeInTheDocument() }) it('should not render Partner badge when badges is empty', () => { const plugin = createMockPlugin({ badges: [], }) render() expect(screen.queryByTestId('partner-badge')).not.toBeInTheDocument() }) it('should not render Verified badge when verified is false', () => { const plugin = createMockPlugin({ verified: false, }) render() expect(screen.queryByTestId('verified-badge')).not.toBeInTheDocument() }) it('should handle undefined badges gracefully', () => { const plugin = createMockPlugin() // @ts-expect-error - Testing undefined badges plugin.badges = undefined render() expect(screen.queryByTestId('partner-badge')).not.toBeInTheDocument() }) }) // ================================ // Limited Install Warning Tests // ================================ describe('Limited Install Warning', () => { it('should render warning when limitedInstall is true', () => { const plugin = createMockPlugin() const { container } = render() expect(container.querySelector('.text-text-warning-secondary')).toBeInTheDocument() }) it('should not render warning by default', () => { const plugin = createMockPlugin() const { container } = render() expect(container.querySelector('.text-text-warning-secondary')).not.toBeInTheDocument() }) it('should apply limited padding when limitedInstall is true', () => { const plugin = createMockPlugin() const { container } = render() expect(container.querySelector('.pb-1')).toBeInTheDocument() }) }) // ================================ // Category Type Tests // ================================ describe('Category Types', () => { it('should display bundle label for bundle type', () => { const plugin = createMockPlugin({ type: 'bundle', category: PluginCategoryEnum.tool, }) render() // For bundle type, should show 'Bundle' instead of category expect(screen.getByText('Bundle')).toBeInTheDocument() }) it('should display category label for non-bundle types', () => { const plugin = createMockPlugin({ type: 'plugin', category: PluginCategoryEnum.model, }) render() expect(screen.getByText('Model')).toBeInTheDocument() }) }) // ================================ // Memoization Tests // ================================ describe('Memoization', () => { it('should be memoized with React.memo', () => { // Card is wrapped with React.memo expect(Card).toBeDefined() // The component should have the memo display name characteristic expect(typeof Card).toBe('object') }) it('should not re-render when props are the same', () => { const plugin = createMockPlugin() const renderCount = vi.fn() const TestWrapper = ({ p }: { p: Plugin }) => { renderCount() return } const { rerender } = render() expect(renderCount).toHaveBeenCalledTimes(1) // Re-render with same plugin reference rerender() expect(renderCount).toHaveBeenCalledTimes(2) }) }) // ================================ // Edge Cases Tests // ================================ describe('Edge Cases', () => { it('should handle empty label object', () => { const plugin = createMockPlugin({ label: {}, }) render() // Should render without crashing expect(document.body).toBeInTheDocument() }) it('should handle empty brief object', () => { const plugin = createMockPlugin({ brief: {}, }) render() expect(document.body).toBeInTheDocument() }) it('should handle undefined label', () => { const plugin = createMockPlugin() // @ts-expect-error - Testing undefined label plugin.label = undefined render() expect(document.body).toBeInTheDocument() }) it('should handle special characters in plugin name', () => { const plugin = createMockPlugin({ name: 'plugin-with-special-chars!@#$%', org: 'org', }) render() expect(screen.getByText('plugin-with-special-chars!@#$%')).toBeInTheDocument() }) it('should handle very long title', () => { const longTitle = 'A'.repeat(500) const plugin = createMockPlugin({ label: { 'en-US': longTitle }, }) const { container } = render() // Should have truncate class for long text expect(container.querySelector('.truncate')).toBeInTheDocument() }) it('should handle very long description', () => { const longDescription = 'B'.repeat(1000) const plugin = createMockPlugin({ brief: { 'en-US': longDescription }, }) const { container } = render() // Should have line-clamp class for long text expect(container.querySelector('.line-clamp-2')).toBeInTheDocument() }) }) })