From 2e1888d37b05de41513f4aa67f54035b9c663f75 Mon Sep 17 00:00:00 2001 From: yyh Date: Tue, 24 Mar 2026 20:10:17 +0800 Subject: [PATCH] fix: tests --- .../skill/file-tree/tree/menu-item.spec.tsx | 56 ++++++--- .../skill/file-tree/tree/node-menu.spec.tsx | 109 ++++++++++++------ 2 files changed, 111 insertions(+), 54 deletions(-) diff --git a/web/app/components/workflow/skill/file-tree/tree/menu-item.spec.tsx b/web/app/components/workflow/skill/file-tree/tree/menu-item.spec.tsx index a3004a0e75..f0c07c187f 100644 --- a/web/app/components/workflow/skill/file-tree/tree/menu-item.spec.tsx +++ b/web/app/components/workflow/skill/file-tree/tree/menu-item.spec.tsx @@ -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) => @@ -13,8 +23,26 @@ const defaultProps: MenuItemProps = { const renderMenuItem = (overrides: Partial = {}) => { const props = { ...defaultProps, ...overrides } + const ui = props.menuType === 'dropdown' + ? ( + + Open + + + + + ) + : ( + + Open + + + + + ) + return { - ...render(), + ...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( -
- -
, - ) + 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() }) }) diff --git a/web/app/components/workflow/skill/file-tree/tree/node-menu.spec.tsx b/web/app/components/workflow/skill/file-tree/tree/node-menu.spec.tsx index 12ecae82d0..db63c23d44 100644 --- a/web/app/components/workflow/skill/file-tree/tree/node-menu.spec.tsx +++ b/web/app/components/workflow/skill/file-tree/tree/node-menu.spec.tsx @@ -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' + ? ( + + Open + + + + + ) + : ( + + Open + + + + + ) + render( - , + 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 }))