import { fireEvent, render, screen } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' import { PluginSource } from '../types' import OperationDropdown from './operation-dropdown' // Mock dependencies vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key: string) => key, }), })) vi.mock('@/context/global-public-context', () => ({ useGlobalPublicStore: (selector: (state: { systemFeatures: { enable_marketplace: boolean } }) => T): T => selector({ systemFeatures: { enable_marketplace: true } }), })) vi.mock('@/utils/classnames', () => ({ cn: (...args: (string | undefined | false | null)[]) => args.filter(Boolean).join(' '), })) vi.mock('@/app/components/base/action-button', () => ({ default: ({ children, className, onClick }: { children: React.ReactNode, className?: string, onClick?: () => void }) => ( ), })) vi.mock('@/app/components/base/portal-to-follow-elem', () => ({ PortalToFollowElem: ({ children, open }: { children: React.ReactNode, open: boolean }) => (
{children}
), PortalToFollowElemTrigger: ({ children, onClick }: { children: React.ReactNode, onClick: () => void }) => (
{children}
), PortalToFollowElemContent: ({ children, className }: { children: React.ReactNode, className?: string }) => (
{children}
), })) describe('OperationDropdown', () => { const mockOnInfo = vi.fn() const mockOnCheckVersion = vi.fn() const mockOnRemove = vi.fn() const defaultProps = { source: PluginSource.github, detailUrl: 'https://github.com/test/repo', onInfo: mockOnInfo, onCheckVersion: mockOnCheckVersion, onRemove: mockOnRemove, } beforeEach(() => { vi.clearAllMocks() }) describe('Rendering', () => { it('should render trigger button', () => { render() expect(screen.getByTestId('portal-trigger')).toBeInTheDocument() expect(screen.getByTestId('action-button')).toBeInTheDocument() }) it('should render dropdown content', () => { render() expect(screen.getByTestId('portal-content')).toBeInTheDocument() }) it('should render info option for github source', () => { render() expect(screen.getByText('detailPanel.operation.info')).toBeInTheDocument() }) it('should render check update option for github source', () => { render() expect(screen.getByText('detailPanel.operation.checkUpdate')).toBeInTheDocument() }) it('should render view detail option for github source with marketplace enabled', () => { render() expect(screen.getByText('detailPanel.operation.viewDetail')).toBeInTheDocument() }) it('should render view detail option for marketplace source', () => { render() expect(screen.getByText('detailPanel.operation.viewDetail')).toBeInTheDocument() }) it('should always render remove option', () => { render() expect(screen.getByText('detailPanel.operation.remove')).toBeInTheDocument() }) it('should not render info option for marketplace source', () => { render() expect(screen.queryByText('detailPanel.operation.info')).not.toBeInTheDocument() }) it('should not render check update option for marketplace source', () => { render() expect(screen.queryByText('detailPanel.operation.checkUpdate')).not.toBeInTheDocument() }) it('should not render view detail for local source', () => { render() expect(screen.queryByText('detailPanel.operation.viewDetail')).not.toBeInTheDocument() }) it('should not render view detail for debugging source', () => { render() expect(screen.queryByText('detailPanel.operation.viewDetail')).not.toBeInTheDocument() }) }) describe('User Interactions', () => { it('should toggle dropdown when trigger is clicked', () => { render() const trigger = screen.getByTestId('portal-trigger') fireEvent.click(trigger) // The portal-elem should reflect the open state expect(screen.getByTestId('portal-elem')).toBeInTheDocument() }) it('should call onInfo when info option is clicked', () => { render() fireEvent.click(screen.getByText('detailPanel.operation.info')) expect(mockOnInfo).toHaveBeenCalledTimes(1) }) it('should call onCheckVersion when check update option is clicked', () => { render() fireEvent.click(screen.getByText('detailPanel.operation.checkUpdate')) expect(mockOnCheckVersion).toHaveBeenCalledTimes(1) }) it('should call onRemove when remove option is clicked', () => { render() fireEvent.click(screen.getByText('detailPanel.operation.remove')) expect(mockOnRemove).toHaveBeenCalledTimes(1) }) it('should have correct href for view detail link', () => { render() const link = screen.getByText('detailPanel.operation.viewDetail').closest('a') expect(link).toHaveAttribute('href', 'https://github.com/test/repo') expect(link).toHaveAttribute('target', '_blank') }) }) describe('Props Variations', () => { it('should handle all plugin sources', () => { const sources = [ PluginSource.github, PluginSource.marketplace, PluginSource.local, PluginSource.debugging, ] sources.forEach((source) => { const { unmount } = render( , ) expect(screen.getByTestId('portal-elem')).toBeInTheDocument() expect(screen.getByText('detailPanel.operation.remove')).toBeInTheDocument() unmount() }) }) it('should handle different detail URLs', () => { const urls = [ 'https://github.com/owner/repo', 'https://marketplace.example.com/plugin/123', ] urls.forEach((url) => { const { unmount } = render( , ) const link = screen.getByText('detailPanel.operation.viewDetail').closest('a') expect(link).toHaveAttribute('href', url) unmount() }) }) }) describe('Memoization', () => { it('should be wrapped with React.memo', () => { // Verify the component is exported as a memo component expect(OperationDropdown).toBeDefined() // React.memo wraps the component, so it should have $$typeof expect((OperationDropdown as { $$typeof?: symbol }).$$typeof).toBeDefined() }) }) })