import type { FilterState } from '../filter-management' import type { SystemFeatures } from '@/types/feature' import { act, fireEvent, render, screen } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' import { defaultSystemFeatures, InstallationScope } from '@/types/feature' // ==================== Imports (after mocks) ==================== import Empty from './index' // ==================== Mock Setup ==================== // Use vi.hoisted to define ALL mock state and functions const { mockSetActiveTab, mockUseInstalledPluginList, mockState, } = vi.hoisted(() => { const state = { filters: { categories: [] as string[], tags: [] as string[], searchQuery: '', } as FilterState, systemFeatures: { enable_marketplace: true, plugin_installation_permission: { plugin_installation_scope: 'all' as const, restrict_to_marketplace_only: false, }, } as Partial, pluginList: { plugins: [] as Array<{ id: string }> } as { plugins: Array<{ id: string }> } | undefined, } return { mockSetActiveTab: vi.fn(), mockUseInstalledPluginList: vi.fn(() => ({ data: state.pluginList })), mockState: state, } }) // Mock plugin page context vi.mock('../context', () => ({ usePluginPageContext: (selector: (value: any) => any) => { const contextValue = { filters: mockState.filters, setActiveTab: mockSetActiveTab, } return selector(contextValue) }, })) // Mock global public store (Zustand store) vi.mock('@/context/global-public-context', () => ({ useGlobalPublicStore: (selector: (state: any) => any) => { return selector({ systemFeatures: { ...defaultSystemFeatures, ...mockState.systemFeatures, }, }) }, })) // Mock useInstalledPluginList hook vi.mock('@/service/use-plugins', () => ({ useInstalledPluginList: () => mockUseInstalledPluginList(), })) // Mock InstallFromGitHub component vi.mock('@/app/components/plugins/install-plugin/install-from-github', () => ({ default: ({ onClose }: { onSuccess: () => void, onClose: () => void }) => (
), })) // Mock InstallFromLocalPackage component vi.mock('@/app/components/plugins/install-plugin/install-from-local-package', () => ({ default: ({ file, onClose }: { file: File, onSuccess: () => void, onClose: () => void }) => (
), })) // Mock Line component vi.mock('../../marketplace/empty/line', () => ({ default: ({ className }: { className?: string }) =>
, })) // ==================== Test Utilities ==================== const resetMockState = () => { mockState.filters = { categories: [], tags: [], searchQuery: '' } mockState.systemFeatures = { enable_marketplace: true, plugin_installation_permission: { plugin_installation_scope: InstallationScope.ALL, restrict_to_marketplace_only: false, }, } mockState.pluginList = { plugins: [] } mockUseInstalledPluginList.mockReturnValue({ data: mockState.pluginList }) } const setMockFilters = (filters: Partial) => { mockState.filters = { ...mockState.filters, ...filters } } const setMockSystemFeatures = (features: Partial) => { mockState.systemFeatures = { ...mockState.systemFeatures, ...features } } const setMockPluginList = (list: { plugins: Array<{ id: string }> } | undefined) => { mockState.pluginList = list mockUseInstalledPluginList.mockReturnValue({ data: list }) } const createMockFile = (name: string, type = 'application/octet-stream'): File => { return new File(['test'], name, { type }) } // Helper to wait for useEffect to complete (single tick) const flushEffects = async () => { await act(async () => {}) } // ==================== Tests ==================== describe('Empty Component', () => { beforeEach(() => { vi.clearAllMocks() resetMockState() }) // ==================== Rendering Tests ==================== describe('Rendering', () => { it('should render basic structure correctly', async () => { // Arrange & Act const { container } = render() await flushEffects() // Assert - file input const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement expect(fileInput).toBeInTheDocument() expect(fileInput.style.display).toBe('none') expect(fileInput.accept).toBe('.difypkg,.difybndl') // Assert - skeleton cards const skeletonCards = container.querySelectorAll('.rounded-xl.bg-components-card-bg') expect(skeletonCards).toHaveLength(20) // Assert - group icon container const iconContainer = document.querySelector('.size-14') expect(iconContainer).toBeInTheDocument() // Assert - line components const lines = screen.getAllByTestId('line-component') expect(lines).toHaveLength(4) }) }) // ==================== Text Display Tests (useMemo) ==================== describe('Text Display (useMemo)', () => { it('should display "noInstalled" text when plugin list is empty', async () => { // Arrange setMockPluginList({ plugins: [] }) // Act render() await flushEffects() // Assert expect(screen.getByText('plugin.list.noInstalled')).toBeInTheDocument() }) it('should display "notFound" text when filters are active with plugins', async () => { // Arrange setMockPluginList({ plugins: [{ id: 'plugin-1' }] }) // Test categories filter setMockFilters({ categories: ['model'] }) const { rerender } = render() await flushEffects() expect(screen.getByText('plugin.list.notFound')).toBeInTheDocument() // Test tags filter setMockFilters({ categories: [], tags: ['tag1'] }) rerender() await flushEffects() expect(screen.getByText('plugin.list.notFound')).toBeInTheDocument() // Test searchQuery filter setMockFilters({ tags: [], searchQuery: 'test query' }) rerender() await flushEffects() expect(screen.getByText('plugin.list.notFound')).toBeInTheDocument() }) it('should prioritize "noInstalled" over "notFound" when no plugins exist', async () => { // Arrange setMockFilters({ categories: ['model'], searchQuery: 'test' }) setMockPluginList({ plugins: [] }) // Act render() await flushEffects() // Assert expect(screen.getByText('plugin.list.noInstalled')).toBeInTheDocument() }) }) // ==================== Install Methods Tests (useEffect) ==================== describe('Install Methods (useEffect)', () => { it('should render all three install methods when marketplace enabled and not restricted', async () => { // Arrange setMockSystemFeatures({ enable_marketplace: true, plugin_installation_permission: { plugin_installation_scope: InstallationScope.ALL, restrict_to_marketplace_only: false, }, }) // Act render() await flushEffects() // Assert const buttons = screen.getAllByRole('button') expect(buttons).toHaveLength(3) expect(screen.getByText('plugin.source.marketplace')).toBeInTheDocument() expect(screen.getByText('plugin.source.github')).toBeInTheDocument() expect(screen.getByText('plugin.source.local')).toBeInTheDocument() // Verify button order const buttonTexts = buttons.map(btn => btn.textContent) expect(buttonTexts[0]).toContain('plugin.source.marketplace') expect(buttonTexts[1]).toContain('plugin.source.github') expect(buttonTexts[2]).toContain('plugin.source.local') }) it('should render only marketplace method when restricted to marketplace only', async () => { // Arrange setMockSystemFeatures({ enable_marketplace: true, plugin_installation_permission: { plugin_installation_scope: InstallationScope.ALL, restrict_to_marketplace_only: true, }, }) // Act render() await flushEffects() // Assert const buttons = screen.getAllByRole('button') expect(buttons).toHaveLength(1) expect(screen.getByText('plugin.source.marketplace')).toBeInTheDocument() expect(screen.queryByText('plugin.source.github')).not.toBeInTheDocument() expect(screen.queryByText('plugin.source.local')).not.toBeInTheDocument() }) it('should render github and local methods when marketplace is disabled', async () => { // Arrange setMockSystemFeatures({ enable_marketplace: false, plugin_installation_permission: { plugin_installation_scope: InstallationScope.ALL, restrict_to_marketplace_only: false, }, }) // Act render() await flushEffects() // Assert const buttons = screen.getAllByRole('button') expect(buttons).toHaveLength(2) expect(screen.queryByText('plugin.source.marketplace')).not.toBeInTheDocument() expect(screen.getByText('plugin.source.github')).toBeInTheDocument() expect(screen.getByText('plugin.source.local')).toBeInTheDocument() }) it('should render no methods when marketplace disabled and restricted', async () => { // Arrange setMockSystemFeatures({ enable_marketplace: false, plugin_installation_permission: { plugin_installation_scope: InstallationScope.ALL, restrict_to_marketplace_only: true, }, }) // Act render() await flushEffects() // Assert const buttons = screen.queryAllByRole('button') expect(buttons).toHaveLength(0) }) }) // ==================== User Interactions Tests ==================== describe('User Interactions', () => { it('should call setActiveTab with "discover" when marketplace button is clicked', async () => { // Arrange render() await flushEffects() // Act fireEvent.click(screen.getByText('plugin.source.marketplace')) // Assert expect(mockSetActiveTab).toHaveBeenCalledWith('discover') }) it('should open and close GitHub modal correctly', async () => { // Arrange render() await flushEffects() // Assert - initially no modal expect(screen.queryByTestId('install-from-github-modal')).not.toBeInTheDocument() // Act - open modal fireEvent.click(screen.getByText('plugin.source.github')) // Assert - modal is open expect(screen.getByTestId('install-from-github-modal')).toBeInTheDocument() // Act - close modal fireEvent.click(screen.getByTestId('github-modal-close')) // Assert - modal is closed expect(screen.queryByTestId('install-from-github-modal')).not.toBeInTheDocument() }) it('should trigger file input click when local button is clicked', async () => { // Arrange render() await flushEffects() const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement const clickSpy = vi.spyOn(fileInput, 'click') // Act fireEvent.click(screen.getByText('plugin.source.local')) // Assert expect(clickSpy).toHaveBeenCalled() }) it('should open and close local modal when file is selected', async () => { // Arrange render() await flushEffects() const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement const mockFile = createMockFile('test-plugin.difypkg') // Assert - initially no modal expect(screen.queryByTestId('install-from-local-modal')).not.toBeInTheDocument() // Act - select file Object.defineProperty(fileInput, 'files', { value: [mockFile], writable: true }) fireEvent.change(fileInput) // Assert - modal is open with correct file expect(screen.getByTestId('install-from-local-modal')).toBeInTheDocument() expect(screen.getByTestId('install-from-local-modal')).toHaveAttribute('data-file-name', 'test-plugin.difypkg') // Act - close modal fireEvent.click(screen.getByTestId('local-modal-close')) // Assert - modal is closed expect(screen.queryByTestId('install-from-local-modal')).not.toBeInTheDocument() }) it('should not open local modal when no file is selected', async () => { // Arrange render() await flushEffects() const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement // Act - trigger change with empty files Object.defineProperty(fileInput, 'files', { value: [], writable: true }) fireEvent.change(fileInput) // Assert expect(screen.queryByTestId('install-from-local-modal')).not.toBeInTheDocument() }) }) // ==================== State Management Tests ==================== describe('State Management', () => { it('should maintain modal state correctly and allow reopening', async () => { // Arrange render() await flushEffects() // Act - Open, close, and reopen GitHub modal fireEvent.click(screen.getByText('plugin.source.github')) expect(screen.getByTestId('install-from-github-modal')).toBeInTheDocument() fireEvent.click(screen.getByTestId('github-modal-close')) expect(screen.queryByTestId('install-from-github-modal')).not.toBeInTheDocument() fireEvent.click(screen.getByText('plugin.source.github')) expect(screen.getByTestId('install-from-github-modal')).toBeInTheDocument() }) it('should update selectedFile state when file is selected', async () => { // Arrange render() await flushEffects() const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement // Act - select .difypkg file Object.defineProperty(fileInput, 'files', { value: [createMockFile('my-plugin.difypkg')], writable: true }) fireEvent.change(fileInput) expect(screen.getByTestId('install-from-local-modal')).toHaveAttribute('data-file-name', 'my-plugin.difypkg') // Close and select .difybndl file fireEvent.click(screen.getByTestId('local-modal-close')) Object.defineProperty(fileInput, 'files', { value: [createMockFile('test-bundle.difybndl')], writable: true }) fireEvent.change(fileInput) expect(screen.getByTestId('install-from-local-modal')).toHaveAttribute('data-file-name', 'test-bundle.difybndl') }) }) // ==================== Side Effects Tests ==================== describe('Side Effects', () => { it('should update installMethods when system features change', async () => { // Arrange - Start with marketplace enabled setMockSystemFeatures({ enable_marketplace: true, plugin_installation_permission: { plugin_installation_scope: InstallationScope.ALL, restrict_to_marketplace_only: false, }, }) const { rerender } = render() await flushEffects() // Assert initial state - 3 methods expect(screen.getAllByRole('button')).toHaveLength(3) // Act - Restrict to marketplace only setMockSystemFeatures({ enable_marketplace: true, plugin_installation_permission: { plugin_installation_scope: InstallationScope.ALL, restrict_to_marketplace_only: true, }, }) rerender() await flushEffects() // Assert - Only marketplace button expect(screen.getAllByRole('button')).toHaveLength(1) expect(screen.getByText('plugin.source.marketplace')).toBeInTheDocument() }) it('should update text when pluginList or filters change', async () => { // Arrange setMockPluginList({ plugins: [] }) const { rerender } = render() await flushEffects() // Assert initial state expect(screen.getByText('plugin.list.noInstalled')).toBeInTheDocument() // Act - Update to have plugins with filters setMockFilters({ categories: ['tool'] }) setMockPluginList({ plugins: [{ id: 'plugin-1' }] }) rerender() await flushEffects() // Assert expect(screen.getByText('plugin.list.notFound')).toBeInTheDocument() }) }) // ==================== Edge Cases ==================== describe('Edge Cases', () => { it('should handle undefined/null plugin data gracefully', () => { // Test undefined plugin list setMockPluginList(undefined) expect(() => render()).not.toThrow() // Test null plugins array mockUseInstalledPluginList.mockReturnValue({ data: { plugins: null as any } }) expect(() => render()).not.toThrow() }) it('should handle file input edge cases', async () => { // Arrange render() await flushEffects() const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement // Test undefined files Object.defineProperty(fileInput, 'files', { value: undefined, writable: true }) fireEvent.change(fileInput) expect(screen.queryByTestId('install-from-local-modal')).not.toBeInTheDocument() }) }) // ==================== React.memo Tests ==================== describe('React.memo Behavior', () => { it('should be wrapped with React.memo and have displayName', () => { // Assert expect(Empty).toBeDefined() expect((Empty as any).$$typeof?.toString()).toContain('Symbol') expect((Empty as any).displayName || (Empty as any).type?.displayName).toBeDefined() }) }) // ==================== Modal Callbacks Tests ==================== describe('Modal Callbacks', () => { it('should handle modal onSuccess callbacks (noop)', async () => { // Arrange render() await flushEffects() // Test GitHub modal onSuccess fireEvent.click(screen.getByText('plugin.source.github')) fireEvent.click(screen.getByTestId('github-modal-success')) expect(screen.getByTestId('install-from-github-modal')).toBeInTheDocument() // Close GitHub modal and test Local modal onSuccess fireEvent.click(screen.getByTestId('github-modal-close')) const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement Object.defineProperty(fileInput, 'files', { value: [createMockFile('test-plugin.difypkg')], writable: true }) fireEvent.change(fileInput) fireEvent.click(screen.getByTestId('local-modal-success')) expect(screen.getByTestId('install-from-local-modal')).toBeInTheDocument() }) }) // ==================== Conditional Modal Rendering ==================== describe('Conditional Modal Rendering', () => { it('should only render one modal at a time and require file for local modal', async () => { // Arrange render() await flushEffects() // Assert - no modals initially expect(screen.queryByTestId('install-from-github-modal')).not.toBeInTheDocument() expect(screen.queryByTestId('install-from-local-modal')).not.toBeInTheDocument() // Open GitHub modal - only GitHub modal visible fireEvent.click(screen.getByText('plugin.source.github')) expect(screen.getByTestId('install-from-github-modal')).toBeInTheDocument() expect(screen.queryByTestId('install-from-local-modal')).not.toBeInTheDocument() // Click local button - triggers file input, no modal yet (no file selected) fireEvent.click(screen.getByText('plugin.source.local')) // GitHub modal should still be visible, local modal requires file selection expect(screen.queryByTestId('install-from-local-modal')).not.toBeInTheDocument() }) }) })