mirror of
https://github.com/langgenius/dify.git
synced 2026-04-17 20:09:34 +08:00
238 lines
5.9 KiB
TypeScript
238 lines
5.9 KiB
TypeScript
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
import * as React from 'react'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import Nav from '@/app/components/header/nav'
|
|
import { AppModeEnum } from '@/types/app'
|
|
|
|
const mockPush = vi.fn()
|
|
const mockSetAppDetail = vi.fn()
|
|
const mockOnCreate = vi.fn()
|
|
const mockOnLoadMore = vi.fn()
|
|
|
|
let mockSelectedSegment = 'app'
|
|
let mockIsCurrentWorkspaceEditor = true
|
|
|
|
vi.mock('@headlessui/react', () => {
|
|
type MenuContextValue = {
|
|
open: boolean
|
|
setOpen: React.Dispatch<React.SetStateAction<boolean>>
|
|
}
|
|
const MenuContext = React.createContext<MenuContextValue | null>(null)
|
|
|
|
const Menu = ({
|
|
children,
|
|
}: {
|
|
children: React.ReactNode | ((props: { open: boolean }) => React.ReactNode)
|
|
}) => {
|
|
const [open, setOpen] = React.useState(false)
|
|
const value = React.useMemo(() => ({ open, setOpen }), [open])
|
|
|
|
return (
|
|
<MenuContext.Provider value={value}>
|
|
{typeof children === 'function' ? children({ open }) : children}
|
|
</MenuContext.Provider>
|
|
)
|
|
}
|
|
|
|
const MenuButton = ({
|
|
children,
|
|
onClick,
|
|
...props
|
|
}: React.ButtonHTMLAttributes<HTMLButtonElement>) => {
|
|
const context = React.useContext(MenuContext)
|
|
|
|
return (
|
|
<button
|
|
type="button"
|
|
aria-expanded={context?.open ?? false}
|
|
onClick={(event) => {
|
|
context?.setOpen(v => !v)
|
|
onClick?.(event)
|
|
}}
|
|
{...props}
|
|
>
|
|
{children}
|
|
</button>
|
|
)
|
|
}
|
|
|
|
const MenuItems = ({
|
|
as: Component = 'div',
|
|
children,
|
|
...props
|
|
}: {
|
|
as?: React.ElementType
|
|
children: React.ReactNode
|
|
} & Record<string, unknown>) => {
|
|
const context = React.useContext(MenuContext)
|
|
if (!context?.open)
|
|
return null
|
|
|
|
return <Component {...props}>{children}</Component>
|
|
}
|
|
|
|
const MenuItem = ({
|
|
as: Component = 'div',
|
|
children,
|
|
...props
|
|
}: {
|
|
as?: React.ElementType
|
|
children: React.ReactNode
|
|
} & Record<string, unknown>) => <Component {...props}>{children}</Component>
|
|
|
|
return {
|
|
Menu,
|
|
MenuButton,
|
|
MenuItems,
|
|
MenuItem,
|
|
Transition: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
|
}
|
|
})
|
|
|
|
vi.mock('react-i18next', () => ({
|
|
useTranslation: () => ({
|
|
t: (key: string) => key,
|
|
}),
|
|
}))
|
|
|
|
vi.mock('@/next/navigation', () => ({
|
|
useSelectedLayoutSegment: () => mockSelectedSegment,
|
|
useRouter: () => ({
|
|
push: mockPush,
|
|
}),
|
|
}))
|
|
|
|
vi.mock('@/next/link', () => ({
|
|
default: ({
|
|
href,
|
|
children,
|
|
}: {
|
|
href: string
|
|
children?: React.ReactNode
|
|
}) => <a href={href}>{children}</a>,
|
|
}))
|
|
|
|
vi.mock('@/app/components/app/store', () => ({
|
|
useStore: () => mockSetAppDetail,
|
|
}))
|
|
|
|
vi.mock('@/context/app-context', () => ({
|
|
useAppContext: () => ({
|
|
isCurrentWorkspaceEditor: mockIsCurrentWorkspaceEditor,
|
|
}),
|
|
}))
|
|
|
|
const navigationItems = [
|
|
{
|
|
id: 'app-1',
|
|
name: 'Alpha',
|
|
link: '/app/app-1/configuration',
|
|
icon_type: 'emoji' as const,
|
|
icon: '🤖',
|
|
icon_background: '#FFEAD5',
|
|
icon_url: null,
|
|
mode: AppModeEnum.CHAT,
|
|
},
|
|
{
|
|
id: 'app-2',
|
|
name: 'Bravo',
|
|
link: '/app/app-2/workflow',
|
|
icon_type: 'emoji' as const,
|
|
icon: '⚙️',
|
|
icon_background: '#E0F2FE',
|
|
icon_url: null,
|
|
mode: AppModeEnum.WORKFLOW,
|
|
},
|
|
]
|
|
|
|
const curNav = {
|
|
id: 'app-1',
|
|
name: 'Alpha',
|
|
icon_type: 'emoji' as const,
|
|
icon: '🤖',
|
|
icon_background: '#FFEAD5',
|
|
icon_url: null,
|
|
mode: AppModeEnum.CHAT,
|
|
}
|
|
|
|
const renderNav = (nav = curNav) => {
|
|
return render(
|
|
<Nav
|
|
isApp
|
|
icon={<span data-testid="nav-icon">icon</span>}
|
|
activeIcon={<span data-testid="nav-icon-active">active-icon</span>}
|
|
text="menus.apps"
|
|
activeSegment={['apps', 'app']}
|
|
link="/apps"
|
|
curNav={nav}
|
|
navigationItems={navigationItems}
|
|
createText="menus.newApp"
|
|
onCreate={mockOnCreate}
|
|
onLoadMore={mockOnLoadMore}
|
|
isLoadingMore={false}
|
|
/>,
|
|
)
|
|
}
|
|
|
|
describe('Header Nav Flow', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
mockSelectedSegment = 'app'
|
|
mockIsCurrentWorkspaceEditor = true
|
|
})
|
|
|
|
it('switches to another app from the selector and clears stale app detail first', async () => {
|
|
renderNav()
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /Alpha/i }))
|
|
fireEvent.click(await screen.findByText('Bravo'))
|
|
|
|
expect(mockSetAppDetail).toHaveBeenCalled()
|
|
expect(mockPush).toHaveBeenCalledWith('/app/app-2/workflow')
|
|
})
|
|
|
|
it('opens the nested create menu and emits all app creation branches', async () => {
|
|
renderNav()
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /Alpha/i }))
|
|
fireEvent.click(await screen.findByText('menus.newApp'))
|
|
fireEvent.click(await screen.findByText('newApp.startFromBlank'))
|
|
fireEvent.click(await screen.findByText('newApp.startFromTemplate'))
|
|
fireEvent.click(await screen.findByText('importDSL'))
|
|
|
|
expect(mockOnCreate).toHaveBeenNthCalledWith(1, 'blank')
|
|
expect(mockOnCreate).toHaveBeenNthCalledWith(2, 'template')
|
|
expect(mockOnCreate).toHaveBeenNthCalledWith(3, 'dsl')
|
|
})
|
|
|
|
it('keeps the current nav label in sync with prop updates', async () => {
|
|
const { rerender } = renderNav()
|
|
|
|
expect(screen.getByRole('button', { name: /Alpha/i })).toBeInTheDocument()
|
|
|
|
rerender(
|
|
<Nav
|
|
isApp
|
|
icon={<span data-testid="nav-icon">icon</span>}
|
|
activeIcon={<span data-testid="nav-icon-active">active-icon</span>}
|
|
text="menus.apps"
|
|
activeSegment={['apps', 'app']}
|
|
link="/apps"
|
|
curNav={{
|
|
...curNav,
|
|
name: 'Alpha Renamed',
|
|
}}
|
|
navigationItems={navigationItems}
|
|
createText="menus.newApp"
|
|
onCreate={mockOnCreate}
|
|
onLoadMore={mockOnLoadMore}
|
|
isLoadingMore={false}
|
|
/>,
|
|
)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('button', { name: /Alpha Renamed/i })).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|