dify/web/app/components/share/text-generation/__tests__/menu-dropdown.spec.tsx
Coding On Star b6c7581a31
refactor(web): replace portal component with DropdownMenu in various components (#35319)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: CodingOnStar <hanxujiang@dify.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: yyh <yuanyouhuilyz@gmail.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: yyh <92089059+lyzno1@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>
2026-04-17 05:53:51 +00:00

288 lines
8.4 KiB
TypeScript

import type { SiteInfo } from '@/models/share'
import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import MenuDropdown from '../menu-dropdown'
vi.mock('../info-modal', () => ({
default: ({
isShow,
onClose,
data,
}: {
isShow: boolean
onClose: () => void
data?: SiteInfo
}) => {
if (!isShow)
return null
return (
<div data-testid="info-modal">
<span>{data?.title}</span>
<button type="button" onClick={onClose}>Close Info</button>
</div>
)
},
}))
const mockReplace = vi.fn()
const mockPathname = '/test-path'
vi.mock('@/next/navigation', () => ({
useRouter: () => ({
replace: mockReplace,
}),
usePathname: () => mockPathname,
}))
const mockShareCode = 'test-share-code'
vi.mock('@/context/web-app-context', () => ({
useWebAppStore: (selector: (state: Record<string, unknown>) => unknown) => {
const state = {
webAppAccessMode: 'code',
shareCode: mockShareCode,
}
return selector(state)
},
}))
const mockWebAppLogout = vi.fn().mockResolvedValue(undefined)
vi.mock('@/service/webapp-auth', () => ({
webAppLogout: (...args: unknown[]) => mockWebAppLogout(...args),
}))
afterEach(() => {
cleanup()
})
describe('MenuDropdown', () => {
const baseSiteInfo: SiteInfo = {
title: 'Test App',
icon: '🚀',
icon_type: 'emoji',
}
beforeEach(() => {
vi.clearAllMocks()
})
describe('rendering', () => {
it('should render the trigger button', () => {
render(<MenuDropdown data={baseSiteInfo} />)
const triggerButton = screen.getByRole('button')
expect(triggerButton).toBeInTheDocument()
})
it('should not show dropdown content initially', () => {
render(<MenuDropdown data={baseSiteInfo} />)
expect(screen.queryByText('common.theme.theme')).not.toBeInTheDocument()
})
it('should show dropdown content when clicked', async () => {
render(<MenuDropdown data={baseSiteInfo} />)
const triggerButton = screen.getByRole('button')
fireEvent.click(triggerButton)
await waitFor(() => {
expect(screen.getByText('common.theme.theme')).toBeInTheDocument()
})
})
it('should show About option in dropdown', async () => {
render(<MenuDropdown data={baseSiteInfo} />)
const triggerButton = screen.getByRole('button')
fireEvent.click(triggerButton)
await waitFor(() => {
expect(screen.getByText('common.userProfile.about')).toBeInTheDocument()
})
})
})
describe('privacy policy link', () => {
it('should show privacy policy link when provided', async () => {
const siteInfoWithPrivacy: SiteInfo = {
...baseSiteInfo,
privacy_policy: 'https://example.com/privacy',
}
render(<MenuDropdown data={siteInfoWithPrivacy} />)
const triggerButton = screen.getByRole('button')
fireEvent.click(triggerButton)
await waitFor(() => {
expect(screen.getByText('share.chat.privacyPolicyMiddle')).toBeInTheDocument()
})
})
it('should not show privacy policy link when not provided', async () => {
render(<MenuDropdown data={baseSiteInfo} />)
const triggerButton = screen.getByRole('button')
fireEvent.click(triggerButton)
await waitFor(() => {
expect(screen.queryByText('share.chat.privacyPolicyMiddle')).not.toBeInTheDocument()
})
})
it('should have correct href for privacy policy link', async () => {
const privacyUrl = 'https://example.com/privacy'
const siteInfoWithPrivacy: SiteInfo = {
...baseSiteInfo,
privacy_policy: privacyUrl,
}
render(<MenuDropdown data={siteInfoWithPrivacy} />)
const triggerButton = screen.getByRole('button')
fireEvent.click(triggerButton)
await waitFor(() => {
const link = screen.getByText('share.chat.privacyPolicyMiddle').closest('a')
expect(link).toHaveAttribute('href', privacyUrl)
expect(link).toHaveAttribute('target', '_blank')
})
})
})
describe('logout functionality', () => {
it('should show logout option when hideLogout is false', async () => {
render(<MenuDropdown data={baseSiteInfo} hideLogout={false} />)
const triggerButton = screen.getByRole('button')
fireEvent.click(triggerButton)
await waitFor(() => {
expect(screen.getByText('common.userProfile.logout')).toBeInTheDocument()
})
})
it('should hide logout option when hideLogout is true', async () => {
render(<MenuDropdown data={baseSiteInfo} hideLogout={true} />)
const triggerButton = screen.getByRole('button')
fireEvent.click(triggerButton)
await waitFor(() => {
expect(screen.queryByText('common.userProfile.logout')).not.toBeInTheDocument()
})
})
it('should call webAppLogout and redirect when logout is clicked', async () => {
render(<MenuDropdown data={baseSiteInfo} hideLogout={false} />)
const triggerButton = screen.getByRole('button')
fireEvent.click(triggerButton)
await waitFor(() => {
expect(screen.getByText('common.userProfile.logout')).toBeInTheDocument()
})
const logoutButton = screen.getByText('common.userProfile.logout')
await act(async () => {
fireEvent.click(logoutButton)
})
await waitFor(() => {
expect(mockWebAppLogout).toHaveBeenCalledWith(mockShareCode)
expect(mockReplace).toHaveBeenCalledWith(`/webapp-signin?redirect_url=${mockPathname}`)
})
})
})
describe('about modal', () => {
it('should show InfoModal when About is clicked', async () => {
render(<MenuDropdown data={baseSiteInfo} />)
const triggerButton = screen.getByRole('button')
fireEvent.click(triggerButton)
await waitFor(() => {
expect(screen.getByText('common.userProfile.about')).toBeInTheDocument()
})
const aboutButton = screen.getByText('common.userProfile.about')
fireEvent.click(aboutButton)
await waitFor(() => {
expect(screen.getByText('Test App')).toBeInTheDocument()
})
})
it('should close InfoModal when the close handler runs', async () => {
render(<MenuDropdown data={baseSiteInfo} />)
fireEvent.click(screen.getByRole('button'))
await waitFor(() => {
expect(screen.getByText('common.userProfile.about')).toBeInTheDocument()
})
fireEvent.click(screen.getByText('common.userProfile.about'))
await waitFor(() => {
expect(screen.getByTestId('info-modal')).toBeInTheDocument()
})
fireEvent.click(screen.getByText('Close Info'))
await waitFor(() => {
expect(screen.queryByTestId('info-modal')).not.toBeInTheDocument()
})
})
})
describe('forceClose prop', () => {
it('should close dropdown when forceClose changes to true', async () => {
const { rerender } = render(<MenuDropdown data={baseSiteInfo} forceClose={false} />)
const triggerButton = screen.getByRole('button')
fireEvent.click(triggerButton)
await waitFor(() => {
expect(screen.getByText('common.theme.theme')).toBeInTheDocument()
})
rerender(<MenuDropdown data={baseSiteInfo} forceClose={true} />)
await waitFor(() => {
expect(screen.queryByText('common.theme.theme')).not.toBeInTheDocument()
})
})
})
describe('placement prop', () => {
it('should accept custom placement', () => {
render(<MenuDropdown data={baseSiteInfo} placement="top-start" />)
const triggerButton = screen.getByRole('button')
expect(triggerButton).toBeInTheDocument()
})
})
describe('toggle behavior', () => {
it('should close dropdown when clicking trigger again', async () => {
render(<MenuDropdown data={baseSiteInfo} />)
const triggerButton = screen.getByRole('button')
fireEvent.click(triggerButton)
await waitFor(() => {
expect(screen.getByText('common.theme.theme')).toBeInTheDocument()
})
fireEvent.click(triggerButton)
await waitFor(() => {
expect(screen.queryByText('common.theme.theme')).not.toBeInTheDocument()
})
})
})
describe('memoization', () => {
it('should be wrapped with React.memo', () => {
expect((MenuDropdown as unknown as { $$typeof: symbol }).$$typeof).toBe(Symbol.for('react.memo'))
})
})
})