mirror of
https://github.com/langgenius/dify.git
synced 2026-05-07 02:46:32 +08:00
Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: jyong <718720800@qq.com> Co-authored-by: Yansong Zhang <916125788@qq.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: hj24 <mambahj24@gmail.com> Co-authored-by: hj24 <huangjian@dify.ai> Co-authored-by: Joel <iamjoel007@gmail.com> Co-authored-by: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Co-authored-by: CodingOnStar <hanxujiang@dify.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: 非法操作 <hjlarry@163.com> Co-authored-by: Ayush Baluni <73417844+aayushbaluni@users.noreply.github.com> Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com> Co-authored-by: jimcody1995 <jjimcody@gmail.com> Co-authored-by: James <63717587+jamesrayammons@users.noreply.github.com> Co-authored-by: Yunlu Wen <yunlu.wen@dify.ai> Co-authored-by: Stephen Zhou <hi@hyoban.cc> Co-authored-by: Coding On Star <447357187@qq.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: jerryzai <jerryzh8710@protonmail.com> Co-authored-by: NVIDIAN <speedy.hpc@hotmail.com> Co-authored-by: ai-hpc <ai-hpc@users.noreply.github.com> Co-authored-by: Asuka Minato <i@asukaminato.eu.org> Co-authored-by: Junghwan <70629228+shaun0927@users.noreply.github.com> Co-authored-by: HeYinKazune <70251095+HeYin-OS@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: yyh <yuanyouhuilyz@gmail.com> Co-authored-by: Jingyi <jingyi.qi@dify.ai> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: sxxtony <166789813+sxxtony@users.noreply.github.com>
200 lines
5.5 KiB
TypeScript
200 lines
5.5 KiB
TypeScript
import type { SVGProps } from 'react'
|
|
import { fireEvent, render, screen } from '@testing-library/react'
|
|
import * as React from 'react'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import AppDetailNav from '@/app/components/app-sidebar'
|
|
|
|
const mockSetAppSidebarExpand = vi.fn()
|
|
|
|
let mockAppSidebarExpand = 'expand'
|
|
let mockPathname = '/app/app-1/logs'
|
|
let mockSelectedSegment = 'logs'
|
|
let mockIsHovering = true
|
|
let keyPressHandler: ((event: { preventDefault: () => void }) => void) | null = null
|
|
|
|
vi.mock('react-i18next', () => ({
|
|
useTranslation: () => ({
|
|
t: (key: string) => key,
|
|
}),
|
|
}))
|
|
|
|
vi.mock('@/app/components/app/store', () => ({
|
|
useStore: (selector: (state: Record<string, unknown>) => unknown) => selector({
|
|
appDetail: {
|
|
id: 'app-1',
|
|
name: 'Demo App',
|
|
mode: 'chat',
|
|
icon: '🤖',
|
|
icon_type: 'emoji',
|
|
icon_background: '#FFEAD5',
|
|
icon_url: null,
|
|
},
|
|
appSidebarExpand: mockAppSidebarExpand,
|
|
setAppSidebarExpand: mockSetAppSidebarExpand,
|
|
}),
|
|
}))
|
|
|
|
vi.mock('zustand/react/shallow', () => ({
|
|
useShallow: (selector: unknown) => selector,
|
|
}))
|
|
|
|
vi.mock('@/next/navigation', () => ({
|
|
usePathname: () => mockPathname,
|
|
useSelectedLayoutSegment: () => mockSelectedSegment,
|
|
}))
|
|
|
|
vi.mock('@/next/link', () => ({
|
|
default: ({
|
|
href,
|
|
children,
|
|
className,
|
|
title,
|
|
}: {
|
|
href: string
|
|
children?: React.ReactNode
|
|
className?: string
|
|
title?: string
|
|
}) => (
|
|
<a href={href} className={className} title={title}>
|
|
{children}
|
|
</a>
|
|
),
|
|
}))
|
|
|
|
vi.mock('ahooks', () => ({
|
|
useHover: () => mockIsHovering,
|
|
useKeyPress: (_key: string, handler: (event: { preventDefault: () => void }) => void) => {
|
|
keyPressHandler = handler
|
|
},
|
|
}))
|
|
|
|
vi.mock('@/hooks/use-breakpoints', () => ({
|
|
default: () => 'desktop',
|
|
MediaType: {
|
|
mobile: 'mobile',
|
|
desktop: 'desktop',
|
|
},
|
|
}))
|
|
|
|
vi.mock('@/context/event-emitter', () => ({
|
|
useEventEmitterContextContext: () => ({
|
|
eventEmitter: {
|
|
useSubscription: vi.fn(),
|
|
},
|
|
}),
|
|
}))
|
|
|
|
vi.mock('@/context/app-context', () => ({
|
|
useAppContext: () => ({
|
|
isCurrentWorkspaceEditor: true,
|
|
}),
|
|
}))
|
|
|
|
vi.mock('@/app/components/workflow/utils', () => ({
|
|
getKeyboardKeyCodeBySystem: () => 'ctrl',
|
|
getKeyboardKeyNameBySystem: (key: string) => key,
|
|
}))
|
|
|
|
vi.mock('@/app/components/base/portal-to-follow-elem', async () => {
|
|
const React = await vi.importActual<typeof import('react')>('react')
|
|
const OpenContext = React.createContext(false)
|
|
|
|
return {
|
|
PortalToFollowElem: ({ children, open }: { children: React.ReactNode, open: boolean }) => (
|
|
<OpenContext.Provider value={open}>
|
|
<div>{children}</div>
|
|
</OpenContext.Provider>
|
|
),
|
|
PortalToFollowElemTrigger: ({
|
|
children,
|
|
onClick,
|
|
}: {
|
|
children: React.ReactNode
|
|
onClick?: () => void
|
|
}) => (
|
|
<button type="button" data-testid="portal-trigger" onClick={onClick}>
|
|
{children}
|
|
</button>
|
|
),
|
|
PortalToFollowElemContent: ({ children }: { children: React.ReactNode }) => {
|
|
const open = React.useContext(OpenContext)
|
|
return open ? <div>{children}</div> : null
|
|
},
|
|
}
|
|
})
|
|
|
|
vi.mock('@/app/components/base/tooltip', () => ({
|
|
default: ({ children }: { children?: React.ReactNode }) => <>{children}</>,
|
|
}))
|
|
|
|
vi.mock('@/app/components/app-sidebar/app-info', () => ({
|
|
default: ({
|
|
expand,
|
|
onlyShowDetail,
|
|
openState,
|
|
}: {
|
|
expand: boolean
|
|
onlyShowDetail?: boolean
|
|
openState?: boolean
|
|
}) => (
|
|
<div
|
|
data-testid={onlyShowDetail ? 'app-info-detail' : 'app-info'}
|
|
data-expand={expand}
|
|
data-open={openState}
|
|
/>
|
|
),
|
|
}))
|
|
|
|
const MockIcon = (props: SVGProps<SVGSVGElement>) => <svg {...props} />
|
|
|
|
const navigation = [
|
|
{ name: 'Overview', href: '/app/app-1/overview', icon: MockIcon, selectedIcon: MockIcon },
|
|
{ name: 'Logs', href: '/app/app-1/logs', icon: MockIcon, selectedIcon: MockIcon },
|
|
]
|
|
|
|
describe('App Sidebar Shell Flow', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
localStorage.clear()
|
|
mockAppSidebarExpand = 'expand'
|
|
mockPathname = '/app/app-1/logs'
|
|
mockSelectedSegment = 'logs'
|
|
mockIsHovering = true
|
|
keyPressHandler = null
|
|
})
|
|
|
|
it('renders the expanded sidebar, marks the active nav item, and toggles collapse by click and shortcut', () => {
|
|
render(<AppDetailNav navigation={navigation} />)
|
|
|
|
expect(screen.getByTestId('app-info')).toHaveAttribute('data-expand', 'true')
|
|
|
|
const logsLink = screen.getByRole('link', { name: /Logs/i })
|
|
expect(logsLink.className).toContain('bg-components-menu-item-bg-active')
|
|
|
|
fireEvent.click(screen.getByRole('button'))
|
|
expect(mockSetAppSidebarExpand).toHaveBeenCalledWith('collapse')
|
|
|
|
const preventDefault = vi.fn()
|
|
keyPressHandler?.({ preventDefault })
|
|
|
|
expect(preventDefault).toHaveBeenCalled()
|
|
expect(mockSetAppSidebarExpand).toHaveBeenCalledWith('collapse')
|
|
})
|
|
|
|
it('switches to the workflow fullscreen dropdown shell and opens its navigation menu', async () => {
|
|
mockPathname = '/app/app-1/workflow'
|
|
mockSelectedSegment = 'workflow'
|
|
localStorage.setItem('workflow-canvas-maximize', 'true')
|
|
|
|
render(<AppDetailNav navigation={navigation} />)
|
|
|
|
expect(screen.queryByTestId('app-info')).not.toBeInTheDocument()
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'operation.more' }))
|
|
|
|
expect(await screen.findByText('Demo App')).toBeInTheDocument()
|
|
expect(screen.getByRole('link', { name: /Overview/i })).toBeInTheDocument()
|
|
expect(screen.getByRole('link', { name: /Logs/i })).toBeInTheDocument()
|
|
})
|
|
})
|