import type { Plugin } from '../types' import { fireEvent, render, screen } from '@testing-library/react' import * as React from 'react' import { beforeEach, describe, expect, it, vi } from 'vitest' import { PluginCategoryEnum } from '../types' import PluginMutationModal from './index' // ================================ // Mock External Dependencies Only // ================================ // Mock react-i18next (translation hook) vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key: string) => key, }), })) // Mock useMixedTranslation hook vi.mock('../marketplace/hooks', () => ({ useMixedTranslation: (_locale?: string) => ({ t: (key: string, options?: { ns?: string }) => { const fullKey = options?.ns ? `${options.ns}.${key}` : key return fullKey }, }), })) // Mock useGetLanguage context vi.mock('@/context/i18n', () => ({ useGetLanguage: () => 'en-US', })) // Mock useTheme hook vi.mock('@/hooks/use-theme', () => ({ default: () => ({ theme: 'light' }), })) // Mock i18n-config vi.mock('@/i18n-config', () => ({ renderI18nObject: (obj: Record, locale: string) => { return obj?.[locale] || obj?.['en-US'] || '' }, })) // Mock i18n-config/language vi.mock('@/i18n-config/language', () => ({ getLanguage: (locale: string) => locale || 'en-US', })) // Mock useCategories hook 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, }), })) // Mock formatNumber utility vi.mock('@/utils/format', () => ({ formatNumber: (num: number) => num.toLocaleString(), })) // Mock shouldUseMcpIcon utility vi.mock('@/utils/mcp', () => ({ shouldUseMcpIcon: (src: unknown) => typeof src === 'object' && src !== null && (src as { content?: string })?.content === '🔗', })) // Mock AppIcon component 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}
}
), })) // Mock Mcp icon component vi.mock('@/app/components/base/icons/src/vender/other', () => ({ Mcp: ({ className }: { className?: string }) => (
MCP
), Group: ({ className }: { className?: string }) => (
Group
), })) // Mock LeftCorner icon component vi.mock('../../base/icons/src/vender/plugin', () => ({ LeftCorner: ({ className }: { className?: string }) => (
LeftCorner
), })) // Mock Partner badge vi.mock('../base/badges/partner', () => ({ default: ({ className, text }: { className?: string, text?: string }) => (
Partner
), })) // Mock Verified badge vi.mock('../base/badges/verified', () => ({ default: ({ className, text }: { className?: string, text?: string }) => (
Verified
), })) // Mock Remix icons vi.mock('@remixicon/react', () => ({ RiCheckLine: ({ className }: { className?: string }) => ( ), RiCloseLine: ({ className }: { className?: string }) => ( ), RiInstallLine: ({ className }: { className?: string }) => ( ), RiAlertFill: ({ className }: { className?: string }) => ( ), RiLoader2Line: ({ className }: { className?: string }) => ( ), })) // Mock Skeleton components 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}
), })) // ================================ // Test Data Factories // ================================ 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, }) type MockMutation = { isSuccess: boolean isPending: boolean } const createMockMutation = ( overrides?: Partial, ): MockMutation => ({ isSuccess: false, isPending: false, ...overrides, }) type PluginMutationModalProps = { plugin: Plugin onCancel: () => void mutation: MockMutation mutate: () => void confirmButtonText: React.ReactNode cancelButtonText: React.ReactNode modelTitle: React.ReactNode description: React.ReactNode cardTitleLeft: React.ReactNode modalBottomLeft?: React.ReactNode } const createDefaultProps = ( overrides?: Partial, ): PluginMutationModalProps => ({ plugin: createMockPlugin(), onCancel: vi.fn(), mutation: createMockMutation(), mutate: vi.fn(), confirmButtonText: 'Confirm', cancelButtonText: 'Cancel', modelTitle: 'Modal Title', description: 'Modal Description', cardTitleLeft: null, ...overrides, }) // ================================ // PluginMutationModal Component Tests // ================================ describe('PluginMutationModal', () => { beforeEach(() => { vi.clearAllMocks() }) // ================================ // Rendering Tests // ================================ describe('Rendering', () => { it('should render without crashing', () => { const props = createDefaultProps() render() expect(document.body).toBeInTheDocument() }) it('should render modal title', () => { const props = createDefaultProps({ modelTitle: 'Update Plugin', }) render() expect(screen.getByText('Update Plugin')).toBeInTheDocument() }) it('should render description', () => { const props = createDefaultProps({ description: 'Are you sure you want to update this plugin?', }) render() expect( screen.getByText('Are you sure you want to update this plugin?'), ).toBeInTheDocument() }) it('should render plugin card with plugin info', () => { const plugin = createMockPlugin({ label: { 'en-US': 'My Test Plugin' }, brief: { 'en-US': 'A test plugin' }, }) const props = createDefaultProps({ plugin }) render() expect(screen.getByText('My Test Plugin')).toBeInTheDocument() expect(screen.getByText('A test plugin')).toBeInTheDocument() }) it('should render confirm button', () => { const props = createDefaultProps({ confirmButtonText: 'Install Now', }) render() expect( screen.getByRole('button', { name: /Install Now/i }), ).toBeInTheDocument() }) it('should render cancel button when not pending', () => { const props = createDefaultProps({ cancelButtonText: 'Cancel Installation', mutation: createMockMutation({ isPending: false }), }) render() expect( screen.getByRole('button', { name: /Cancel Installation/i }), ).toBeInTheDocument() }) it('should render modal with closable prop', () => { const props = createDefaultProps() render() // The modal should have a close button expect(screen.getByTestId('ri-close-line')).toBeInTheDocument() }) }) // ================================ // Props Testing // ================================ describe('Props', () => { it('should render cardTitleLeft when provided', () => { const props = createDefaultProps({ cardTitleLeft: v2.0.0, }) render() expect(screen.getByTestId('version-badge')).toBeInTheDocument() }) it('should render modalBottomLeft when provided', () => { const props = createDefaultProps({ modalBottomLeft: ( Additional Info ), }) render() expect(screen.getByTestId('bottom-left-content')).toBeInTheDocument() }) it('should not render modalBottomLeft when not provided', () => { const props = createDefaultProps({ modalBottomLeft: undefined, }) render() expect( screen.queryByTestId('bottom-left-content'), ).not.toBeInTheDocument() }) it('should render custom ReactNode for modelTitle', () => { const props = createDefaultProps({ modelTitle:
Custom Title Node
, }) render() expect(screen.getByTestId('custom-title')).toBeInTheDocument() }) it('should render custom ReactNode for description', () => { const props = createDefaultProps({ description: (
Warning: {' '} This action is irreversible.
), }) render() expect(screen.getByTestId('custom-description')).toBeInTheDocument() }) it('should render custom ReactNode for confirmButtonText', () => { const props = createDefaultProps({ confirmButtonText: ( {' '} Confirm Action ), }) render() expect(screen.getByTestId('confirm-icon')).toBeInTheDocument() }) it('should render custom ReactNode for cancelButtonText', () => { const props = createDefaultProps({ cancelButtonText: ( {' '} Abort ), }) render() expect(screen.getByTestId('cancel-icon')).toBeInTheDocument() }) }) // ================================ // User Interactions // ================================ describe('User Interactions', () => { it('should call onCancel when cancel button is clicked', () => { const onCancel = vi.fn() const props = createDefaultProps({ onCancel }) render() const cancelButton = screen.getByRole('button', { name: /Cancel/i }) fireEvent.click(cancelButton) expect(onCancel).toHaveBeenCalledTimes(1) }) it('should call mutate when confirm button is clicked', () => { const mutate = vi.fn() const props = createDefaultProps({ mutate }) render() const confirmButton = screen.getByRole('button', { name: /Confirm/i }) fireEvent.click(confirmButton) expect(mutate).toHaveBeenCalledTimes(1) }) it('should render close button in modal header', () => { const props = createDefaultProps() render() // Find the close icon - the Modal component handles the onClose callback const closeIcon = screen.getByTestId('ri-close-line') expect(closeIcon).toBeInTheDocument() }) it('should not call mutate when button is disabled during pending', () => { const mutate = vi.fn() const props = createDefaultProps({ mutate, mutation: createMockMutation({ isPending: true }), }) render() const confirmButton = screen.getByRole('button', { name: /Confirm/i }) expect(confirmButton).toBeDisabled() fireEvent.click(confirmButton) // Button is disabled, so mutate might still be called depending on implementation // The important thing is the button has disabled attribute expect(confirmButton).toHaveAttribute('disabled') }) }) // ================================ // Mutation State Tests // ================================ describe('Mutation States', () => { describe('when isPending is true', () => { it('should hide cancel button', () => { const props = createDefaultProps({ mutation: createMockMutation({ isPending: true }), }) render() expect( screen.queryByRole('button', { name: /Cancel/i }), ).not.toBeInTheDocument() }) it('should show loading state on confirm button', () => { const props = createDefaultProps({ mutation: createMockMutation({ isPending: true }), }) render() const confirmButton = screen.getByRole('button', { name: /Confirm/i }) expect(confirmButton).toBeDisabled() }) it('should disable confirm button', () => { const props = createDefaultProps({ mutation: createMockMutation({ isPending: true }), }) render() const confirmButton = screen.getByRole('button', { name: /Confirm/i }) expect(confirmButton).toBeDisabled() }) }) describe('when isPending is false', () => { it('should show cancel button', () => { const props = createDefaultProps({ mutation: createMockMutation({ isPending: false }), }) render() expect( screen.getByRole('button', { name: /Cancel/i }), ).toBeInTheDocument() }) it('should enable confirm button', () => { const props = createDefaultProps({ mutation: createMockMutation({ isPending: false }), }) render() const confirmButton = screen.getByRole('button', { name: /Confirm/i }) expect(confirmButton).not.toBeDisabled() }) }) describe('when isSuccess is true', () => { it('should show installed state on card', () => { const props = createDefaultProps({ mutation: createMockMutation({ isSuccess: true }), }) render() // The Card component should receive installed=true // This will show a check icon expect(screen.getByTestId('ri-check-line')).toBeInTheDocument() }) }) describe('when isSuccess is false', () => { it('should not show installed state on card', () => { const props = createDefaultProps({ mutation: createMockMutation({ isSuccess: false }), }) render() // The check icon should not be present (installed=false) expect(screen.queryByTestId('ri-check-line')).not.toBeInTheDocument() }) }) describe('state combinations', () => { it('should handle isPending=true and isSuccess=false', () => { const props = createDefaultProps({ mutation: createMockMutation({ isPending: true, isSuccess: false }), }) render() expect( screen.queryByRole('button', { name: /Cancel/i }), ).not.toBeInTheDocument() expect(screen.queryByTestId('ri-check-line')).not.toBeInTheDocument() }) it('should handle isPending=false and isSuccess=true', () => { const props = createDefaultProps({ mutation: createMockMutation({ isPending: false, isSuccess: true }), }) render() expect( screen.getByRole('button', { name: /Cancel/i }), ).toBeInTheDocument() expect(screen.getByTestId('ri-check-line')).toBeInTheDocument() }) it('should handle both isPending=true and isSuccess=true', () => { const props = createDefaultProps({ mutation: createMockMutation({ isPending: true, isSuccess: true }), }) render() expect( screen.queryByRole('button', { name: /Cancel/i }), ).not.toBeInTheDocument() expect(screen.getByTestId('ri-check-line')).toBeInTheDocument() }) }) }) // ================================ // Plugin Card Integration Tests // ================================ describe('Plugin Card Integration', () => { it('should display plugin label', () => { const plugin = createMockPlugin({ label: { 'en-US': 'Amazing Plugin' }, }) const props = createDefaultProps({ plugin }) render() expect(screen.getByText('Amazing Plugin')).toBeInTheDocument() }) it('should display plugin brief description', () => { const plugin = createMockPlugin({ brief: { 'en-US': 'This is an amazing plugin' }, }) const props = createDefaultProps({ plugin }) render() expect(screen.getByText('This is an amazing plugin')).toBeInTheDocument() }) it('should display plugin org and name', () => { const plugin = createMockPlugin({ org: 'my-organization', name: 'my-plugin-name', }) const props = createDefaultProps({ plugin }) render() expect(screen.getByText('my-organization')).toBeInTheDocument() expect(screen.getByText('my-plugin-name')).toBeInTheDocument() }) it('should display plugin category', () => { const plugin = createMockPlugin({ category: PluginCategoryEnum.model, }) const props = createDefaultProps({ plugin }) render() expect(screen.getByText('Model')).toBeInTheDocument() }) it('should display verified badge when plugin is verified', () => { const plugin = createMockPlugin({ verified: true, }) const props = createDefaultProps({ plugin }) render() expect(screen.getByTestId('verified-badge')).toBeInTheDocument() }) it('should display partner badge when plugin has partner badge', () => { const plugin = createMockPlugin({ badges: ['partner'], }) const props = createDefaultProps({ plugin }) render() expect(screen.getByTestId('partner-badge')).toBeInTheDocument() }) }) // ================================ // Memoization Tests // ================================ describe('Memoization', () => { it('should be memoized with React.memo', () => { // Verify the component is wrapped with memo expect(PluginMutationModal).toBeDefined() expect(typeof PluginMutationModal).toBe('object') }) it('should have displayName set', () => { // The component sets displayName = 'PluginMutationModal' const displayName = (PluginMutationModal as any).type?.displayName || (PluginMutationModal as any).displayName expect(displayName).toBe('PluginMutationModal') }) it('should not re-render when props unchanged', () => { const renderCount = vi.fn() const TestWrapper = ({ props }: { props: PluginMutationModalProps }) => { renderCount() return } const props = createDefaultProps() const { rerender } = render() expect(renderCount).toHaveBeenCalledTimes(1) // Re-render with same props reference rerender() expect(renderCount).toHaveBeenCalledTimes(2) }) }) // ================================ // Edge Cases Tests // ================================ describe('Edge Cases', () => { it('should handle empty label object', () => { const plugin = createMockPlugin({ label: {}, }) const props = createDefaultProps({ plugin }) render() expect(document.body).toBeInTheDocument() }) it('should handle empty brief object', () => { const plugin = createMockPlugin({ brief: {}, }) const props = createDefaultProps({ plugin }) render() expect(document.body).toBeInTheDocument() }) it('should handle plugin with undefined badges', () => { const plugin = createMockPlugin() // @ts-expect-error - Testing undefined badges plugin.badges = undefined const props = createDefaultProps({ plugin }) render() expect(document.body).toBeInTheDocument() }) it('should handle empty string description', () => { const props = createDefaultProps({ description: '', }) render() expect(document.body).toBeInTheDocument() }) it('should handle empty string modelTitle', () => { const props = createDefaultProps({ modelTitle: '', }) render() expect(document.body).toBeInTheDocument() }) it('should handle special characters in plugin name', () => { const plugin = createMockPlugin({ name: 'plugin-with-special!@#$%', org: 'org', }) const props = createDefaultProps({ plugin }) render() expect(screen.getByText('plugin-with-special!@#$%')).toBeInTheDocument() }) it('should handle very long title', () => { const longTitle = 'A'.repeat(500) const plugin = createMockPlugin({ label: { 'en-US': longTitle }, }) const props = createDefaultProps({ plugin }) render() // Should render the long title text expect(screen.getByText(longTitle)).toBeInTheDocument() }) it('should handle very long description', () => { const longDescription = 'B'.repeat(1000) const plugin = createMockPlugin({ brief: { 'en-US': longDescription }, }) const props = createDefaultProps({ plugin }) render() // Should render the long description text expect(screen.getByText(longDescription)).toBeInTheDocument() }) it('should handle unicode characters in title', () => { const props = createDefaultProps({ modelTitle: '更新插件 🎉', }) render() expect(screen.getByText('更新插件 🎉')).toBeInTheDocument() }) it('should handle unicode characters in description', () => { const props = createDefaultProps({ description: '确定要更新这个插件吗?この操作は元に戻せません。', }) render() expect( screen.getByText('确定要更新这个插件吗?この操作は元に戻せません。'), ).toBeInTheDocument() }) it('should handle null cardTitleLeft', () => { const props = createDefaultProps({ cardTitleLeft: null, }) render() expect(document.body).toBeInTheDocument() }) it('should handle undefined modalBottomLeft', () => { const props = createDefaultProps({ modalBottomLeft: undefined, }) render() expect(document.body).toBeInTheDocument() }) }) // ================================ // Modal Behavior Tests // ================================ describe('Modal Behavior', () => { it('should render modal with isShow=true', () => { const props = createDefaultProps() render() // Modal should be visible - check for dialog role using screen query expect(screen.getByRole('dialog')).toBeInTheDocument() }) it('should have modal structure', () => { const props = createDefaultProps() render() // Check that modal content is rendered expect(screen.getByRole('dialog')).toBeInTheDocument() // Modal should have title expect(screen.getByText('Modal Title')).toBeInTheDocument() }) it('should render modal as closable', () => { const props = createDefaultProps() render() // Close icon should be present expect(screen.getByTestId('ri-close-line')).toBeInTheDocument() }) }) // ================================ // Button Styling Tests // ================================ describe('Button Styling', () => { it('should render confirm button with primary variant', () => { const props = createDefaultProps() render() const confirmButton = screen.getByRole('button', { name: /Confirm/i }) // Button component with variant="primary" should have primary styling expect(confirmButton).toBeInTheDocument() }) it('should render cancel button with default variant', () => { const props = createDefaultProps() render() const cancelButton = screen.getByRole('button', { name: /Cancel/i }) expect(cancelButton).toBeInTheDocument() }) }) // ================================ // Layout Tests // ================================ describe('Layout', () => { it('should render description text', () => { const props = createDefaultProps({ description: 'Test Description Content', }) render() // Description should be rendered expect(screen.getByText('Test Description Content')).toBeInTheDocument() }) it('should render card with plugin info', () => { const plugin = createMockPlugin({ label: { 'en-US': 'Layout Test Plugin' }, }) const props = createDefaultProps({ plugin }) render() // Card should display plugin info expect(screen.getByText('Layout Test Plugin')).toBeInTheDocument() }) it('should render both cancel and confirm buttons', () => { const props = createDefaultProps() render() // Both buttons should be rendered expect(screen.getByRole('button', { name: /Cancel/i })).toBeInTheDocument() expect(screen.getByRole('button', { name: /Confirm/i })).toBeInTheDocument() }) it('should render buttons in correct order', () => { const props = createDefaultProps() render() // Get all buttons and verify order const buttons = screen.getAllByRole('button') // Cancel button should come before Confirm button const cancelIndex = buttons.findIndex(b => b.textContent?.includes('Cancel')) const confirmIndex = buttons.findIndex(b => b.textContent?.includes('Confirm')) expect(cancelIndex).toBeLessThan(confirmIndex) }) }) // ================================ // Accessibility Tests // ================================ describe('Accessibility', () => { it('should have accessible dialog role', () => { const props = createDefaultProps() render() expect(screen.getByRole('dialog')).toBeInTheDocument() }) it('should have accessible button roles', () => { const props = createDefaultProps() render() expect(screen.getAllByRole('button').length).toBeGreaterThan(0) }) it('should have accessible text content', () => { const props = createDefaultProps({ modelTitle: 'Accessible Title', description: 'Accessible Description', }) render() expect(screen.getByText('Accessible Title')).toBeInTheDocument() expect(screen.getByText('Accessible Description')).toBeInTheDocument() }) }) // ================================ // All Plugin Categories Tests // ================================ describe('All Plugin Categories', () => { const categories = [ { category: PluginCategoryEnum.tool, label: 'Tool' }, { category: PluginCategoryEnum.model, label: 'Model' }, { category: PluginCategoryEnum.extension, label: 'Extension' }, { category: PluginCategoryEnum.agent, label: 'Agent' }, { category: PluginCategoryEnum.datasource, label: 'Datasource' }, { category: PluginCategoryEnum.trigger, label: 'Trigger' }, ] categories.forEach(({ category, label }) => { it(`should display ${label} category correctly`, () => { const plugin = createMockPlugin({ category }) const props = createDefaultProps({ plugin }) render() expect(screen.getByText(label)).toBeInTheDocument() }) }) }) // ================================ // Bundle Type Tests // ================================ describe('Bundle Type', () => { it('should display bundle label for bundle type plugin', () => { const plugin = createMockPlugin({ type: 'bundle', category: PluginCategoryEnum.tool, }) const props = createDefaultProps({ plugin }) render() // For bundle type, should show 'Bundle' instead of category expect(screen.getByText('Bundle')).toBeInTheDocument() }) }) // ================================ // Event Handler Isolation Tests // ================================ describe('Event Handler Isolation', () => { it('should not call mutate when clicking cancel button', () => { const mutate = vi.fn() const onCancel = vi.fn() const props = createDefaultProps({ mutate, onCancel }) render() const cancelButton = screen.getByRole('button', { name: /Cancel/i }) fireEvent.click(cancelButton) expect(onCancel).toHaveBeenCalledTimes(1) expect(mutate).not.toHaveBeenCalled() }) it('should not call onCancel when clicking confirm button', () => { const mutate = vi.fn() const onCancel = vi.fn() const props = createDefaultProps({ mutate, onCancel }) render() const confirmButton = screen.getByRole('button', { name: /Confirm/i }) fireEvent.click(confirmButton) expect(mutate).toHaveBeenCalledTimes(1) expect(onCancel).not.toHaveBeenCalled() }) }) // ================================ // Multiple Renders Tests // ================================ describe('Multiple Renders', () => { it('should handle rapid state changes', () => { const props = createDefaultProps() const { rerender } = render() // Simulate rapid pending state changes rerender( , ) rerender( , ) rerender( , ) // Should show success state expect(screen.getByTestId('ri-check-line')).toBeInTheDocument() }) it('should handle plugin prop changes', () => { const plugin1 = createMockPlugin({ label: { 'en-US': 'Plugin One' } }) const plugin2 = createMockPlugin({ label: { 'en-US': 'Plugin Two' } }) const props = createDefaultProps({ plugin: plugin1 }) const { rerender } = render() expect(screen.getByText('Plugin One')).toBeInTheDocument() rerender() expect(screen.getByText('Plugin Two')).toBeInTheDocument() }) }) })