import type { App, AppSSO } from '@/types/app' import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import * as React from 'react' import { AppModeEnum } from '@/types/app' import AppSidebarDropdown from '../app-sidebar-dropdown' let mockAppDetail: (App & Partial) | undefined vi.mock('@/app/components/app/store', () => ({ useStore: (selector: (state: Record) => unknown) => selector({ appDetail: mockAppDetail, }), })) vi.mock('@/context/app-context', () => ({ useAppContext: () => ({ isCurrentWorkspaceEditor: true, }), })) vi.mock('@langgenius/dify-ui/dropdown-menu', () => { const DropdownMenuContext = React.createContext<{ isOpen: boolean, setOpen: (open: boolean) => void } | null>(null) const useDropdownMenuContext = () => { const context = React.use(DropdownMenuContext) if (!context) throw new Error('DropdownMenu components must be wrapped in DropdownMenu') return context } return { DropdownMenu: ({ children, open, onOpenChange }: { children: React.ReactNode, open: boolean, onOpenChange?: (open: boolean) => void }) => (
{children}
), DropdownMenuTrigger: ({ children, onClick }: { children: React.ReactNode, onClick?: React.MouseEventHandler }) => { const { isOpen, setOpen } = useDropdownMenuContext() return ( ) }, DropdownMenuContent: ({ children }: { children: React.ReactNode }) =>
{children}
, } }) vi.mock('../../base/app-icon', () => ({ default: ({ size, icon }: { size: string, icon: string }) => (
), })) vi.mock('../../base/divider', () => ({ default: () =>
, })) vi.mock('../app-info', () => ({ default: ({ expand, onlyShowDetail, openState }: { expand: boolean onlyShowDetail?: boolean openState?: boolean }) => (
), })) vi.mock('../nav-link', () => ({ default: ({ name, href, mode }: { name: string, href: string, mode?: string }) => ( {name} ), })) const MockIcon = (props: React.SVGProps) => const createAppDetail = (overrides: Partial = {}): App & Partial => ({ id: 'app-1', name: 'Test App', mode: AppModeEnum.CHAT, icon: '🤖', icon_type: 'emoji', icon_background: '#FFEAD5', icon_url: '', description: '', use_icon_as_answer_icon: false, ...overrides, } as App & Partial) const navigation = [ { name: 'Overview', href: '/overview', icon: MockIcon, selectedIcon: MockIcon }, { name: 'Logs', href: '/logs', icon: MockIcon, selectedIcon: MockIcon }, ] describe('AppSidebarDropdown', () => { beforeEach(() => { vi.clearAllMocks() mockAppDetail = createAppDetail() }) it('should return null when appDetail is not available', () => { mockAppDetail = undefined const { container } = render() expect(container.innerHTML).toBe('') }) it('should render trigger with app icon', () => { render() const icons = screen.getAllByTestId('app-icon') const smallIcon = icons.find(i => i.getAttribute('data-size') === 'small') expect(smallIcon).toBeInTheDocument() }) it('should render navigation links', () => { render() expect(screen.getByTestId('nav-link-Overview')).toBeInTheDocument() expect(screen.getByTestId('nav-link-Logs')).toBeInTheDocument() }) it('should display app name', () => { render() expect(screen.getByText('Test App')).toBeInTheDocument() }) it('should display app mode label', () => { render() expect(screen.getByText('app.types.chatbot')).toBeInTheDocument() }) it('should display mode labels for different modes', () => { mockAppDetail = createAppDetail({ mode: AppModeEnum.ADVANCED_CHAT }) render() expect(screen.getByText('app.types.advanced')).toBeInTheDocument() }) it('should render AppInfo component for detail expand', () => { render() expect(screen.getByTestId('app-info')).toBeInTheDocument() expect(screen.getByTestId('app-info')).toHaveAttribute('data-only-detail', 'true') }) it('should toggle portal open state when trigger is clicked', async () => { const user = userEvent.setup() render() const trigger = screen.getByTestId('dropdown-trigger') await user.click(trigger) const dropdown = screen.getByTestId('dropdown-menu') expect(dropdown).toHaveAttribute('data-open', 'true') }) it('should render divider between app info and navigation', () => { render() expect(screen.getByTestId('divider')).toBeInTheDocument() }) it('should render large app icon in dropdown content', () => { render() const icons = screen.getAllByTestId('app-icon') const largeIcon = icons.find(icon => icon.getAttribute('data-size') === 'large') expect(largeIcon).toBeInTheDocument() }) it('should set detailExpand when clicking app info area', async () => { const user = userEvent.setup() render() const appName = screen.getByText('Test App') const appInfoArea = appName.closest('[class*="cursor-pointer"]') if (appInfoArea) await user.click(appInfoArea) }) it('should display workflow mode label', () => { mockAppDetail = createAppDetail({ mode: AppModeEnum.WORKFLOW }) render() expect(screen.getByText('app.types.workflow')).toBeInTheDocument() }) it('should display agent mode label', () => { mockAppDetail = createAppDetail({ mode: AppModeEnum.AGENT_CHAT }) render() expect(screen.getByText('app.types.agent')).toBeInTheDocument() }) it('should display completion mode label', () => { mockAppDetail = createAppDetail({ mode: AppModeEnum.COMPLETION }) render() expect(screen.getByText('app.types.completion')).toBeInTheDocument() }) })