mirror of
https://github.com/langgenius/dify.git
synced 2026-04-05 20:22:39 +08:00
fix: tests
This commit is contained in:
@@ -1,5 +1,15 @@
|
||||
import type { MenuItemProps } from './menu-item'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuContent,
|
||||
ContextMenuTrigger,
|
||||
} from '@/app/components/base/ui/context-menu'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/app/components/base/ui/dropdown-menu'
|
||||
import MenuItem from './menu-item'
|
||||
|
||||
const MockIcon = (props: React.SVGProps<SVGSVGElement>) => <svg {...props} />
|
||||
@@ -13,8 +23,26 @@ const defaultProps: MenuItemProps = {
|
||||
|
||||
const renderMenuItem = (overrides: Partial<MenuItemProps> = {}) => {
|
||||
const props = { ...defaultProps, ...overrides }
|
||||
const ui = props.menuType === 'dropdown'
|
||||
? (
|
||||
<DropdownMenu open>
|
||||
<DropdownMenuTrigger aria-label="menu trigger">Open</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<MenuItem {...props} />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
: (
|
||||
<ContextMenu open>
|
||||
<ContextMenuTrigger aria-label="context trigger">Open</ContextMenuTrigger>
|
||||
<ContextMenuContent>
|
||||
<MenuItem {...props} />
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
)
|
||||
|
||||
return {
|
||||
...render(<MenuItem {...props} />),
|
||||
...render(ui),
|
||||
onClick: props.onClick,
|
||||
}
|
||||
}
|
||||
@@ -31,7 +59,7 @@ describe('MenuItem', () => {
|
||||
renderMenuItem()
|
||||
|
||||
// Assert
|
||||
expect(screen.getByRole('button', { name: /rename/i })).toBeInTheDocument()
|
||||
expect(screen.getByRole('menuitem', { name: /rename/i })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should apply destructive variant styles when variant is destructive', () => {
|
||||
@@ -39,7 +67,7 @@ describe('MenuItem', () => {
|
||||
renderMenuItem({ variant: 'destructive', label: 'Delete' })
|
||||
|
||||
// Act
|
||||
const button = screen.getByRole('button', { name: /delete/i })
|
||||
const button = screen.getByRole('menuitem', { name: /delete/i })
|
||||
|
||||
// Assert
|
||||
expect(button).toHaveClass('group')
|
||||
@@ -68,8 +96,8 @@ describe('MenuItem', () => {
|
||||
it('should show tooltip content when hovering the tooltip trigger', async () => {
|
||||
// Arrange
|
||||
const tooltipText = 'Show help'
|
||||
const { container } = renderMenuItem({ tooltip: tooltipText })
|
||||
const tooltipIcon = container.querySelector('.i-ri-question-line')
|
||||
renderMenuItem({ tooltip: tooltipText })
|
||||
const tooltipIcon = document.body.querySelector('.i-ri-question-line')
|
||||
|
||||
// Act
|
||||
expect(tooltipIcon).toBeTruthy()
|
||||
@@ -84,27 +112,21 @@ describe('MenuItem', () => {
|
||||
describe('Interactions', () => {
|
||||
it('should call onClick and stop click propagation when button is clicked', () => {
|
||||
// Arrange
|
||||
const outerClick = vi.fn()
|
||||
const onClick = vi.fn()
|
||||
render(
|
||||
<div onClick={outerClick}>
|
||||
<MenuItem {...defaultProps} onClick={onClick} />
|
||||
</div>,
|
||||
)
|
||||
renderMenuItem({ onClick })
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByRole('button', { name: /rename/i }))
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: /rename/i }))
|
||||
|
||||
// Assert
|
||||
expect(onClick).toHaveBeenCalledTimes(1)
|
||||
expect(outerClick).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not trigger onClick when tooltip icon is clicked', () => {
|
||||
// Arrange
|
||||
const onClick = vi.fn()
|
||||
const { container } = renderMenuItem({ onClick, tooltip: 'Help' })
|
||||
const tooltipIcon = container.querySelector('.i-ri-question-line')
|
||||
renderMenuItem({ onClick, tooltip: 'Help' })
|
||||
const tooltipIcon = document.body.querySelector('.i-ri-question-line')
|
||||
|
||||
// Act
|
||||
expect(tooltipIcon).toBeTruthy()
|
||||
@@ -121,13 +143,13 @@ describe('MenuItem', () => {
|
||||
// Arrange
|
||||
const onClick = vi.fn()
|
||||
renderMenuItem({ onClick, disabled: true })
|
||||
const button = screen.getByRole('button', { name: /rename/i })
|
||||
const button = screen.getByRole('menuitem', { name: /rename/i })
|
||||
|
||||
// Act
|
||||
fireEvent.click(button)
|
||||
|
||||
// Assert
|
||||
expect(button).toBeDisabled()
|
||||
expect(button).toHaveAttribute('aria-disabled', 'true')
|
||||
expect(onClick).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,6 +2,16 @@ import type { ReactElement, RefObject } from 'react'
|
||||
import type { NodeApi, TreeApi } from 'react-arborist'
|
||||
import type { TreeNodeData } from '../../type'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuContent,
|
||||
ContextMenuTrigger,
|
||||
} from '@/app/components/base/ui/context-menu'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/app/components/base/ui/dropdown-menu'
|
||||
import { NODE_MENU_TYPE } from '../../constants'
|
||||
import NodeMenu from './node-menu'
|
||||
|
||||
@@ -102,15 +112,40 @@ const renderNodeMenu = ({
|
||||
treeRef,
|
||||
node,
|
||||
}: RenderNodeMenuProps = {}) => {
|
||||
const ui = menuType === 'dropdown'
|
||||
? (
|
||||
<DropdownMenu open>
|
||||
<DropdownMenuTrigger aria-label="menu trigger">Open</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<NodeMenu
|
||||
type={type}
|
||||
menuType={menuType}
|
||||
nodeId={nodeId}
|
||||
onClose={onClose}
|
||||
treeRef={treeRef}
|
||||
node={node}
|
||||
/>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
: (
|
||||
<ContextMenu open>
|
||||
<ContextMenuTrigger aria-label="context trigger">Open</ContextMenuTrigger>
|
||||
<ContextMenuContent>
|
||||
<NodeMenu
|
||||
type={type}
|
||||
menuType={menuType}
|
||||
nodeId={nodeId}
|
||||
onClose={onClose}
|
||||
treeRef={treeRef}
|
||||
node={node}
|
||||
/>
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
)
|
||||
|
||||
render(
|
||||
<NodeMenu
|
||||
type={type}
|
||||
menuType={menuType}
|
||||
nodeId={nodeId}
|
||||
onClose={onClose}
|
||||
treeRef={treeRef}
|
||||
node={node}
|
||||
/>,
|
||||
ui,
|
||||
)
|
||||
|
||||
return { onClose }
|
||||
@@ -128,35 +163,35 @@ describe('NodeMenu', () => {
|
||||
it('should render root folder actions and hide file-only actions', () => {
|
||||
renderNodeMenu({ type: NODE_MENU_TYPE.ROOT })
|
||||
|
||||
expect(screen.getByRole('button', { name: /workflow\.skillSidebar\.menu\.newFile/i })).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: /workflow\.skillSidebar\.menu\.newFolder/i })).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: /workflow\.skillSidebar\.menu\.uploadFile/i })).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: /workflow\.skillSidebar\.menu\.uploadFolder/i })).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: /workflow\.skillSidebar\.menu\.importSkills/i })).toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: /workflow\.skillSidebar\.menu\.cut/i })).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: /workflow\.skillSidebar\.menu\.rename/i })).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: /workflow\.skillSidebar\.menu\.delete/i })).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('menuitem', { name: /workflow\.skillSidebar\.menu\.newFile/i })).toBeInTheDocument()
|
||||
expect(screen.getByRole('menuitem', { name: /workflow\.skillSidebar\.menu\.newFolder/i })).toBeInTheDocument()
|
||||
expect(screen.getByRole('menuitem', { name: /workflow\.skillSidebar\.menu\.uploadFile/i })).toBeInTheDocument()
|
||||
expect(screen.getByRole('menuitem', { name: /workflow\.skillSidebar\.menu\.uploadFolder/i })).toBeInTheDocument()
|
||||
expect(screen.getByRole('menuitem', { name: /workflow\.skillSidebar\.menu\.importSkills/i })).toBeInTheDocument()
|
||||
expect(screen.queryByRole('menuitem', { name: /workflow\.skillSidebar\.menu\.cut/i })).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('menuitem', { name: /workflow\.skillSidebar\.menu\.rename/i })).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('menuitem', { name: /workflow\.skillSidebar\.menu\.delete/i })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render file actions and hide folder-only actions', () => {
|
||||
renderNodeMenu({ type: NODE_MENU_TYPE.FILE })
|
||||
|
||||
expect(screen.getByRole('button', { name: /workflow\.skillSidebar\.menu\.download/i })).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: /workflow\.skillSidebar\.menu\.cut/i })).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: /workflow\.skillSidebar\.menu\.rename/i })).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: /workflow\.skillSidebar\.menu\.delete/i })).toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: /workflow\.skillSidebar\.menu\.newFile/i })).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: /workflow\.skillSidebar\.menu\.newFolder/i })).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: /workflow\.skillSidebar\.menu\.paste/i })).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('menuitem', { name: /workflow\.skillSidebar\.menu\.download/i })).toBeInTheDocument()
|
||||
expect(screen.getByRole('menuitem', { name: /workflow\.skillSidebar\.menu\.cut/i })).toBeInTheDocument()
|
||||
expect(screen.getByRole('menuitem', { name: /workflow\.skillSidebar\.menu\.rename/i })).toBeInTheDocument()
|
||||
expect(screen.getByRole('menuitem', { name: /workflow\.skillSidebar\.menu\.delete/i })).toBeInTheDocument()
|
||||
expect(screen.queryByRole('menuitem', { name: /workflow\.skillSidebar\.menu\.newFile/i })).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('menuitem', { name: /workflow\.skillSidebar\.menu\.newFolder/i })).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('menuitem', { name: /workflow\.skillSidebar\.menu\.paste/i })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should disable menu actions when file operations are loading', () => {
|
||||
mocks.fileOperations.isLoading = true
|
||||
renderNodeMenu({ type: NODE_MENU_TYPE.FOLDER })
|
||||
|
||||
expect(screen.getByRole('button', { name: /workflow\.skillSidebar\.menu\.newFile/i })).toBeDisabled()
|
||||
expect(screen.getByRole('button', { name: /workflow\.skillSidebar\.menu\.newFolder/i })).toBeDisabled()
|
||||
expect(screen.getByRole('button', { name: /workflow\.skillSidebar\.menu\.uploadFile/i })).toBeDisabled()
|
||||
expect(screen.getByRole('menuitem', { name: /workflow\.skillSidebar\.menu\.newFile/i })).toHaveAttribute('aria-disabled', 'true')
|
||||
expect(screen.getByRole('menuitem', { name: /workflow\.skillSidebar\.menu\.newFolder/i })).toHaveAttribute('aria-disabled', 'true')
|
||||
expect(screen.getByRole('menuitem', { name: /workflow\.skillSidebar\.menu\.uploadFile/i })).toHaveAttribute('aria-disabled', 'true')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -164,8 +199,8 @@ describe('NodeMenu', () => {
|
||||
it('should trigger create operations when clicking new file and new folder', () => {
|
||||
renderNodeMenu({ type: NODE_MENU_TYPE.FOLDER })
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /workflow\.skillSidebar\.menu\.newFile/i }))
|
||||
fireEvent.click(screen.getByRole('button', { name: /workflow\.skillSidebar\.menu\.newFolder/i }))
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: /workflow\.skillSidebar\.menu\.newFile/i }))
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: /workflow\.skillSidebar\.menu\.newFolder/i }))
|
||||
|
||||
expect(mocks.fileOperations.handleNewFile).toHaveBeenCalledTimes(1)
|
||||
expect(mocks.fileOperations.handleNewFolder).toHaveBeenCalledTimes(1)
|
||||
@@ -175,8 +210,8 @@ describe('NodeMenu', () => {
|
||||
const clickSpy = vi.spyOn(HTMLInputElement.prototype, 'click')
|
||||
renderNodeMenu({ type: NODE_MENU_TYPE.FOLDER })
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /workflow\.skillSidebar\.menu\.uploadFile/i }))
|
||||
fireEvent.click(screen.getByRole('button', { name: /workflow\.skillSidebar\.menu\.uploadFolder/i }))
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: /workflow\.skillSidebar\.menu\.uploadFile/i }))
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: /workflow\.skillSidebar\.menu\.uploadFolder/i }))
|
||||
|
||||
expect(clickSpy).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
@@ -185,7 +220,7 @@ describe('NodeMenu', () => {
|
||||
mocks.storeState.selectedNodeIds = new Set(['file-1', 'file-2'])
|
||||
const { onClose } = renderNodeMenu({ type: NODE_MENU_TYPE.FILE, nodeId: 'fallback-id' })
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /workflow\.skillSidebar\.menu\.cut/i }))
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: /workflow\.skillSidebar\.menu\.cut/i }))
|
||||
|
||||
expect(mocks.cutNodes).toHaveBeenCalledTimes(1)
|
||||
expect(mocks.cutNodes).toHaveBeenCalledWith(['file-1', 'file-2'])
|
||||
@@ -195,7 +230,7 @@ describe('NodeMenu', () => {
|
||||
it('should cut current node id when no multi-selection exists', () => {
|
||||
const { onClose } = renderNodeMenu({ type: NODE_MENU_TYPE.FILE, nodeId: 'file-3' })
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /workflow\.skillSidebar\.menu\.cut/i }))
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: /workflow\.skillSidebar\.menu\.cut/i }))
|
||||
|
||||
expect(mocks.cutNodes).toHaveBeenCalledWith(['file-3'])
|
||||
expect(onClose).toHaveBeenCalledTimes(1)
|
||||
@@ -207,7 +242,7 @@ describe('NodeMenu', () => {
|
||||
window.addEventListener('skill:paste', pasteListener)
|
||||
const { onClose } = renderNodeMenu({ type: NODE_MENU_TYPE.FOLDER })
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /workflow\.skillSidebar\.menu\.paste/i }))
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: /workflow\.skillSidebar\.menu\.paste/i }))
|
||||
|
||||
expect(pasteListener).toHaveBeenCalledTimes(1)
|
||||
expect(onClose).toHaveBeenCalledTimes(1)
|
||||
@@ -217,9 +252,9 @@ describe('NodeMenu', () => {
|
||||
it('should call download, rename, and delete handlers for file menu actions', () => {
|
||||
renderNodeMenu({ type: NODE_MENU_TYPE.FILE })
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /workflow\.skillSidebar\.menu\.download/i }))
|
||||
fireEvent.click(screen.getByRole('button', { name: /workflow\.skillSidebar\.menu\.rename/i }))
|
||||
fireEvent.click(screen.getByRole('button', { name: /workflow\.skillSidebar\.menu\.delete/i }))
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: /workflow\.skillSidebar\.menu\.download/i }))
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: /workflow\.skillSidebar\.menu\.rename/i }))
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: /workflow\.skillSidebar\.menu\.delete/i }))
|
||||
|
||||
expect(mocks.fileOperations.handleDownload).toHaveBeenCalledTimes(1)
|
||||
expect(mocks.fileOperations.handleRename).toHaveBeenCalledTimes(1)
|
||||
@@ -231,7 +266,7 @@ describe('NodeMenu', () => {
|
||||
it('should open and close import modal from root menu', () => {
|
||||
renderNodeMenu({ type: NODE_MENU_TYPE.ROOT })
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /workflow\.skillSidebar\.menu\.importSkills/i }))
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: /workflow\.skillSidebar\.menu\.importSkills/i }))
|
||||
expect(screen.getByTestId('import-skill-modal')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /close-import-modal/i }))
|
||||
|
||||
Reference in New Issue
Block a user