import type { PluginDeclaration, PluginDetail } from '../../types' import { render, screen } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' import { PluginCategoryEnum, PluginSource } from '../../types' // ==================== Imports (after mocks) ==================== import PluginList from './index' // ==================== Mock Setup ==================== // Mock PluginItem component to avoid complex dependency chain vi.mock('../../plugin-item', () => ({ default: ({ plugin }: { plugin: PluginDetail }) => (
{plugin.name}
), })) // ==================== Test Utilities ==================== /** * Factory function to create a PluginDeclaration with defaults */ const createPluginDeclaration = (overrides: Partial = {}): PluginDeclaration => ({ plugin_unique_identifier: 'test-plugin-id', version: '1.0.0', author: 'test-author', icon: 'test-icon.png', icon_dark: 'test-icon-dark.png', name: 'test-plugin', category: PluginCategoryEnum.tool, label: { en_US: 'Test Plugin' } as any, description: { en_US: 'Test plugin description' } as any, created_at: '2024-01-01', resource: null, plugins: null, verified: false, endpoint: {} as any, model: null, tags: [], agent_strategy: null, meta: { version: '1.0.0', minimum_dify_version: '0.5.0', }, trigger: {} as any, ...overrides, }) /** * Factory function to create a PluginDetail with defaults */ const createPluginDetail = (overrides: Partial = {}): PluginDetail => ({ id: 'plugin-1', created_at: '2024-01-01', updated_at: '2024-01-01', name: 'test-plugin', plugin_id: 'plugin-1', plugin_unique_identifier: 'test-author/test-plugin@1.0.0', declaration: createPluginDeclaration(), installation_id: 'install-1', tenant_id: 'tenant-1', endpoints_setups: 0, endpoints_active: 0, version: '1.0.0', latest_version: '1.0.0', latest_unique_identifier: 'test-author/test-plugin@1.0.0', source: PluginSource.marketplace, meta: { repo: 'test-author/test-plugin', version: '1.0.0', package: 'test-plugin.difypkg', }, status: 'active', deprecated_reason: '', alternative_plugin_id: '', ...overrides, }) /** * Factory function to create a list of plugins */ const createPluginList = (count: number, baseOverrides: Partial = {}): PluginDetail[] => { return Array.from({ length: count }, (_, index) => createPluginDetail({ id: `plugin-${index + 1}`, plugin_id: `plugin-${index + 1}`, name: `plugin-${index + 1}`, plugin_unique_identifier: `test-author/plugin-${index + 1}@1.0.0`, ...baseOverrides, })) } // ==================== Tests ==================== describe('PluginList', () => { beforeEach(() => { vi.clearAllMocks() }) // ==================== Rendering Tests ==================== describe('Rendering', () => { it('should render without crashing', () => { // Arrange const pluginList: PluginDetail[] = [] // Act const { container } = render() // Assert expect(container).toBeInTheDocument() }) it('should render container with correct structure', () => { // Arrange const pluginList: PluginDetail[] = [] // Act const { container } = render() // Assert const outerDiv = container.firstChild as HTMLElement expect(outerDiv).toHaveClass('pb-3') const gridDiv = outerDiv.firstChild as HTMLElement expect(gridDiv).toHaveClass('grid', 'grid-cols-2', 'gap-3') }) it('should render single plugin correctly', () => { // Arrange const pluginList = [createPluginDetail({ name: 'single-plugin' })] // Act render() // Assert const pluginItems = screen.getAllByTestId('plugin-item') expect(pluginItems).toHaveLength(1) expect(pluginItems[0]).toHaveAttribute('data-plugin-name', 'single-plugin') }) it('should render multiple plugins correctly', () => { // Arrange const pluginList = createPluginList(5) // Act render() // Assert const pluginItems = screen.getAllByTestId('plugin-item') expect(pluginItems).toHaveLength(5) }) it('should render plugins in correct order', () => { // Arrange const pluginList = [ createPluginDetail({ plugin_id: 'first', name: 'First Plugin' }), createPluginDetail({ plugin_id: 'second', name: 'Second Plugin' }), createPluginDetail({ plugin_id: 'third', name: 'Third Plugin' }), ] // Act render() // Assert const pluginItems = screen.getAllByTestId('plugin-item') expect(pluginItems[0]).toHaveAttribute('data-plugin-id', 'first') expect(pluginItems[1]).toHaveAttribute('data-plugin-id', 'second') expect(pluginItems[2]).toHaveAttribute('data-plugin-id', 'third') }) it('should pass plugin prop to each PluginItem', () => { // Arrange const pluginList = [ createPluginDetail({ plugin_id: 'plugin-a', name: 'Plugin A' }), createPluginDetail({ plugin_id: 'plugin-b', name: 'Plugin B' }), ] // Act render() // Assert expect(screen.getByText('Plugin A')).toBeInTheDocument() expect(screen.getByText('Plugin B')).toBeInTheDocument() }) }) // ==================== Props Testing ==================== describe('Props', () => { it('should accept empty pluginList array', () => { // Arrange & Act const { container } = render() // Assert const gridDiv = container.querySelector('.grid') expect(gridDiv).toBeEmptyDOMElement() }) it('should handle pluginList with various categories', () => { // Arrange const pluginList = [ createPluginDetail({ plugin_id: 'tool-plugin', declaration: createPluginDeclaration({ category: PluginCategoryEnum.tool }), }), createPluginDetail({ plugin_id: 'model-plugin', declaration: createPluginDeclaration({ category: PluginCategoryEnum.model }), }), createPluginDetail({ plugin_id: 'extension-plugin', declaration: createPluginDeclaration({ category: PluginCategoryEnum.extension }), }), ] // Act render() // Assert const pluginItems = screen.getAllByTestId('plugin-item') expect(pluginItems).toHaveLength(3) }) it('should handle pluginList with various sources', () => { // Arrange const pluginList = [ createPluginDetail({ plugin_id: 'marketplace-plugin', source: PluginSource.marketplace }), createPluginDetail({ plugin_id: 'github-plugin', source: PluginSource.github }), createPluginDetail({ plugin_id: 'local-plugin', source: PluginSource.local }), createPluginDetail({ plugin_id: 'debugging-plugin', source: PluginSource.debugging }), ] // Act render() // Assert const pluginItems = screen.getAllByTestId('plugin-item') expect(pluginItems).toHaveLength(4) }) }) // ==================== Edge Cases ==================== describe('Edge Cases', () => { it('should handle empty array', () => { // Arrange & Act render() // Assert expect(screen.queryByTestId('plugin-item')).not.toBeInTheDocument() }) it('should handle large number of plugins', () => { // Arrange const pluginList = createPluginList(100) // Act render() // Assert const pluginItems = screen.getAllByTestId('plugin-item') expect(pluginItems).toHaveLength(100) }) it('should handle plugins with duplicate plugin_ids (key warning scenario)', () => { // Arrange - Testing that the component uses plugin_id as key const pluginList = [ createPluginDetail({ plugin_id: 'unique-1', name: 'Plugin 1' }), createPluginDetail({ plugin_id: 'unique-2', name: 'Plugin 2' }), ] // Act & Assert - Should render without issues expect(() => render()).not.toThrow() expect(screen.getAllByTestId('plugin-item')).toHaveLength(2) }) it('should handle plugins with special characters in names', () => { // Arrange const pluginList = [ createPluginDetail({ plugin_id: 'special-1', name: 'Plugin "special" & chars' }), createPluginDetail({ plugin_id: 'special-2', name: '日本語プラグイン' }), createPluginDetail({ plugin_id: 'special-3', name: 'Emoji Plugin 🔌' }), ] // Act render() // Assert const pluginItems = screen.getAllByTestId('plugin-item') expect(pluginItems).toHaveLength(3) }) it('should handle plugins with very long names', () => { // Arrange const longName = 'A'.repeat(500) const pluginList = [createPluginDetail({ name: longName })] // Act render() // Assert expect(screen.getByTestId('plugin-item')).toBeInTheDocument() }) it('should handle plugin with minimal data', () => { // Arrange const minimalPlugin = createPluginDetail({ name: '', plugin_id: 'minimal', }) // Act render() // Assert expect(screen.getByTestId('plugin-item')).toBeInTheDocument() }) it('should handle plugins with undefined optional fields', () => { // Arrange const pluginList = [ createPluginDetail({ plugin_id: 'no-meta', meta: undefined, }), ] // Act render() // Assert expect(screen.getByTestId('plugin-item')).toBeInTheDocument() }) }) // ==================== Grid Layout Tests ==================== describe('Grid Layout', () => { it('should render with 2-column grid', () => { // Arrange const pluginList = createPluginList(4) // Act const { container } = render() // Assert const gridDiv = container.querySelector('.grid') expect(gridDiv).toHaveClass('grid-cols-2') }) it('should have proper gap between items', () => { // Arrange const pluginList = createPluginList(4) // Act const { container } = render() // Assert const gridDiv = container.querySelector('.grid') expect(gridDiv).toHaveClass('gap-3') }) it('should have bottom padding on container', () => { // Arrange const pluginList = createPluginList(2) // Act const { container } = render() // Assert const outerDiv = container.firstChild as HTMLElement expect(outerDiv).toHaveClass('pb-3') }) }) // ==================== Re-render Tests ==================== describe('Re-render Behavior', () => { it('should update when pluginList changes', () => { // Arrange const initialList = createPluginList(2) const updatedList = createPluginList(4) // Act const { rerender } = render() expect(screen.getAllByTestId('plugin-item')).toHaveLength(2) rerender() // Assert expect(screen.getAllByTestId('plugin-item')).toHaveLength(4) }) it('should handle pluginList update from non-empty to empty', () => { // Arrange const initialList = createPluginList(3) const emptyList: PluginDetail[] = [] // Act const { rerender } = render() expect(screen.getAllByTestId('plugin-item')).toHaveLength(3) rerender() // Assert expect(screen.queryByTestId('plugin-item')).not.toBeInTheDocument() }) it('should handle pluginList update from empty to non-empty', () => { // Arrange const emptyList: PluginDetail[] = [] const filledList = createPluginList(3) // Act const { rerender } = render() expect(screen.queryByTestId('plugin-item')).not.toBeInTheDocument() rerender() // Assert expect(screen.getAllByTestId('plugin-item')).toHaveLength(3) }) it('should update individual plugin data on re-render', () => { // Arrange const initialList = [createPluginDetail({ plugin_id: 'plugin-1', name: 'Original Name' })] const updatedList = [createPluginDetail({ plugin_id: 'plugin-1', name: 'Updated Name' })] // Act const { rerender } = render() expect(screen.getByText('Original Name')).toBeInTheDocument() rerender() // Assert expect(screen.getByText('Updated Name')).toBeInTheDocument() expect(screen.queryByText('Original Name')).not.toBeInTheDocument() }) }) // ==================== Key Prop Tests ==================== describe('Key Prop Behavior', () => { it('should use plugin_id as key for efficient re-renders', () => { // Arrange - Create plugins with unique plugin_ids const pluginList = [ createPluginDetail({ plugin_id: 'stable-key-1', name: 'Plugin 1' }), createPluginDetail({ plugin_id: 'stable-key-2', name: 'Plugin 2' }), createPluginDetail({ plugin_id: 'stable-key-3', name: 'Plugin 3' }), ] // Act const { rerender } = render() // Reorder the list const reorderedList = [pluginList[2], pluginList[0], pluginList[1]] rerender() // Assert - All items should still be present const items = screen.getAllByTestId('plugin-item') expect(items).toHaveLength(3) expect(items[0]).toHaveAttribute('data-plugin-id', 'stable-key-3') expect(items[1]).toHaveAttribute('data-plugin-id', 'stable-key-1') expect(items[2]).toHaveAttribute('data-plugin-id', 'stable-key-2') }) }) // ==================== Plugin Status Variations ==================== describe('Plugin Status Variations', () => { it('should render active plugins', () => { // Arrange const pluginList = [createPluginDetail({ status: 'active' })] // Act render() // Assert expect(screen.getByTestId('plugin-item')).toBeInTheDocument() }) it('should render deleted/deprecated plugins', () => { // Arrange const pluginList = [ createPluginDetail({ status: 'deleted', deprecated_reason: 'No longer maintained', }), ] // Act render() // Assert expect(screen.getByTestId('plugin-item')).toBeInTheDocument() }) it('should render mixed status plugins', () => { // Arrange const pluginList = [ createPluginDetail({ plugin_id: 'active-plugin', status: 'active' }), createPluginDetail({ plugin_id: 'deprecated-plugin', status: 'deleted', deprecated_reason: 'Deprecated', }), ] // Act render() // Assert expect(screen.getAllByTestId('plugin-item')).toHaveLength(2) }) }) // ==================== Version Variations ==================== describe('Version Variations', () => { it('should render plugins with same version as latest', () => { // Arrange const pluginList = [ createPluginDetail({ version: '1.0.0', latest_version: '1.0.0', }), ] // Act render() // Assert expect(screen.getByTestId('plugin-item')).toBeInTheDocument() }) it('should render plugins with outdated version', () => { // Arrange const pluginList = [ createPluginDetail({ version: '1.0.0', latest_version: '2.0.0', }), ] // Act render() // Assert expect(screen.getByTestId('plugin-item')).toBeInTheDocument() }) }) // ==================== Accessibility ==================== describe('Accessibility', () => { it('should render as a semantic container', () => { // Arrange const pluginList = createPluginList(2) // Act const { container } = render() // Assert - The list is rendered as divs which is appropriate for a grid layout const outerDiv = container.firstChild as HTMLElement expect(outerDiv.tagName).toBe('DIV') }) }) // ==================== Component Type ==================== describe('Component Type', () => { it('should be a functional component', () => { // Assert expect(typeof PluginList).toBe('function') }) it('should accept pluginList as required prop', () => { // Arrange & Act - TypeScript ensures this at compile time // but we verify runtime behavior const pluginList = createPluginList(1) // Assert expect(() => render()).not.toThrow() }) }) // ==================== Mixed Content Tests ==================== describe('Mixed Content', () => { it('should render plugins from different sources together', () => { // Arrange const pluginList = [ createPluginDetail({ plugin_id: 'marketplace-1', name: 'Marketplace Plugin', source: PluginSource.marketplace, }), createPluginDetail({ plugin_id: 'github-1', name: 'GitHub Plugin', source: PluginSource.github, }), createPluginDetail({ plugin_id: 'local-1', name: 'Local Plugin', source: PluginSource.local, }), ] // Act render() // Assert expect(screen.getByText('Marketplace Plugin')).toBeInTheDocument() expect(screen.getByText('GitHub Plugin')).toBeInTheDocument() expect(screen.getByText('Local Plugin')).toBeInTheDocument() }) it('should render plugins of different categories together', () => { // Arrange const pluginList = [ createPluginDetail({ plugin_id: 'tool-1', name: 'Tool Plugin', declaration: createPluginDeclaration({ category: PluginCategoryEnum.tool }), }), createPluginDetail({ plugin_id: 'model-1', name: 'Model Plugin', declaration: createPluginDeclaration({ category: PluginCategoryEnum.model }), }), createPluginDetail({ plugin_id: 'agent-1', name: 'Agent Plugin', declaration: createPluginDeclaration({ category: PluginCategoryEnum.agent }), }), ] // Act render() // Assert expect(screen.getByText('Tool Plugin')).toBeInTheDocument() expect(screen.getByText('Model Plugin')).toBeInTheDocument() expect(screen.getByText('Agent Plugin')).toBeInTheDocument() }) }) // ==================== Boundary Tests ==================== describe('Boundary Tests', () => { it('should handle single item list', () => { // Arrange const pluginList = createPluginList(1) // Act render() // Assert expect(screen.getAllByTestId('plugin-item')).toHaveLength(1) }) it('should handle two items (fills one row)', () => { // Arrange const pluginList = createPluginList(2) // Act render() // Assert expect(screen.getAllByTestId('plugin-item')).toHaveLength(2) }) it('should handle three items (partial second row)', () => { // Arrange const pluginList = createPluginList(3) // Act render() // Assert expect(screen.getAllByTestId('plugin-item')).toHaveLength(3) }) it('should handle odd number of items', () => { // Arrange const pluginList = createPluginList(7) // Act render() // Assert expect(screen.getAllByTestId('plugin-item')).toHaveLength(7) }) it('should handle even number of items', () => { // Arrange const pluginList = createPluginList(8) // Act render() // Assert expect(screen.getAllByTestId('plugin-item')).toHaveLength(8) }) }) })