import type { PluginDeclaration } from '../../../types' import { fireEvent, render, screen, waitFor } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' import { PluginCategoryEnum, TaskStatus } from '../../../types' import Install from './install' // Factory function for test data const createMockManifest = (overrides: Partial = {}): PluginDeclaration => ({ plugin_unique_identifier: 'test-plugin-uid', version: '1.0.0', author: 'test-author', icon: 'test-icon.png', name: 'Test Plugin', category: PluginCategoryEnum.tool, label: { 'en-US': 'Test Plugin' } as PluginDeclaration['label'], description: { 'en-US': 'A test plugin' } as PluginDeclaration['description'], created_at: '2024-01-01T00:00:00Z', resource: {}, plugins: [], verified: true, endpoint: { settings: [], endpoints: [] }, model: null, tags: [], agent_strategy: null, meta: { version: '1.0.0', minimum_dify_version: '0.8.0' }, trigger: {} as PluginDeclaration['trigger'], ...overrides, }) // Mock external dependencies const mockUseCheckInstalled = vi.fn() vi.mock('@/app/components/plugins/install-plugin/hooks/use-check-installed', () => ({ default: () => mockUseCheckInstalled(), })) const mockInstallPackageFromLocal = vi.fn() vi.mock('@/service/use-plugins', () => ({ useInstallPackageFromLocal: () => ({ mutateAsync: mockInstallPackageFromLocal, }), usePluginTaskList: () => ({ handleRefetch: vi.fn(), }), })) const mockUninstallPlugin = vi.fn() vi.mock('@/service/plugins', () => ({ uninstallPlugin: (...args: unknown[]) => mockUninstallPlugin(...args), })) const mockCheck = vi.fn() const mockStop = vi.fn() vi.mock('../../base/check-task-status', () => ({ default: () => ({ check: mockCheck, stop: mockStop, }), })) const mockLangGeniusVersionInfo = { current_version: '1.0.0' } vi.mock('@/context/app-context', () => ({ useAppContext: () => ({ langGeniusVersionInfo: mockLangGeniusVersionInfo, }), })) vi.mock('react-i18next', async (importOriginal) => { const actual = await importOriginal() const { createReactI18nextMock } = await import('@/test/i18n-mock') return { ...actual, ...createReactI18nextMock(), Trans: ({ i18nKey, components }: { i18nKey: string, components?: Record }) => ( {i18nKey} {components?.trustSource} ), } }) vi.mock('../../../card', () => ({ default: ({ payload, titleLeft }: { payload: Record titleLeft?: React.ReactNode }) => (
{payload?.name as string}
{titleLeft}
), })) vi.mock('../../base/version', () => ({ default: ({ hasInstalled, installedVersion, toInstallVersion }: { hasInstalled: boolean installedVersion?: string toInstallVersion: string }) => (
{hasInstalled ? 'true' : 'false'} {installedVersion || 'null'} {toInstallVersion}
), })) vi.mock('../../utils', () => ({ pluginManifestToCardPluginProps: (manifest: PluginDeclaration) => ({ name: manifest.name, author: manifest.author, version: manifest.version, }), })) describe('Install', () => { const defaultProps = { uniqueIdentifier: 'test-unique-identifier', payload: createMockManifest(), onCancel: vi.fn(), onStartToInstall: vi.fn(), onInstalled: vi.fn(), onFailed: vi.fn(), } beforeEach(() => { vi.clearAllMocks() mockUseCheckInstalled.mockReturnValue({ installedInfo: null, isLoading: false, }) mockInstallPackageFromLocal.mockReset() mockUninstallPlugin.mockReset() mockCheck.mockReset() mockStop.mockReset() }) // ================================ // Rendering Tests // ================================ describe('Rendering', () => { it('should render ready to install message', () => { render() expect(screen.getByText('plugin.installModal.readyToInstall')).toBeInTheDocument() }) it('should render trust source message', () => { render() expect(screen.getByTestId('trans')).toBeInTheDocument() }) it('should render plugin card', () => { render() expect(screen.getByTestId('card')).toBeInTheDocument() expect(screen.getByTestId('card-name')).toHaveTextContent('Test Plugin') }) it('should render cancel button', () => { render() expect(screen.getByRole('button', { name: 'common.operation.cancel' })).toBeInTheDocument() }) it('should render install button', () => { render() expect(screen.getByRole('button', { name: 'plugin.installModal.install' })).toBeInTheDocument() }) it('should show version component when not loading', () => { mockUseCheckInstalled.mockReturnValue({ installedInfo: null, isLoading: false, }) render() expect(screen.getByTestId('version')).toBeInTheDocument() }) it('should not show version component when loading', () => { mockUseCheckInstalled.mockReturnValue({ installedInfo: null, isLoading: true, }) render() expect(screen.queryByTestId('version')).not.toBeInTheDocument() }) }) // ================================ // Version Display Tests // ================================ describe('Version Display', () => { it('should display toInstallVersion from payload', () => { const payload = createMockManifest({ version: '2.0.0' }) mockUseCheckInstalled.mockReturnValue({ installedInfo: null, isLoading: false, }) render() expect(screen.getByTestId('version-to-install')).toHaveTextContent('2.0.0') }) it('should display hasInstalled=false when not installed', () => { mockUseCheckInstalled.mockReturnValue({ installedInfo: null, isLoading: false, }) render() expect(screen.getByTestId('version-has-installed')).toHaveTextContent('false') }) it('should display hasInstalled=true when already installed', () => { mockUseCheckInstalled.mockReturnValue({ installedInfo: { 'test-author/Test Plugin': { installedVersion: '0.9.0', installedId: 'installed-id', uniqueIdentifier: 'old-uid', }, }, isLoading: false, }) render() expect(screen.getByTestId('version-has-installed')).toHaveTextContent('true') expect(screen.getByTestId('version-installed')).toHaveTextContent('0.9.0') }) }) // ================================ // Install Button State Tests // ================================ describe('Install Button State', () => { it('should disable install button when loading', () => { mockUseCheckInstalled.mockReturnValue({ installedInfo: null, isLoading: true, }) render() expect(screen.getByRole('button', { name: 'plugin.installModal.install' })).toBeDisabled() }) it('should enable install button when not loading', () => { mockUseCheckInstalled.mockReturnValue({ installedInfo: null, isLoading: false, }) render() expect(screen.getByRole('button', { name: 'plugin.installModal.install' })).not.toBeDisabled() }) }) // ================================ // Cancel Button Tests // ================================ describe('Cancel Button', () => { it('should call onCancel and stop when cancel button is clicked', () => { const onCancel = vi.fn() render() fireEvent.click(screen.getByRole('button', { name: 'common.operation.cancel' })) expect(mockStop).toHaveBeenCalled() expect(onCancel).toHaveBeenCalledTimes(1) }) it('should hide cancel button when installing', async () => { mockInstallPackageFromLocal.mockImplementation(() => new Promise(() => {})) render() fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.install' })) await waitFor(() => { expect(screen.queryByRole('button', { name: 'common.operation.cancel' })).not.toBeInTheDocument() }) }) }) // ================================ // Installation Flow Tests // ================================ describe('Installation Flow', () => { it('should call onStartToInstall when install button is clicked', async () => { mockInstallPackageFromLocal.mockResolvedValue({ all_installed: true, task_id: 'task-123', }) const onStartToInstall = vi.fn() render() fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.install' })) await waitFor(() => { expect(onStartToInstall).toHaveBeenCalledTimes(1) }) }) it('should call onInstalled when all_installed is true', async () => { mockInstallPackageFromLocal.mockResolvedValue({ all_installed: true, task_id: 'task-123', }) const onInstalled = vi.fn() render() fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.install' })) await waitFor(() => { expect(onInstalled).toHaveBeenCalled() }) }) it('should check task status when all_installed is false', async () => { mockInstallPackageFromLocal.mockResolvedValue({ all_installed: false, task_id: 'task-123', }) mockCheck.mockResolvedValue({ status: TaskStatus.success, error: null }) const onInstalled = vi.fn() render() fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.install' })) await waitFor(() => { expect(mockCheck).toHaveBeenCalledWith({ taskId: 'task-123', pluginUniqueIdentifier: 'test-unique-identifier', }) }) await waitFor(() => { expect(onInstalled).toHaveBeenCalledWith(true) }) }) it('should call onFailed when task status is failed', async () => { mockInstallPackageFromLocal.mockResolvedValue({ all_installed: false, task_id: 'task-123', }) mockCheck.mockResolvedValue({ status: TaskStatus.failed, error: 'Task failed error' }) const onFailed = vi.fn() render() fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.install' })) await waitFor(() => { expect(onFailed).toHaveBeenCalledWith('Task failed error') }) }) it('should uninstall existing plugin before installing new version', async () => { mockUseCheckInstalled.mockReturnValue({ installedInfo: { 'test-author/Test Plugin': { installedVersion: '0.9.0', installedId: 'installed-id-to-uninstall', uniqueIdentifier: 'old-uid', }, }, isLoading: false, }) mockUninstallPlugin.mockResolvedValue({}) mockInstallPackageFromLocal.mockResolvedValue({ all_installed: true, task_id: 'task-123', }) render() fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.install' })) await waitFor(() => { expect(mockUninstallPlugin).toHaveBeenCalledWith('installed-id-to-uninstall') }) await waitFor(() => { expect(mockInstallPackageFromLocal).toHaveBeenCalled() }) }) }) // ================================ // Error Handling Tests // ================================ describe('Error Handling', () => { it('should call onFailed with error string', async () => { mockInstallPackageFromLocal.mockRejectedValue('Installation error string') const onFailed = vi.fn() render() fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.install' })) await waitFor(() => { expect(onFailed).toHaveBeenCalledWith('Installation error string') }) }) it('should call onFailed without message when error is not string', async () => { mockInstallPackageFromLocal.mockRejectedValue({ code: 'ERROR' }) const onFailed = vi.fn() render() fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.install' })) await waitFor(() => { expect(onFailed).toHaveBeenCalledWith() }) }) }) // ================================ // Auto Install Behavior Tests // ================================ describe('Auto Install Behavior', () => { it('should call onInstalled when already installed with same uniqueIdentifier', async () => { mockUseCheckInstalled.mockReturnValue({ installedInfo: { 'test-author/Test Plugin': { installedVersion: '1.0.0', installedId: 'installed-id', uniqueIdentifier: 'test-unique-identifier', }, }, isLoading: false, }) const onInstalled = vi.fn() render() await waitFor(() => { expect(onInstalled).toHaveBeenCalled() }) }) it('should not auto-call onInstalled when uniqueIdentifier differs', () => { mockUseCheckInstalled.mockReturnValue({ installedInfo: { 'test-author/Test Plugin': { installedVersion: '1.0.0', installedId: 'installed-id', uniqueIdentifier: 'different-uid', }, }, isLoading: false, }) const onInstalled = vi.fn() render() // Should not be called immediately expect(onInstalled).not.toHaveBeenCalled() }) }) // ================================ // Dify Version Compatibility Tests // ================================ describe('Dify Version Compatibility', () => { it('should not show warning when dify version is compatible', () => { mockLangGeniusVersionInfo.current_version = '1.0.0' const payload = createMockManifest({ meta: { version: '1.0.0', minimum_dify_version: '0.8.0' } }) render() expect(screen.queryByText(/plugin.difyVersionNotCompatible/)).not.toBeInTheDocument() }) it('should show warning when dify version is incompatible', () => { mockLangGeniusVersionInfo.current_version = '1.0.0' const payload = createMockManifest({ meta: { version: '1.0.0', minimum_dify_version: '2.0.0' } }) render() expect(screen.getByText(/plugin.difyVersionNotCompatible/)).toBeInTheDocument() }) it('should be compatible when minimum_dify_version is undefined', () => { mockLangGeniusVersionInfo.current_version = '1.0.0' const payload = createMockManifest({ meta: { version: '1.0.0' } }) render() expect(screen.queryByText(/plugin.difyVersionNotCompatible/)).not.toBeInTheDocument() }) it('should be compatible when current_version is empty', () => { mockLangGeniusVersionInfo.current_version = '' const payload = createMockManifest({ meta: { version: '1.0.0', minimum_dify_version: '2.0.0' } }) render() // When current_version is empty, should be compatible (no warning) expect(screen.queryByText(/plugin.difyVersionNotCompatible/)).not.toBeInTheDocument() }) it('should be compatible when current_version is undefined', () => { mockLangGeniusVersionInfo.current_version = undefined as unknown as string const payload = createMockManifest({ meta: { version: '1.0.0', minimum_dify_version: '2.0.0' } }) render() // When current_version is undefined, should be compatible (no warning) expect(screen.queryByText(/plugin.difyVersionNotCompatible/)).not.toBeInTheDocument() }) }) // ================================ // Installing State Tests // ================================ describe('Installing State', () => { it('should show installing text when installing', async () => { mockInstallPackageFromLocal.mockImplementation(() => new Promise(() => {})) render() fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.install' })) await waitFor(() => { expect(screen.getByText('plugin.installModal.installing')).toBeInTheDocument() }) }) it('should disable install button when installing', async () => { mockInstallPackageFromLocal.mockImplementation(() => new Promise(() => {})) render() fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.install' })) await waitFor(() => { expect(screen.getByRole('button', { name: /plugin.installModal.installing/ })).toBeDisabled() }) }) it('should show loading spinner when installing', async () => { mockInstallPackageFromLocal.mockImplementation(() => new Promise(() => {})) render() fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.install' })) await waitFor(() => { const spinner = document.querySelector('.animate-spin-slow') expect(spinner).toBeInTheDocument() }) }) it('should not trigger install twice when already installing', async () => { mockInstallPackageFromLocal.mockImplementation(() => new Promise(() => {})) render() const installButton = screen.getByRole('button', { name: 'plugin.installModal.install' }) // Click install fireEvent.click(installButton) await waitFor(() => { expect(mockInstallPackageFromLocal).toHaveBeenCalledTimes(1) }) // Try to click again (button should be disabled but let's verify the guard works) fireEvent.click(screen.getByRole('button', { name: /plugin.installModal.installing/ })) // Should still only be called once due to isInstalling guard expect(mockInstallPackageFromLocal).toHaveBeenCalledTimes(1) }) }) // ================================ // Callback Props Tests // ================================ describe('Callback Props', () => { it('should work without onStartToInstall callback', async () => { mockInstallPackageFromLocal.mockResolvedValue({ all_installed: true, task_id: 'task-123', }) const onInstalled = vi.fn() render( , ) fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.install' })) await waitFor(() => { expect(onInstalled).toHaveBeenCalled() }) }) }) })