import type { PluginPageProps } from './index' import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' import { useQueryState } from 'nuqs' import { beforeEach, describe, expect, it, vi } from 'vitest' import { usePluginInstallation } from '@/hooks/use-query-params' // Import mocked modules for assertions import { fetchBundleInfoFromMarketPlace, fetchManifestFromMarketPlace } from '@/service/plugins' import PluginPageWithContext from './index' // Mock external dependencies vi.mock('@/service/plugins', () => ({ fetchManifestFromMarketPlace: vi.fn(), fetchBundleInfoFromMarketPlace: vi.fn(), })) vi.mock('@/hooks/use-query-params', () => ({ usePluginInstallation: vi.fn(() => [{ packageId: null, bundleInfo: null }, vi.fn()]), })) vi.mock('@/hooks/use-document-title', () => ({ default: vi.fn(), })) vi.mock('@/context/i18n', () => ({ useLocale: () => 'en-US', useDocLink: () => (path: string) => `https://docs.example.com${path}`, })) vi.mock('@/context/global-public-context', () => ({ useGlobalPublicStore: vi.fn((selector) => { const state = { systemFeatures: { enable_marketplace: true, }, } return selector(state) }), })) vi.mock('@/context/app-context', () => ({ useAppContext: () => ({ isCurrentWorkspaceManager: true, isCurrentWorkspaceOwner: false, }), })) vi.mock('@/service/use-plugins', () => ({ useReferenceSettings: () => ({ data: { permission: { install_permission: 'everyone', debug_permission: 'admins', }, }, }), useMutationReferenceSettings: () => ({ mutate: vi.fn(), isPending: false, }), useInvalidateReferenceSettings: () => vi.fn(), usePluginTaskList: () => ({ pluginTasks: [], handleRefetch: vi.fn(), }), useMutationClearTaskPlugin: () => ({ mutateAsync: vi.fn(), }), useInstalledPluginList: () => ({ data: [], isLoading: false, isFetching: false, isLastPage: true, loadNextPage: vi.fn(), }), useInstalledLatestVersion: () => ({ data: {}, }), useInvalidateInstalledPluginList: () => vi.fn(), })) vi.mock('nuqs', () => ({ useQueryState: vi.fn(() => ['plugins', vi.fn()]), })) vi.mock('./plugin-tasks', () => ({ default: () =>
PluginTasks
, })) vi.mock('./debug-info', () => ({ default: () =>
DebugInfo
, })) vi.mock('./install-plugin-dropdown', () => ({ default: ({ onSwitchToMarketplaceTab }: { onSwitchToMarketplaceTab: () => void }) => ( ), })) vi.mock('../install-plugin/install-from-local-package', () => ({ default: ({ onClose }: { onClose: () => void }) => (
), })) vi.mock('../install-plugin/install-from-marketplace', () => ({ default: ({ onClose }: { onClose: () => void }) => (
), })) vi.mock('@/app/components/plugins/reference-setting-modal', () => ({ default: ({ onHide }: { onHide: () => void }) => (
), })) // Helper to create default props const createDefaultProps = (): PluginPageProps => ({ plugins:
Plugins Content
, marketplace:
Marketplace Content
, }) // ============================================================================ // PluginPage Component Tests // ============================================================================ describe('PluginPage Component', () => { beforeEach(() => { vi.clearAllMocks() // Reset to default mock values vi.mocked(usePluginInstallation).mockReturnValue([ { packageId: null, bundleInfo: null }, vi.fn(), ]) vi.mocked(useQueryState).mockReturnValue(['plugins', vi.fn()]) }) // ============================================================================ // Rendering Tests // ============================================================================ describe('Rendering', () => { it('should render without crashing', () => { render() expect(document.getElementById('marketplace-container')).toBeInTheDocument() }) it('should render with correct container id', () => { render() const container = document.getElementById('marketplace-container') expect(container).toBeInTheDocument() }) it('should render PluginTasks component', () => { render() expect(screen.getByTestId('plugin-tasks')).toBeInTheDocument() }) it('should render plugins content when on plugins tab', () => { vi.mocked(useQueryState).mockReturnValue(['plugins', vi.fn()]) render() expect(screen.getByTestId('plugins-content')).toBeInTheDocument() }) it('should render marketplace content when on marketplace tab', () => { vi.mocked(useQueryState).mockReturnValue(['discover', vi.fn()]) render() // The marketplace content should be visible when enable_marketplace is true and on discover tab const container = document.getElementById('marketplace-container') expect(container).toBeInTheDocument() // Check that marketplace-specific links are shown expect(screen.getByText(/requestAPlugin/i)).toBeInTheDocument() }) it('should render TabSlider', () => { render() // TabSlider renders tab options expect(document.querySelector('.flex-1')).toBeInTheDocument() }) it('should render drag and drop hint when on plugins tab', () => { vi.mocked(useQueryState).mockReturnValue(['plugins', vi.fn()]) render() expect(screen.getByText(/dropPluginToInstall/i)).toBeInTheDocument() }) it('should render file input for plugin upload', () => { vi.mocked(useQueryState).mockReturnValue(['plugins', vi.fn()]) render() const fileInput = document.getElementById('fileUploader') expect(fileInput).toBeInTheDocument() expect(fileInput).toHaveAttribute('type', 'file') }) }) // ============================================================================ // Tab Navigation Tests // ============================================================================ describe('Tab Navigation', () => { it('should display plugins tab as active by default', () => { vi.mocked(useQueryState).mockReturnValue(['plugins', vi.fn()]) render() expect(screen.getByTestId('plugins-content')).toBeInTheDocument() }) it('should show marketplace links when on marketplace tab', () => { vi.mocked(useQueryState).mockReturnValue(['discover', vi.fn()]) render() // Check for marketplace-specific buttons expect(screen.getByText(/requestAPlugin/i)).toBeInTheDocument() expect(screen.getByText(/publishPlugins/i)).toBeInTheDocument() }) it('should not show marketplace links when on plugins tab', () => { vi.mocked(useQueryState).mockReturnValue(['plugins', vi.fn()]) render() expect(screen.queryByText(/requestAPlugin/i)).not.toBeInTheDocument() }) }) // ============================================================================ // Permission-based Rendering Tests // ============================================================================ describe('Permission-based Rendering', () => { it('should render InstallPluginDropdown when canManagement is true', () => { render() expect(screen.getByTestId('install-dropdown')).toBeInTheDocument() }) it('should render DebugInfo when canDebugger is true', () => { render() expect(screen.getByTestId('debug-info')).toBeInTheDocument() }) it('should render settings button when canSetPermissions is true', () => { render() // Settings button with RiEqualizer2Line icon const settingsButtons = document.querySelectorAll('button') expect(settingsButtons.length).toBeGreaterThan(0) }) it('should call setActiveTab when onSwitchToMarketplaceTab is called', async () => { const mockSetActiveTab = vi.fn() vi.mocked(useQueryState).mockReturnValue(['plugins', mockSetActiveTab]) render() // Click the install dropdown button which triggers onSwitchToMarketplaceTab fireEvent.click(screen.getByTestId('install-dropdown')) // The mock onSwitchToMarketplaceTab calls setActiveTab('discover') // Since our mock InstallPluginDropdown calls onSwitchToMarketplaceTab on click // we verify that setActiveTab was called with 'discover'. expect(mockSetActiveTab).toHaveBeenCalledWith('discover') }) it('should use noop for file handlers when canManagement is false', () => { // Override mock to disable management permission vi.doMock('@/service/use-plugins', () => ({ useReferenceSettings: () => ({ data: { permission: { install_permission: 'noone', debug_permission: 'noone', }, }, }), useMutationReferenceSettings: () => ({ mutate: vi.fn(), isPending: false, }), useInvalidateReferenceSettings: () => vi.fn(), usePluginTaskList: () => ({ pluginTasks: [], handleRefetch: vi.fn(), }), useMutationClearTaskPlugin: () => ({ mutateAsync: vi.fn(), }), useInstalledPluginList: () => ({ data: [], isLoading: false, isFetching: false, isLastPage: true, loadNextPage: vi.fn(), }), useInstalledLatestVersion: () => ({ data: {}, }), useInvalidateInstalledPluginList: () => vi.fn(), })) vi.mocked(useQueryState).mockReturnValue(['plugins', vi.fn()]) render() // File input should still be in the document (even if handlers are noop) const fileInput = document.getElementById('fileUploader') expect(fileInput).toBeInTheDocument() }) }) // ============================================================================ // File Upload Tests // ============================================================================ describe('File Upload', () => { it('should have hidden file input', () => { vi.mocked(useQueryState).mockReturnValue(['plugins', vi.fn()]) render() const fileInput = document.getElementById('fileUploader') as HTMLInputElement expect(fileInput).toHaveClass('hidden') }) it('should accept .difypkg files', () => { vi.mocked(useQueryState).mockReturnValue(['plugins', vi.fn()]) render() const fileInput = document.getElementById('fileUploader') as HTMLInputElement expect(fileInput.accept).toContain('.difypkg') }) it('should show InstallFromLocalPackage modal when valid file is selected', async () => { vi.mocked(useQueryState).mockReturnValue(['plugins', vi.fn()]) render() const fileInput = document.getElementById('fileUploader') as HTMLInputElement const file = new File(['content'], 'plugin.difypkg', { type: 'application/octet-stream' }) Object.defineProperty(fileInput, 'files', { value: [file], }) fireEvent.change(fileInput) await waitFor(() => { expect(screen.getByTestId('install-local-modal')).toBeInTheDocument() }) }) it('should not show modal for non-.difypkg files', async () => { vi.mocked(useQueryState).mockReturnValue(['plugins', vi.fn()]) render() const fileInput = document.getElementById('fileUploader') as HTMLInputElement const file = new File(['content'], 'plugin.txt', { type: 'text/plain' }) Object.defineProperty(fileInput, 'files', { value: [file], }) fireEvent.change(fileInput) await waitFor(() => { expect(screen.queryByTestId('install-local-modal')).not.toBeInTheDocument() }) }) }) // ============================================================================ // Marketplace Installation Tests // ============================================================================ describe('Marketplace Installation', () => { it('should fetch manifest when packageId is provided', async () => { const mockSetInstallState = vi.fn() vi.mocked(usePluginInstallation).mockReturnValue([ { packageId: 'test-package-id', bundleInfo: null }, mockSetInstallState, ]) vi.mocked(fetchManifestFromMarketPlace).mockResolvedValue({ data: { plugin: { org: 'test-org', name: 'test-plugin', category: 'tool' }, version: { version: '1.0.0' }, }, } as Awaited>) render() await waitFor(() => { expect(fetchManifestFromMarketPlace).toHaveBeenCalledWith('test-package-id') }) }) it('should fetch bundle info when bundleInfo is provided', async () => { const mockSetInstallState = vi.fn() vi.mocked(usePluginInstallation).mockReturnValue([ { packageId: null, bundleInfo: 'test-bundle-info' as unknown }, mockSetInstallState, ] as ReturnType) vi.mocked(fetchBundleInfoFromMarketPlace).mockResolvedValue({ data: { version: { dependencies: [] } }, } as unknown as Awaited>) render() await waitFor(() => { expect(fetchBundleInfoFromMarketPlace).toHaveBeenCalledWith('test-bundle-info') }) }) it('should show InstallFromMarketplace modal after fetching manifest', async () => { const mockSetInstallState = vi.fn() vi.mocked(usePluginInstallation).mockReturnValue([ { packageId: 'test-package-id', bundleInfo: null }, mockSetInstallState, ]) vi.mocked(fetchManifestFromMarketPlace).mockResolvedValue({ data: { plugin: { org: 'test-org', name: 'test-plugin', category: 'tool' }, version: { version: '1.0.0' }, }, } as Awaited>) render() await waitFor(() => { expect(screen.getByTestId('install-marketplace-modal')).toBeInTheDocument() }, { timeout: 3000 }) }) it('should handle fetch error gracefully', async () => { const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) vi.mocked(usePluginInstallation).mockReturnValue([ { packageId: null, bundleInfo: 'invalid-bundle' as unknown }, vi.fn(), ] as ReturnType) vi.mocked(fetchBundleInfoFromMarketPlace).mockRejectedValue(new Error('Network error')) render() await waitFor(() => { expect(consoleSpy).toHaveBeenCalledWith('Failed to load bundle info:', expect.any(Error)) }) consoleSpy.mockRestore() }) }) // ============================================================================ // Settings Modal Tests // ============================================================================ describe('Settings Modal', () => { it('should open settings modal when settings button is clicked', async () => { render() fireEvent.click(screen.getByTestId('plugin-settings-button')) await waitFor(() => { expect(screen.getByTestId('reference-setting-modal')).toBeInTheDocument() }) }) it('should close settings modal when onHide is called', async () => { render() // Open modal fireEvent.click(screen.getByTestId('plugin-settings-button')) await waitFor(() => { expect(screen.getByTestId('reference-setting-modal')).toBeInTheDocument() }) // Close modal fireEvent.click(screen.getByText('Close Settings')) await waitFor(() => { expect(screen.queryByTestId('reference-setting-modal')).not.toBeInTheDocument() }) }) }) // ============================================================================ // Drag and Drop Tests // ============================================================================ describe('Drag and Drop', () => { it('should show dragging overlay when dragging files over container', () => { vi.mocked(useQueryState).mockReturnValue(['plugins', vi.fn()]) render() const container = document.getElementById('marketplace-container')! // Simulate drag enter const dragEnterEvent = new Event('dragenter', { bubbles: true }) Object.defineProperty(dragEnterEvent, 'dataTransfer', { value: { types: ['Files'] }, }) container.dispatchEvent(dragEnterEvent) // Check for dragging overlay styles expect(container).toBeInTheDocument() }) it('should highlight drop zone text when dragging', () => { vi.mocked(useQueryState).mockReturnValue(['plugins', vi.fn()]) render() // The drag hint should be visible const dragHint = screen.getByText(/dropPluginToInstall/i) expect(dragHint).toBeInTheDocument() }) }) // ============================================================================ // Memoization Tests // ============================================================================ describe('Memoization', () => { it('should memoize isPluginsTab correctly', () => { vi.mocked(useQueryState).mockReturnValue(['plugins', vi.fn()]) const { rerender } = render() // Should show plugins content expect(screen.getByTestId('plugins-content')).toBeInTheDocument() // Rerender with same props - memoized value should be same rerender() expect(screen.getByTestId('plugins-content')).toBeInTheDocument() }) it('should memoize isExploringMarketplace correctly', () => { vi.mocked(useQueryState).mockReturnValue(['discover', vi.fn()]) const { rerender } = render() // Should show marketplace links when on discover tab expect(screen.getByText(/requestAPlugin/i)).toBeInTheDocument() // Rerender with same props rerender() expect(screen.getByText(/requestAPlugin/i)).toBeInTheDocument() }) it('should recognize plugin type tabs as marketplace', () => { // Test with a plugin type tab like 'tool' vi.mocked(useQueryState).mockReturnValue(['tool', vi.fn()]) render() // Should show marketplace links when on a plugin type tab expect(screen.getByText(/requestAPlugin/i)).toBeInTheDocument() expect(screen.getByText(/publishPlugins/i)).toBeInTheDocument() }) it('should render marketplace content when isExploringMarketplace and enable_marketplace are true', () => { vi.mocked(useQueryState).mockReturnValue(['discover', vi.fn()]) render() // The marketplace prop content should be rendered // Since we mock the marketplace as a div, check it's not hidden const container = document.getElementById('marketplace-container') expect(container).toBeInTheDocument() expect(container).toHaveClass('bg-background-body') }) }) // ============================================================================ // Context Provider Tests // ============================================================================ describe('Context Provider', () => { it('should wrap component with PluginPageContextProvider', () => { render() // The component should render, indicating context is working expect(document.getElementById('marketplace-container')).toBeInTheDocument() }) it('should filter out marketplace tab when enable_marketplace is false', () => { // This tests line 69 in context.tsx - the false branch of enable_marketplace // The marketplace tab should be filtered out from options render() // Component should still work without marketplace expect(document.getElementById('marketplace-container')).toBeInTheDocument() }) }) // ============================================================================ // Edge Cases and Error Handling // ============================================================================ describe('Edge Cases', () => { it('should handle null plugins prop', () => { vi.mocked(useQueryState).mockReturnValue(['plugins', vi.fn()]) render() expect(document.getElementById('marketplace-container')).toBeInTheDocument() }) it('should handle empty marketplace prop', () => { vi.mocked(useQueryState).mockReturnValue(['discover', vi.fn()]) render() expect(document.getElementById('marketplace-container')).toBeInTheDocument() }) it('should handle rapid tab switches', async () => { const mockSetActiveTab = vi.fn() vi.mocked(useQueryState).mockReturnValue(['plugins', mockSetActiveTab]) render() // Simulate rapid switches by updating state act(() => { vi.mocked(useQueryState).mockReturnValue(['discover', mockSetActiveTab]) }) expect(document.getElementById('marketplace-container')).toBeInTheDocument() }) it('should handle marketplace disabled', () => { // Mock marketplace disabled vi.mock('@/context/global-public-context', async () => ({ useGlobalPublicStore: vi.fn((selector) => { const state = { systemFeatures: { enable_marketplace: false, }, } return selector(state) }), })) vi.mocked(useQueryState).mockReturnValue(['discover', vi.fn()]) render() // Component should still render but without marketplace content when disabled expect(document.getElementById('marketplace-container')).toBeInTheDocument() }) it('should handle file with empty name', async () => { vi.mocked(useQueryState).mockReturnValue(['plugins', vi.fn()]) render() const fileInput = document.getElementById('fileUploader') as HTMLInputElement const file = new File(['content'], '', { type: 'application/octet-stream' }) Object.defineProperty(fileInput, 'files', { value: [file], }) fireEvent.change(fileInput) // Should not show modal for file without proper extension await waitFor(() => { expect(screen.queryByTestId('install-local-modal')).not.toBeInTheDocument() }) }) it('should handle no files selected', async () => { vi.mocked(useQueryState).mockReturnValue(['plugins', vi.fn()]) render() const fileInput = document.getElementById('fileUploader') as HTMLInputElement Object.defineProperty(fileInput, 'files', { value: [], }) fireEvent.change(fileInput) // Should not show modal expect(screen.queryByTestId('install-local-modal')).not.toBeInTheDocument() }) }) // ============================================================================ // Cleanup Tests // ============================================================================ describe('Cleanup', () => { it('should reset install state when hiding marketplace modal', async () => { const mockSetInstallState = vi.fn() vi.mocked(usePluginInstallation).mockReturnValue([ { packageId: 'test-package', bundleInfo: null }, mockSetInstallState, ]) vi.mocked(fetchManifestFromMarketPlace).mockResolvedValue({ data: { plugin: { org: 'test-org', name: 'test-plugin', category: 'tool' }, version: { version: '1.0.0' }, }, } as Awaited>) render() // Wait for modal to appear await waitFor(() => { expect(screen.getByTestId('install-marketplace-modal')).toBeInTheDocument() }, { timeout: 3000 }) // Close modal fireEvent.click(screen.getByText('Close')) await waitFor(() => { expect(mockSetInstallState).toHaveBeenCalledWith(null) }) }) }) // ============================================================================ // Styling Tests // ============================================================================ describe('Styling', () => { it('should apply correct background for plugins tab', () => { vi.mocked(useQueryState).mockReturnValue(['plugins', vi.fn()]) render() const container = document.getElementById('marketplace-container') expect(container).toHaveClass('bg-components-panel-bg') }) it('should apply correct background for marketplace tab', () => { vi.mocked(useQueryState).mockReturnValue(['discover', vi.fn()]) render() const container = document.getElementById('marketplace-container') expect(container).toHaveClass('bg-background-body') }) it('should have scrollbar-gutter stable style', () => { render() const container = document.getElementById('marketplace-container') expect(container).toHaveStyle({ scrollbarGutter: 'stable' }) }) }) }) // ============================================================================ // Uploader Hook Integration Tests // ============================================================================ describe('Uploader Hook Integration', () => { beforeEach(() => { vi.clearAllMocks() vi.mocked(useQueryState).mockReturnValue(['plugins', vi.fn()]) }) describe('Drag Events', () => { it('should handle dragover event', async () => { render() const container = document.getElementById('marketplace-container')! const dragOverEvent = new Event('dragover', { bubbles: true, cancelable: true }) Object.defineProperty(dragOverEvent, 'dataTransfer', { value: { types: ['Files'] }, }) act(() => { container.dispatchEvent(dragOverEvent) }) expect(container).toBeInTheDocument() }) it('should handle dragleave event when leaving container', async () => { render() const container = document.getElementById('marketplace-container')! const dragEnterEvent = new Event('dragenter', { bubbles: true }) Object.defineProperty(dragEnterEvent, 'dataTransfer', { value: { types: ['Files'] }, }) act(() => { container.dispatchEvent(dragEnterEvent) }) const dragLeaveEvent = new Event('dragleave', { bubbles: true }) Object.defineProperty(dragLeaveEvent, 'relatedTarget', { value: null, }) act(() => { container.dispatchEvent(dragLeaveEvent) }) expect(container).toBeInTheDocument() }) it('should handle dragleave event when moving to element outside container', async () => { render() const container = document.getElementById('marketplace-container')! const dragEnterEvent = new Event('dragenter', { bubbles: true }) Object.defineProperty(dragEnterEvent, 'dataTransfer', { value: { types: ['Files'] }, }) act(() => { container.dispatchEvent(dragEnterEvent) }) const outsideElement = document.createElement('div') document.body.appendChild(outsideElement) const dragLeaveEvent = new Event('dragleave', { bubbles: true }) Object.defineProperty(dragLeaveEvent, 'relatedTarget', { value: outsideElement, }) act(() => { container.dispatchEvent(dragLeaveEvent) }) expect(container).toBeInTheDocument() document.body.removeChild(outsideElement) }) it('should handle drop event with files', async () => { render() const container = document.getElementById('marketplace-container')! const dragEnterEvent = new Event('dragenter', { bubbles: true }) Object.defineProperty(dragEnterEvent, 'dataTransfer', { value: { types: ['Files'] }, }) act(() => { container.dispatchEvent(dragEnterEvent) }) const file = new File(['content'], 'test-plugin.difypkg', { type: 'application/octet-stream' }) const dropEvent = new Event('drop', { bubbles: true, cancelable: true }) Object.defineProperty(dropEvent, 'dataTransfer', { value: { files: [file] }, }) act(() => { container.dispatchEvent(dropEvent) }) await waitFor(() => { expect(screen.getByTestId('install-local-modal')).toBeInTheDocument() }) }) it('should handle drop event without dataTransfer', async () => { render() const container = document.getElementById('marketplace-container')! const dropEvent = new Event('drop', { bubbles: true, cancelable: true }) act(() => { container.dispatchEvent(dropEvent) }) expect(screen.queryByTestId('install-local-modal')).not.toBeInTheDocument() }) it('should handle drop event with empty files array', async () => { render() const container = document.getElementById('marketplace-container')! const dropEvent = new Event('drop', { bubbles: true, cancelable: true }) Object.defineProperty(dropEvent, 'dataTransfer', { value: { files: [] }, }) act(() => { container.dispatchEvent(dropEvent) }) expect(screen.queryByTestId('install-local-modal')).not.toBeInTheDocument() }) }) describe('File Change Handler', () => { it('should handle file change with null file', async () => { render() const fileInput = document.getElementById('fileUploader') as HTMLInputElement Object.defineProperty(fileInput, 'files', { value: null }) fireEvent.change(fileInput) expect(screen.queryByTestId('install-local-modal')).not.toBeInTheDocument() }) }) describe('Remove File', () => { it('should clear file input when removeFile is called', async () => { render() const fileInput = document.getElementById('fileUploader') as HTMLInputElement const file = new File(['content'], 'plugin.difypkg', { type: 'application/octet-stream' }) Object.defineProperty(fileInput, 'files', { value: [file] }) fireEvent.change(fileInput) await waitFor(() => { expect(screen.getByTestId('install-local-modal')).toBeInTheDocument() }) fireEvent.click(screen.getByText('Close')) await waitFor(() => { expect(screen.queryByTestId('install-local-modal')).not.toBeInTheDocument() }) }) }) }) // ============================================================================ // Reference Setting Hook Integration Tests // ============================================================================ describe('Reference Setting Hook Integration', () => { describe('Permission Handling', () => { it('should render InstallPluginDropdown when permission is everyone', () => { render() expect(screen.getByTestId('install-dropdown')).toBeInTheDocument() }) it('should render DebugInfo when permission is admins and user is manager', () => { render() expect(screen.getByTestId('debug-info')).toBeInTheDocument() }) }) }) // ============================================================================ // Marketplace Installation Permission Tests // ============================================================================ describe('Marketplace Installation Permission', () => { it('should show InstallPluginDropdown when marketplace is enabled and has permission', () => { render() expect(screen.getByTestId('install-dropdown')).toBeInTheDocument() }) }) // ============================================================================ // Integration Tests // ============================================================================ describe('PluginPage Integration', () => { beforeEach(() => { vi.clearAllMocks() vi.mocked(usePluginInstallation).mockReturnValue([ { packageId: null, bundleInfo: null }, vi.fn(), ]) vi.mocked(useQueryState).mockReturnValue(['plugins', vi.fn()]) }) it('should render complete plugin page with all features', () => { render() // Check all major elements are present expect(document.getElementById('marketplace-container')).toBeInTheDocument() expect(screen.getByTestId('plugin-tasks')).toBeInTheDocument() expect(screen.getByTestId('install-dropdown')).toBeInTheDocument() expect(screen.getByTestId('debug-info')).toBeInTheDocument() expect(screen.getByTestId('plugins-content')).toBeInTheDocument() }) it('should handle full install from marketplace flow', async () => { const mockSetInstallState = vi.fn() vi.mocked(usePluginInstallation).mockReturnValue([ { packageId: 'test-package', bundleInfo: null }, mockSetInstallState, ]) vi.mocked(fetchManifestFromMarketPlace).mockResolvedValue({ data: { plugin: { org: 'langgenius', name: 'test-plugin', category: 'tool' }, version: { version: '1.0.0' }, }, } as Awaited>) render() // Wait for API call await waitFor(() => { expect(fetchManifestFromMarketPlace).toHaveBeenCalled() }) // Wait for modal await waitFor(() => { expect(screen.getByTestId('install-marketplace-modal')).toBeInTheDocument() }, { timeout: 3000 }) // Close modal fireEvent.click(screen.getByText('Close')) // Verify state reset await waitFor(() => { expect(mockSetInstallState).toHaveBeenCalledWith(null) }) }) it('should handle full local plugin install flow', async () => { vi.mocked(useQueryState).mockReturnValue(['plugins', vi.fn()]) render() const fileInput = document.getElementById('fileUploader') as HTMLInputElement const file = new File(['plugin content'], 'my-plugin.difypkg', { type: 'application/octet-stream', }) Object.defineProperty(fileInput, 'files', { value: [file] }) fireEvent.change(fileInput) await waitFor(() => { expect(screen.getByTestId('install-local-modal')).toBeInTheDocument() }) // Close modal (triggers removeFile via onClose) fireEvent.click(screen.getByText('Close')) await waitFor(() => { expect(screen.queryByTestId('install-local-modal')).not.toBeInTheDocument() }) }) it('should render marketplace content only when enable_marketplace is true', () => { vi.mocked(useQueryState).mockReturnValue(['discover', vi.fn()]) const { rerender } = render() // With enable_marketplace: true (default mock), marketplace links should show expect(screen.getByText(/requestAPlugin/i)).toBeInTheDocument() // Rerender to verify consistent behavior rerender() expect(screen.getByText(/publishPlugins/i)).toBeInTheDocument() }) })