diff --git a/oxlint-suppressions.json b/oxlint-suppressions.json index d459c3fafe7..f42c0310ed5 100644 --- a/oxlint-suppressions.json +++ b/oxlint-suppressions.json @@ -44,44 +44,6 @@ "count": 3 } }, - "web/__mocks__/base-ui-dropdown-menu.tsx": { - "jsx_a11y/click-events-have-key-events": { - "count": 3 - }, - "jsx_a11y/interactive-supports-focus": { - "count": 2 - }, - "jsx_a11y/role-has-required-aria-props": { - "count": 1 - } - }, - "web/__mocks__/base-ui-popover.tsx": { - "jsx_a11y/click-events-have-key-events": { - "count": 1 - }, - "jsx_a11y/no-static-element-interactions": { - "count": 1 - } - }, - "web/__mocks__/base-ui-select.tsx": { - "jsx_a11y/click-events-have-key-events": { - "count": 1 - }, - "jsx_a11y/interactive-supports-focus": { - "count": 1 - }, - "jsx_a11y/role-has-required-aria-props": { - "count": 2 - } - }, - "web/__mocks__/base-ui-tooltip.tsx": { - "jsx_a11y/click-events-have-key-events": { - "count": 1 - }, - "jsx_a11y/no-static-element-interactions": { - "count": 1 - } - }, "web/__mocks__/zustand.ts": { "no-barrel-files/no-barrel-files": { "count": 1 @@ -568,11 +530,6 @@ "count": 1 } }, - "web/app/components/app/configuration/debug/__tests__/chat-user-input.spec.tsx": { - "jsx_a11y/role-has-required-aria-props": { - "count": 1 - } - }, "web/app/components/app/configuration/debug/__tests__/index.spec.tsx": { "typescript/no-explicit-any": { "count": 1 @@ -3420,14 +3377,6 @@ "count": 1 } }, - "web/app/components/plugins/reference-setting-modal/auto-update-setting/__tests__/tool-picker.spec.tsx": { - "jsx_a11y/click-events-have-key-events": { - "count": 1 - }, - "jsx_a11y/no-static-element-interactions": { - "count": 1 - } - }, "web/app/components/plugins/reference-setting-modal/auto-update-setting/tool-picker.tsx": { "jsx_a11y/click-events-have-key-events": { "count": 1 @@ -3762,14 +3711,6 @@ "count": 1 } }, - "web/app/components/tools/labels/__tests__/selector.spec.tsx": { - "jsx_a11y/click-events-have-key-events": { - "count": 1 - }, - "jsx_a11y/no-static-element-interactions": { - "count": 1 - } - }, "web/app/components/tools/labels/filter.tsx": { "no-restricted-imports": { "count": 1 @@ -4024,14 +3965,6 @@ "count": 1 } }, - "web/app/components/workflow/nodes/_base/components/__tests__/agent-strategy-selector.spec.tsx": { - "jsx_a11y/click-events-have-key-events": { - "count": 1 - }, - "jsx_a11y/no-static-element-interactions": { - "count": 1 - } - }, "web/app/components/workflow/nodes/_base/components/__tests__/node-handle.spec.tsx": { "jsx_a11y/click-events-have-key-events": { "count": 1 @@ -5244,11 +5177,6 @@ "count": 1 } }, - "web/app/components/workflow/note-node/note-editor/toolbar/__tests__/operator.spec.tsx": { - "jsx_a11y/click-events-have-key-events": { - "count": 1 - } - }, "web/app/components/workflow/note-node/note-editor/toolbar/color-picker.tsx": { "jsx_a11y/click-events-have-key-events": { "count": 1 diff --git a/web/__mocks__/__tests__/base-ui-popover.spec.tsx b/web/__mocks__/__tests__/base-ui-popover.spec.tsx deleted file mode 100644 index 915895977a8..00000000000 --- a/web/__mocks__/__tests__/base-ui-popover.spec.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import { fireEvent, render, screen } from '@testing-library/react' -import * as React from 'react' -import { Popover, PopoverContent, PopoverTrigger } from '../base-ui-popover' - -type PopoverHarnessProps = { - useRenderElement?: boolean - preventDefaultOnTrigger?: boolean -} - -const PopoverHarness = ({ - useRenderElement = false, - preventDefaultOnTrigger = false, -}: PopoverHarnessProps) => { - const [open, setOpen] = React.useState(false) - - return ( -
-
outside
- - { - if (preventDefaultOnTrigger) event.preventDefault() - }} - > - toggle - - ) : undefined - } - > - fallback trigger - - - } - popupProps={{ 'data-popup': 'true' } as unknown as React.HTMLAttributes} - > -
popover body
-
-
-
{open ? 'open' : 'closed'}
-
- ) -} - -describe('base-ui-popover mock', () => { - it('should toggle popover content from the fallback trigger and expose content props', () => { - render() - - expect(screen.getByTestId('open-state')).toHaveTextContent('closed') - expect(screen.queryByTestId('popover-content')).not.toBeInTheDocument() - - fireEvent.click(screen.getByTestId('popover-trigger')) - - expect(screen.getByTestId('open-state')).toHaveTextContent('open') - expect(screen.getByTestId('popover-content')).toHaveAttribute('data-placement', 'bottom-start') - expect(screen.getByTestId('popover-content')).toHaveAttribute('data-side-offset', '4') - expect(screen.getByTestId('popover-content')).toHaveAttribute('data-align-offset', '8') - expect(screen.getByTestId('popover-content')).toHaveAttribute('data-positioner', 'true') - expect(screen.getByTestId('popover-content')).toHaveAttribute('data-popup', 'true') - expect(screen.getByTestId('popover-content')).toHaveClass('custom-content') - }) - - it('should keep the popover open on inside clicks and close it on outside clicks or escape', () => { - render() - - fireEvent.click(screen.getByTestId('custom-trigger')) - expect(screen.getByTestId('open-state')).toHaveTextContent('open') - - fireEvent.mouseDown(screen.getByTestId('popover-content')) - expect(screen.getByTestId('open-state')).toHaveTextContent('open') - - fireEvent.keyDown(document, { key: 'Escape' }) - expect(screen.getByTestId('open-state')).toHaveTextContent('closed') - - fireEvent.click(screen.getByTestId('custom-trigger')) - expect(screen.getByTestId('open-state')).toHaveTextContent('open') - - fireEvent.mouseDown(screen.getByTestId('outside-area')) - expect(screen.getByTestId('open-state')).toHaveTextContent('closed') - }) - - it('should preserve rendered trigger props and respect preventDefault', () => { - render() - - fireEvent.click(screen.getByTestId('custom-trigger')) - - expect(screen.getByTestId('custom-trigger')).toHaveAttribute('data-popover-trigger', 'true') - expect(screen.getByTestId('open-state')).toHaveTextContent('closed') - expect(screen.queryByTestId('popover-content')).not.toBeInTheDocument() - }) - - it('should keep the popover closed when the fallback trigger click is prevented', () => { - const handleClick = (event: React.MouseEvent) => { - event.preventDefault() - } - - render( -
- - fallback trigger - -
popover body
-
-
-
, - ) - - fireEvent.click(screen.getByTestId('popover-trigger')) - - expect(screen.queryByTestId('popover-content')).not.toBeInTheDocument() - }) -}) diff --git a/web/__mocks__/base-ui-dropdown-menu.tsx b/web/__mocks__/base-ui-dropdown-menu.tsx deleted file mode 100644 index d3b439f0dfc..00000000000 --- a/web/__mocks__/base-ui-dropdown-menu.tsx +++ /dev/null @@ -1,230 +0,0 @@ -import type { ReactNode } from 'react' -import * as React from 'react' - -const DropdownMenuContext = React.createContext({ - open: false, - onOpenChange: (_open: boolean) => {}, -}) - -type DropdownMenuProps = { - children?: ReactNode - open?: boolean - onOpenChange?: (open: boolean) => void -} - -type TriggerHtmlProps = React.HTMLAttributes & { - 'data-testid'?: string - 'data-disabled'?: string - 'data-popup-open'?: string -} - -type DropdownMenuTriggerProps = TriggerHtmlProps & { - children?: ReactNode - disabled?: boolean - nativeButton?: boolean - render?: - | React.ReactElement - | ((props: TriggerHtmlProps, state: { open: boolean }) => React.ReactElement) -} - -type DropdownMenuContentProps = React.HTMLAttributes & { - children?: ReactNode - placement?: string - sideOffset?: number - alignOffset?: number - popupClassName?: string -} - -export const DropdownMenu = ({ children, open, onOpenChange }: DropdownMenuProps) => { - const [localOpen, setLocalOpen] = React.useState(false) - const resolvedOpen = open ?? localOpen - const handleOpenChange = React.useCallback( - (nextOpen: boolean) => { - setLocalOpen(nextOpen) - onOpenChange?.(nextOpen) - }, - [onOpenChange], - ) - - return ( - -
- {children} -
-
- ) -} - -export const DropdownMenuTrigger = ({ - children, - render, - nativeButton: _nativeButton, - onClick, - disabled, - ...props -}: DropdownMenuTriggerProps) => { - const { open, onOpenChange } = React.useContext(DropdownMenuContext) - const handleClick = (event: React.MouseEvent) => { - if (disabled) return - onClick?.(event) - if (!event.defaultPrevented) onOpenChange(!open) - } - - if (typeof render === 'function') { - return render( - { - ...props, - 'aria-disabled': disabled || undefined, - 'data-testid': props['data-testid'] ?? 'dropdown-menu-trigger', - 'data-disabled': disabled ? '' : undefined, - 'data-popup-open': open ? '' : undefined, - onClick: handleClick, - }, - { open }, - ) - } - - const node = render ?? children - const isNativeButton = React.isValidElement(node) && node.type === 'button' - - if (React.isValidElement(node)) { - const triggerElement = node as React.ReactElement> - const childProps = (triggerElement.props ?? {}) as React.HTMLAttributes & { - 'data-testid'?: string - } - const triggerProps = props as React.HTMLAttributes & { 'data-testid'?: string } - const role = - childProps.role ?? - triggerProps.role ?? - (!isNativeButton && (childProps['aria-label'] || triggerProps['aria-label']) - ? 'button' - : undefined) - return React.cloneElement( - triggerElement, - { - ...props, - ...childProps, - 'data-testid': - childProps['data-testid'] ?? triggerProps['data-testid'] ?? 'dropdown-menu-trigger', - 'data-disabled': disabled ? '' : undefined, - 'data-popup-open': open ? '' : undefined, - disabled: isNativeButton ? disabled : undefined, - 'aria-disabled': !isNativeButton && disabled ? true : childProps['aria-disabled'], - role, - tabIndex: - childProps.tabIndex ?? triggerProps.tabIndex ?? (role === 'button' ? 0 : undefined), - onClick: (event: React.MouseEvent) => { - childProps.onClick?.(event) - handleClick(event) - }, - }, - render ? (children ?? childProps.children) : childProps.children, - ) - } - - return ( -
- {node} -
- ) -} - -export const DropdownMenuContent = ({ - children, - className, - popupClassName, - placement, - sideOffset, - alignOffset, - ...props -}: DropdownMenuContentProps) => { - const { open } = React.useContext(DropdownMenuContext) - if (!open) return null - - return ( -
- {children} -
- ) -} - -export const DropdownMenuItem = ({ - children, - onClick, - ...props -}: React.HTMLAttributes & { children?: ReactNode }) => ( -
- {children} -
-) - -export const DropdownMenuRadioGroup = ({ - children, - onValueChange, - ...props -}: React.HTMLAttributes & { - children?: ReactNode - value?: unknown - onValueChange?: (value: unknown) => void -}) => ( -
- {React.Children.map(children, (child) => { - if (!React.isValidElement(child)) return child - return React.cloneElement( - child as React.ReactElement<{ __onValueChange?: (value: unknown) => void }>, - { __onValueChange: onValueChange }, - ) - })} -
-) - -export const DropdownMenuRadioItem = ({ - children, - value, - onClick, - __onValueChange, - ...props -}: React.HTMLAttributes & { - children?: ReactNode - value?: unknown - __onValueChange?: (value: unknown) => void -}) => ( -
{ - onClick?.(event) - __onValueChange?.(value) - }} - {...props} - > - {children} -
-) - -export const DropdownMenuRadioItemIndicator = ({ children }: { children?: ReactNode }) => ( - <>{children} -) -export const DropdownMenuCheckboxItem = DropdownMenuItem -export const DropdownMenuCheckboxItemIndicator = ({ children }: { children?: ReactNode }) => ( - <>{children} -) -export const DropdownMenuLabel = ({ children }: { children?: ReactNode }) => <>{children} -export const DropdownMenuSeparator = (props: React.HTMLAttributes) => ( -
-) -export const DropdownMenuSub = ({ children }: { children?: ReactNode }) => <>{children} -export const DropdownMenuSubTrigger = DropdownMenuItem -export const DropdownMenuSubContent = ({ children }: { children?: ReactNode }) => <>{children} diff --git a/web/__mocks__/base-ui-popover.tsx b/web/__mocks__/base-ui-popover.tsx deleted file mode 100644 index 65324a56733..00000000000 --- a/web/__mocks__/base-ui-popover.tsx +++ /dev/null @@ -1,190 +0,0 @@ -import type { ReactNode } from 'react' -import * as React from 'react' - -const PopoverContext = React.createContext({ - open: false, - onOpenChange: (_open: boolean) => {}, -}) - -type PopoverProps = { - children?: ReactNode - open?: boolean - onOpenChange?: (open: boolean) => void -} - -type TriggerHtmlProps = React.HTMLAttributes & { - 'data-testid'?: string - 'data-popover-trigger'?: string - 'data-popup-open'?: string -} - -type PopoverTriggerProps = TriggerHtmlProps & { - children?: ReactNode - nativeButton?: boolean - render?: - | React.ReactElement - | ((props: TriggerHtmlProps, state: { open: boolean }) => React.ReactElement) -} - -type PopoverContentProps = React.HTMLAttributes & { - children?: ReactNode - placement?: string - sideOffset?: number - alignOffset?: number - popupClassName?: string - positionerProps?: React.HTMLAttributes - popupProps?: React.HTMLAttributes -} - -export const Popover = ({ children, open, onOpenChange }: PopoverProps) => { - const [localOpen, setLocalOpen] = React.useState(false) - const resolvedOpen = open ?? localOpen - const handleOpenChange = React.useCallback( - (nextOpen: boolean) => { - setLocalOpen(nextOpen) - onOpenChange?.(nextOpen) - }, - [onOpenChange], - ) - - React.useEffect(() => { - if (!resolvedOpen) return - - const handleMouseDown = (event: MouseEvent) => { - const target = event.target as Element | null - if (target?.closest?.('[data-popover-trigger="true"], [data-popover-content="true"]')) return - - handleOpenChange(false) - } - - const handleKeyDown = (event: KeyboardEvent) => { - if (event.key === 'Escape') handleOpenChange(false) - } - - document.addEventListener('mousedown', handleMouseDown) - document.addEventListener('keydown', handleKeyDown) - - return () => { - document.removeEventListener('mousedown', handleMouseDown) - document.removeEventListener('keydown', handleKeyDown) - } - }, [resolvedOpen, handleOpenChange]) - - return ( - -
- {children} -
-
- ) -} - -export const PopoverTrigger = ({ - children, - render, - nativeButton: _nativeButton, - onClick, - ...props -}: PopoverTriggerProps) => { - const { open, onOpenChange } = React.useContext(PopoverContext) - if (typeof render === 'function') { - const triggerProps: TriggerHtmlProps = { - ...props, - 'data-testid': props['data-testid'] ?? 'popover-trigger', - 'data-popover-trigger': 'true', - 'data-popup-open': open ? '' : undefined, - onClick: (event: React.MouseEvent) => { - onClick?.(event) - if (event.defaultPrevented) return - onOpenChange(!open) - }, - } - return render(triggerProps, { open }) - } - - const node = render ?? children - - if (React.isValidElement(node)) { - const triggerElement = node as React.ReactElement> - const childProps = (triggerElement.props ?? {}) as React.HTMLAttributes & { - 'data-testid'?: string - } - const triggerProps = props as React.HTMLAttributes & { 'data-testid'?: string } - - return React.cloneElement( - triggerElement, - { - ...props, - ...childProps, - 'data-testid': - childProps['data-testid'] ?? triggerProps['data-testid'] ?? 'popover-trigger', - 'data-popover-trigger': 'true', - 'data-popup-open': open ? '' : undefined, - onClick: (event: React.MouseEvent) => { - childProps.onClick?.(event) - onClick?.(event) - if (event.defaultPrevented) return - onOpenChange(!open) - }, - }, - render ? (children ?? childProps.children) : childProps.children, - ) - } - - return ( -
{ - onClick?.(event) - if (event.defaultPrevented) return - onOpenChange(!open) - }} - {...props} - > - {node} -
- ) -} - -export const PopoverContent = ({ - children, - className, - placement, - sideOffset, - alignOffset, - popupClassName, - positionerProps, - popupProps, - ...props -}: PopoverContentProps) => { - const { open } = React.useContext(PopoverContext) - - if (!open) return null - - return ( -
- {children} -
- ) -} - -export const PopoverClose = ({ children }: { children?: ReactNode }) => <>{children} -export const PopoverTitle = ({ children }: { children?: ReactNode }) => <>{children} -export const PopoverDescription = ({ children }: { children?: ReactNode }) => <>{children} diff --git a/web/__mocks__/base-ui-select.tsx b/web/__mocks__/base-ui-select.tsx deleted file mode 100644 index 80fce8a5a9b..00000000000 --- a/web/__mocks__/base-ui-select.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import type { ReactNode } from 'react' -import * as React from 'react' - -const SelectContext = React.createContext({ - value: undefined as unknown, - onValueChange: (_value: unknown) => {}, -}) - -type SelectProps = { - children?: ReactNode - value?: unknown - onValueChange?: (value: unknown) => void -} - -export const Select = ({ children, value, onValueChange }: SelectProps) => ( - {}) }}> -
{children}
-
-) - -export const SelectTrigger = ({ - children, - ...props -}: React.ButtonHTMLAttributes & { children?: ReactNode }) => ( - -) - -export const SelectValue = ({ placeholder }: { placeholder?: ReactNode }) => <>{placeholder} - -export const SelectContent = ({ - children, - popupClassName, -}: { - children?: ReactNode - popupClassName?: string -}) => ( -
- {children} -
-) - -export const SelectItem = ({ - children, - value, - onClick, - ...props -}: React.HTMLAttributes & { children?: ReactNode; value?: unknown }) => { - const select = React.useContext(SelectContext) - return ( -
{ - onClick?.(event) - select.onValueChange(value) - }} - {...props} - > - {children} -
- ) -} - -export const SelectItemText = ({ children }: { children?: ReactNode }) => <>{children} -export const SelectItemIndicator = ({ children }: { children?: ReactNode }) => <>{children} -export const SelectGroup = ({ children }: { children?: ReactNode }) => <>{children} -export const SelectLabel = () => null -export const SelectGroupLabel = ({ children }: { children?: ReactNode }) => <>{children} -export const SelectSeparator = (props: React.HTMLAttributes) => ( -
-) diff --git a/web/__mocks__/base-ui-tooltip.tsx b/web/__mocks__/base-ui-tooltip.tsx deleted file mode 100644 index c1ee0790b0a..00000000000 --- a/web/__mocks__/base-ui-tooltip.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import type { ReactNode } from 'react' -import * as React from 'react' - -const TooltipContext = React.createContext({ - open: false, - onOpenChange: (_open: boolean) => {}, -}) - -type TooltipProps = { - children?: ReactNode - open?: boolean - onOpenChange?: (open: boolean) => void -} - -export const Tooltip = ({ children, open, onOpenChange }: TooltipProps) => { - const [localOpen, setLocalOpen] = React.useState(false) - const resolvedOpen = open ?? localOpen - const handleOpenChange = React.useCallback( - (nextOpen: boolean) => { - setLocalOpen(nextOpen) - onOpenChange?.(nextOpen) - }, - [onOpenChange], - ) - - return ( - - {children} - - ) -} - -export const TooltipTrigger = ({ - children, - render, - nativeButton: _nativeButton, - ...props -}: React.HTMLAttributes & { - children?: ReactNode - render?: React.ReactElement - nativeButton?: boolean -}) => { - const { open, onOpenChange } = React.useContext(TooltipContext) - const node = render ?? children - - if (React.isValidElement(node)) { - const triggerElement = node as React.ReactElement> - const childProps = (triggerElement.props ?? {}) as React.HTMLAttributes - - return React.cloneElement(triggerElement, { - ...props, - ...childProps, - onMouseEnter: (event: React.MouseEvent) => { - childProps.onMouseEnter?.(event) - props.onMouseEnter?.(event) - onOpenChange(true) - }, - onMouseLeave: (event: React.MouseEvent) => { - childProps.onMouseLeave?.(event) - props.onMouseLeave?.(event) - onOpenChange(false) - }, - onClick: (event: React.MouseEvent) => { - childProps.onClick?.(event) - props.onClick?.(event) - onOpenChange(!open) - }, - }) - } - - return ( - { - props.onMouseEnter?.(event) - onOpenChange(true) - }} - onMouseLeave={(event) => { - props.onMouseLeave?.(event) - onOpenChange(false) - }} - onClick={(event) => { - props.onClick?.(event) - onOpenChange(!open) - }} - > - {node} - - ) -} - -export const TooltipContent = ({ - children, - ...props -}: React.HTMLAttributes & { children?: ReactNode }) => { - const { open } = React.useContext(TooltipContext) - if (!open) return null - return
{children}
-} - -export const TooltipProvider = ({ children }: { children?: ReactNode }) => <>{children} diff --git a/web/__tests__/interactive-component-mock-boundary.spec.ts b/web/__tests__/interactive-component-mock-boundary.spec.ts new file mode 100644 index 00000000000..668bb4c47ca --- /dev/null +++ b/web/__tests__/interactive-component-mock-boundary.spec.ts @@ -0,0 +1,37 @@ +import { readdirSync, readFileSync } from 'node:fs' +import { relative, resolve } from 'node:path' + +const testFilePattern = /\.(?:spec|test)\.[cm]?[jt]sx?$/ +const interactivePrimitiveMockPattern = + /vi\.mock\(\s*['"]@langgenius\/dify-ui\/(?:alert-dialog|avatar|button|dialog|dropdown-menu|pagination|popover|select|slider|switch|textarea|tooltip)['"]/ +const interactiveWrapperMockPattern = + /vi\.mock\(\s*['"][^'"]*(?:block-selector|plugin-version-picker|time-picker)['"]/ + +const collectTestFiles = (directory: string): string[] => { + return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => { + const path = resolve(directory, entry.name) + if (entry.isDirectory()) return collectTestFiles(path) + return testFilePattern.test(entry.name) ? [path] : [] + }) +} + +describe('interactive component mock boundary', () => { + it('keeps Dify UI primitives and feature-owned interactive wrappers real', () => { + const webRoot = process.cwd() + const testFiles = ['__tests__', 'app', 'features'].flatMap((directory) => + collectTestFiles(resolve(webRoot, directory)), + ) + const violations = testFiles.flatMap((file) => { + const source = readFileSync(file, 'utf8') + if ( + !interactivePrimitiveMockPattern.test(source) && + !interactiveWrapperMockPattern.test(source) + ) { + return [] + } + return [relative(webRoot, file)] + }) + + expect(violations).toEqual([]) + }) +}) diff --git a/web/app/components/app-sidebar/app-info/__tests__/app-info-detail-panel.spec.tsx b/web/app/components/app-sidebar/app-info/__tests__/app-info-detail-panel.spec.tsx index f92d245027e..db2d9345f74 100644 --- a/web/app/components/app-sidebar/app-info/__tests__/app-info-detail-panel.spec.tsx +++ b/web/app/components/app-sidebar/app-info/__tests__/app-info-detail-panel.spec.tsx @@ -58,32 +58,6 @@ vi.mock('@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view', default: ({ appId }: { appId: string }) =>
, })) -vi.mock('@langgenius/dify-ui/button', () => ({ - Button: ({ - children, - onClick, - className, - size, - variant, - }: { - children: React.ReactNode - onClick?: () => void - className?: string - size?: string - variant?: string - }) => ( - - ), -})) - vi.mock('../app-operations', () => ({ default: ({ primaryOperations, diff --git a/web/app/components/app-sidebar/snippet-info/__tests__/dropdown.spec.tsx b/web/app/components/app-sidebar/snippet-info/__tests__/dropdown.spec.tsx index a10c856fad4..0dddfd30adb 100644 --- a/web/app/components/app-sidebar/snippet-info/__tests__/dropdown.spec.tsx +++ b/web/app/components/app-sidebar/snippet-info/__tests__/dropdown.spec.tsx @@ -14,8 +14,6 @@ const mockUpdateMutate = vi.fn() const mockExportMutateAsync = vi.fn() const mockDeleteMutate = vi.fn() let mockWorkspacePermissionKeys: string[] = ['snippets.create_and_modify', 'snippets.management'] -let mockDropdownOpen = false -let mockDropdownOnOpenChange: ((open: boolean) => void) | undefined const mockConsoleState = vi.hoisted(() => ({ current: { get workspacePermissionKeys() { @@ -46,51 +44,6 @@ vi.mock('@langgenius/dify-ui/toast', () => ({ }, })) -vi.mock('@langgenius/dify-ui/dropdown-menu', () => ({ - DropdownMenu: ({ - open, - onOpenChange, - children, - }: { - open?: boolean - onOpenChange?: (open: boolean) => void - children: React.ReactNode - }) => { - mockDropdownOpen = !!open - mockDropdownOnOpenChange = onOpenChange - return
{children}
- }, - DropdownMenuTrigger: ({ - children, - className, - }: { - children: React.ReactNode - className?: string - }) => ( - - ), - DropdownMenuContent: ({ children }: { children: React.ReactNode }) => - mockDropdownOpen ?
{children}
: null, - DropdownMenuItem: ({ - children, - onClick, - }: { - children: React.ReactNode - onClick?: () => void - }) => ( - - ), - DropdownMenuSeparator: () =>
, -})) - vi.mock('@/service/use-snippets', () => ({ useUpdateSnippetMutation: () => ({ mutate: mockUpdateMutate, @@ -173,8 +126,6 @@ describe('SnippetInfoDropdown', () => { beforeEach(() => { vi.clearAllMocks() mockWorkspacePermissionKeys = ['snippets.create_and_modify', 'snippets.management'] - mockDropdownOpen = false - mockDropdownOnOpenChange = undefined }) // Rendering coverage for the menu trigger itself. diff --git a/web/app/components/app/app-publisher/__tests__/index.spec.tsx b/web/app/components/app/app-publisher/__tests__/index.spec.tsx index ae5cf4a4e9f..1b1a82991bd 100644 --- a/web/app/components/app/app-publisher/__tests__/index.spec.tsx +++ b/web/app/components/app/app-publisher/__tests__/index.spec.tsx @@ -171,8 +171,6 @@ vi.mock('@/app/components/tools/workflow-tool', () => ({ ), })) -vi.mock('@langgenius/dify-ui/popover', () => import('@/__mocks__/base-ui-popover')) - vi.mock('../sections', () => ({ PublisherSummarySection: (props: Record) => { sectionProps.summary = props diff --git a/web/app/components/app/app-publisher/__tests__/publish-with-multiple-model.spec.tsx b/web/app/components/app/app-publisher/__tests__/publish-with-multiple-model.spec.tsx index 6991ea8aab7..1d840da7e9b 100644 --- a/web/app/components/app/app-publisher/__tests__/publish-with-multiple-model.spec.tsx +++ b/web/app/components/app/app-publisher/__tests__/publish-with-multiple-model.spec.tsx @@ -17,91 +17,6 @@ vi.mock('../../header/account-setting/model-provider-page/model-icon', () => ({ ), })) -vi.mock('@langgenius/dify-ui/dropdown-menu', async () => { - const ReactModule = await vi.importActual('react') - const OpenContext = ReactModule.createContext<{ - open: boolean - setOpen: (nextOpen: boolean) => void - } | null>(null) - - const useOpenContext = () => { - const context = ReactModule.use(OpenContext) - if (!context) throw new Error('DropdownMenu components must be wrapped in DropdownMenu') - return context - } - - return { - DropdownMenu: ({ - children, - open, - onOpenChange, - }: { - children: React.ReactNode - open: boolean - onOpenChange?: (open: boolean) => void - }) => ( - -
{children}
-
- ), - DropdownMenuTrigger: ({ - children, - render, - }: { - children: React.ReactNode - render?: React.ReactElement - }) => { - const { open, setOpen } = useOpenContext() - - if (render) { - return ReactModule.cloneElement( - render, - { - onClick: () => setOpen(!open), - } as Record, - children, - ) - } - - return ( - - ) - }, - DropdownMenuContent: ({ - children, - popupClassName, - }: { - children: React.ReactNode - popupClassName?: string - }) => { - const context = useOpenContext() - return context.open ?
{children}
: null - }, - DropdownMenuItem: ({ - children, - onClick, - }: { - children: React.ReactNode - onClick?: React.MouseEventHandler - }) => { - const { setOpen } = useOpenContext() - return ( - - ) - }, - } -}) - describe('PublishWithMultipleModel', () => { beforeEach(() => { vi.clearAllMocks() diff --git a/web/app/components/app/configuration/config-prompt/conversation-history/__tests__/edit-modal.spec.tsx b/web/app/components/app/configuration/config-prompt/conversation-history/__tests__/edit-modal.spec.tsx index 7ffa5d72524..1983417a0c1 100644 --- a/web/app/components/app/configuration/config-prompt/conversation-history/__tests__/edit-modal.spec.tsx +++ b/web/app/components/app/configuration/config-prompt/conversation-history/__tests__/edit-modal.spec.tsx @@ -3,13 +3,6 @@ import { fireEvent, render, screen } from '@testing-library/react' import * as React from 'react' import EditModal from '../edit-modal' -vi.mock('@langgenius/dify-ui/dialog', () => ({ - Dialog: ({ children, open }: { children: React.ReactNode; open?: boolean }) => - open === false ? null : <>{children}, - DialogContent: ({ children }: { children: React.ReactNode }) =>
{children}
, - DialogTitle: ({ children }: { children: React.ReactNode }) =>
{children}
, -})) - describe('Conversation history edit modal', () => { const data: ConversationHistoriesRole = { user_prefix: 'user', diff --git a/web/app/components/app/configuration/config-var/config-modal/__tests__/form-fields.spec.tsx b/web/app/components/app/configuration/config-var/config-modal/__tests__/form-fields.spec.tsx index 7a471f053a9..822629be8f9 100644 --- a/web/app/components/app/configuration/config-var/config-modal/__tests__/form-fields.spec.tsx +++ b/web/app/components/app/configuration/config-var/config-modal/__tests__/form-fields.spec.tsx @@ -1,6 +1,7 @@ /* oxlint-disable typescript/no-explicit-any */ import type { ReactNode } from 'react' import { fireEvent, render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' import { InputVarType } from '@/app/components/workflow/types' import { withSelectorKey } from '@/test/i18n-mock' import ConfigModalFormFields from '../form-fields' @@ -84,46 +85,6 @@ vi.mock('@/app/components/workflow/nodes/_base/components/editor/code-editor', ( ), })) -vi.mock('@langgenius/dify-ui/select', async (importOriginal) => { - const actual = await importOriginal() - - return { - ...actual, - Select: ({ - value, - onValueChange, - children, - }: { - value: string - onValueChange: (value: string) => void - children: ReactNode - }) => ( -
- - - {children} -
- ), - SelectTrigger: ({ children }: { children: ReactNode }) =>
{children}
, - SelectValue: () => select-value, - SelectContent: ({ children }: { children: ReactNode }) =>
{children}
, - SelectItem: ({ children }: { children: ReactNode }) =>
{children}
, - SelectItemText: ({ children }: { children: ReactNode }) => {children}, - SelectItemIndicator: () => , - } -}) - -vi.mock('@langgenius/dify-ui/tooltip', () => ({ - Tooltip: ({ children }: { children: ReactNode }) =>
{children}
, - TooltipTrigger: ({ children }: { children: ReactNode }) =>
{children}
, - TooltipContent: ({ children }: { children: ReactNode }) =>
{children}
, -})) - vi.mock('../field', () => ({ default: ({ children, title }: { children: ReactNode; title: string }) => (
@@ -201,7 +162,8 @@ const createBaseProps = () => { } describe('ConfigModalFormFields', () => { - it('should update paragraph, number, checkbox, and select defaults', () => { + it('should update paragraph, number, checkbox, and select defaults', async () => { + const user = userEvent.setup() const paragraphProps = createBaseProps() paragraphProps.tempPayload = { ...paragraphProps.tempPayload, @@ -229,9 +191,12 @@ describe('ConfigModalFormFields', () => { default: false, } checkboxProps.checkboxDefaultSelectValue = 'true' - render() - fireEvent.click(screen.getByText('ui-select:true')) + const checkboxView = render() + await user.click(screen.getByRole('combobox')) + const checkboxOptions = await screen.findAllByRole('option') + await user.click(checkboxOptions[1]!) expect(checkboxProps.payloadChangeHandlers.default).toHaveBeenCalledWith(false) + checkboxView.unmount() const selectProps = createBaseProps() selectProps.tempPayload = { @@ -242,7 +207,9 @@ describe('ConfigModalFormFields', () => { selectProps.options = ['alpha', 'beta'] render() fireEvent.click(screen.getByText('config-select')) - fireEvent.click(screen.getByText('ui-select:alpha')) + await user.click(screen.getByRole('combobox')) + const selectOptions = await screen.findAllByRole('option') + await user.click(selectOptions[2]!) expect(selectProps.payloadChangeHandlers.options).toHaveBeenCalledWith(['alpha', 'beta']) expect(selectProps.payloadChangeHandlers.default).toHaveBeenCalledWith('beta') }) @@ -348,7 +315,8 @@ describe('ConfigModalFormFields', () => { expect(textProps.payloadChangeHandlers.default).toHaveBeenCalledWith(undefined) }) - it('should clear select defaults and apply uploader fallback values', () => { + it('should clear select defaults and apply uploader fallback values', async () => { + const user = userEvent.setup() const selectProps = createBaseProps() selectProps.tempPayload = { ...selectProps.tempPayload, @@ -356,10 +324,13 @@ describe('ConfigModalFormFields', () => { default: 'alpha', } selectProps.options = ['alpha', ' ', 'beta'] - render() + const selectView = render() - fireEvent.click(screen.getByText('ui-select-empty')) + await user.click(screen.getByRole('combobox')) + const selectOptions = await screen.findAllByRole('option') + await user.click(selectOptions[0]!) expect(selectProps.payloadChangeHandlers.default).toHaveBeenCalledWith(undefined) + selectView.unmount() const singleFallbackProps = createBaseProps() singleFallbackProps.tempPayload = { @@ -416,21 +387,10 @@ describe('ConfigModalFormFields', () => { render() expect(screen.getAllByText('config-select')).toHaveLength(1) - expect(screen.queryByText('ui-select:__empty__')).not.toBeInTheDocument() + expect(screen.queryByRole('combobox')).not.toBeInTheDocument() }) - it('should preserve existing select and file defaults when present', () => { - const selectProps = createBaseProps() - selectProps.tempPayload = { - ...selectProps.tempPayload, - type: InputVarType.select, - default: undefined, - } - selectProps.options = ['alpha', 'beta'] - render() - - expect(screen.getByText('ui-select:__empty__')).toBeInTheDocument() - + it('should preserve existing file defaults when present', () => { const existingFile = { fileId: 'existing-file', type: 'local_file', diff --git a/web/app/components/app/configuration/config-var/config-modal/__tests__/type-select.spec.tsx b/web/app/components/app/configuration/config-var/config-modal/__tests__/type-select.spec.tsx index 148f5872647..4aad4aadd31 100644 --- a/web/app/components/app/configuration/config-var/config-modal/__tests__/type-select.spec.tsx +++ b/web/app/components/app/configuration/config-var/config-modal/__tests__/type-select.spec.tsx @@ -3,8 +3,6 @@ import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import TypeSelector from '../type-select' -vi.mock('@langgenius/dify-ui/select', () => import('@/__mocks__/base-ui-select')) - vi.mock('@/app/components/workflow/nodes/_base/components/input-var-type-icon', () => ({ default: ({ type }: { type: string }) => {type}, })) diff --git a/web/app/components/app/configuration/config/agent/agent-setting/__tests__/index.spec.tsx b/web/app/components/app/configuration/config/agent/agent-setting/__tests__/index.spec.tsx index e05d0af638f..287fbbc49bf 100644 --- a/web/app/components/app/configuration/config/agent/agent-setting/__tests__/index.spec.tsx +++ b/web/app/components/app/configuration/config/agent/agent-setting/__tests__/index.spec.tsx @@ -3,25 +3,6 @@ import { act, fireEvent, render, screen } from '@testing-library/react' import { MAX_ITERATIONS_NUM } from '@/config' import { AgentSetting } from '../index' -vi.mock('@langgenius/dify-ui/slider', () => ({ - Slider: (props: { - className?: string - min?: number - max?: number - value: number - onValueChange: (value: number) => void - }) => ( - props.onValueChange(Number(e.target.value))} - /> - ), -})) - const basePayload = { enabled: true, strategy: 'react', diff --git a/web/app/components/app/configuration/dataset-config/context-var/__tests__/var-picker.spec.tsx b/web/app/components/app/configuration/dataset-config/context-var/__tests__/var-picker.spec.tsx index 0b3c1a63886..5babe06c125 100644 --- a/web/app/components/app/configuration/dataset-config/context-var/__tests__/var-picker.spec.tsx +++ b/web/app/components/app/configuration/dataset-config/context-var/__tests__/var-picker.spec.tsx @@ -1,45 +1,8 @@ -import type { ReactNode } from 'react' import type { Props } from '../var-picker' import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' -import * as React from 'react' import VarPicker from '../var-picker' -vi.mock('@langgenius/dify-ui/popover', () => { - const PopoverContext = React.createContext({ - open: false, - onOpenChange: undefined as ((open: boolean) => void) | undefined, - }) - - return { - Popover: ({ - children, - open, - onOpenChange, - }: { - children: ReactNode - open?: boolean - onOpenChange?: (open: boolean) => void - }) => {children}, - PopoverTrigger: ({ render }: { render?: ReactNode }) => { - const { open, onOpenChange } = React.use(PopoverContext) - return ( - - ) - }, - PopoverContent: ({ children }: { children: ReactNode }) => { - const { open } = React.use(PopoverContext) - return open ? ( -
- {children} -
- ) : null - }, - } -}) - const options: Props['options'] = [ { name: 'Variable 1', value: 'var1', type: 'string' }, { name: 'Variable 2', value: 'var2', type: 'number' }, @@ -48,7 +11,7 @@ const options: Props['options'] = [ describe('VarPicker', () => { it('shows the selected variable', () => { render() - expect(screen.getByRole('button', { name: 'choose variable' })).toHaveTextContent('var1') + expect(screen.getByRole('button')).toHaveTextContent('var1') }) it('shows the configured empty-selection message', () => { @@ -64,19 +27,21 @@ describe('VarPicker', () => { }) it('selects a variable and closes the options', async () => { + const user = userEvent.setup() const onChange = vi.fn() render() - await userEvent.click(screen.getByRole('button', { name: 'choose variable' })) - await userEvent.click(screen.getByText('var2')) + await user.click(screen.getByRole('button')) + await user.click(screen.getByText('var2')) expect(onChange).toHaveBeenCalledWith('var2') - expect(screen.queryByRole('dialog', { name: 'variable options' })).not.toBeInTheDocument() + expect(screen.queryByText('var2')).not.toBeInTheDocument() }) it('shows the empty state when no variables are available', async () => { + const user = userEvent.setup() render() - await userEvent.click(screen.getByRole('button', { name: 'choose variable' })) + await user.click(screen.getByRole('button')) expect(screen.getByText('appDebug.feature.dataSet.queryVariable.noVar')).toBeInTheDocument() }) }) diff --git a/web/app/components/app/configuration/debug/__tests__/chat-user-input.spec.tsx b/web/app/components/app/configuration/debug/__tests__/chat-user-input.spec.tsx index b4506f33dcb..636d64fcd4d 100644 --- a/web/app/components/app/configuration/debug/__tests__/chat-user-input.spec.tsx +++ b/web/app/components/app/configuration/debug/__tests__/chat-user-input.spec.tsx @@ -1,6 +1,7 @@ import type { Inputs, ModelConfig } from '@/models/debug' import type { PromptVariable } from '@/types/app' import { fireEvent, render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' import ChatUserInput from '../chat-user-input' const mockSetInputs = vi.fn() @@ -41,75 +42,6 @@ vi.mock('@/app/components/base/input', () => ({ ), })) -vi.mock('@langgenius/dify-ui/select', async () => { - const React = await import('react') - const SelectContext = React.createContext<{ - disabled?: boolean - onValueChange?: (value: string) => void - value?: string | null - }>({}) - - return { - Select: ({ - children, - disabled, - onValueChange, - value, - }: { - children: React.ReactNode - disabled?: boolean - onValueChange?: (value: string) => void - value?: string | null - }) => ( - -
{children}
-
- ), - SelectValue: ({ placeholder }: { placeholder?: React.ReactNode }) => { - const context = React.use(SelectContext) - return <>{context.value || placeholder} - }, - SelectTrigger: ({ children, className }: { children: React.ReactNode; className?: string }) => { - const context = React.useContext(SelectContext) - return ( -
- - -
- ) - }, - SelectContent: ({ children }: { children: React.ReactNode }) =>
{children}
, - SelectItem: ({ children, value }: { children: React.ReactNode; value: string }) => { - const context = React.useContext(SelectContext) - return ( - - ) - }, - SelectItemText: ({ children }: { children: React.ReactNode }) => <>{children}, - SelectItemIndicator: () => null, - } -}) - vi.mock('@/app/components/workflow/nodes/_base/components/before-run-form/bool-input', () => ({ default: ({ name, @@ -256,7 +188,8 @@ describe('ChatUserInput', () => { expect(screen.getByRole('textbox', { name: 'Description' })).toBeInTheDocument() }) - it('should render select input type', () => { + it('should render select input type', async () => { + const user = userEvent.setup() mockUseContext.mockReturnValue( createContextValue({ modelConfig: createModelConfig([ @@ -271,10 +204,11 @@ describe('ChatUserInput', () => { ) render() - expect(screen.getByTestId('select-input')).toBeInTheDocument() - expect(screen.getByText('A')).toBeInTheDocument() - expect(screen.getByText('B')).toBeInTheDocument() - expect(screen.getByText('C')).toBeInTheDocument() + const select = screen.getByRole('combobox') + await user.click(select) + expect(await screen.findByRole('option', { name: 'A' })).toBeInTheDocument() + expect(screen.getByRole('option', { name: 'B' })).toBeInTheDocument() + expect(screen.getByRole('option', { name: 'C' })).toBeInTheDocument() }) it('should render number input type', () => { @@ -324,7 +258,7 @@ describe('ChatUserInput', () => { render() expect(screen.getByTestId('input-Name')).toBeInTheDocument() expect(screen.getByRole('textbox', { name: 'Description' })).toBeInTheDocument() - expect(screen.getByTestId('select-input')).toBeInTheDocument() + expect(screen.getByRole('combobox')).toBeInTheDocument() }) it('should show optional label for non-required fields', () => { @@ -497,7 +431,8 @@ describe('ChatUserInput', () => { expect(mockSetInputs).toHaveBeenCalledWith({ desc: 'New Description' }) }) - it('should call setInputs when select input changes', () => { + it('should call setInputs when select input changes', async () => { + const user = userEvent.setup() mockUseContext.mockReturnValue( createContextValue({ modelConfig: createModelConfig([ @@ -512,31 +447,12 @@ describe('ChatUserInput', () => { ) render() - fireEvent.click(screen.getByTestId('select-B')) + await user.click(screen.getByRole('combobox')) + await user.click(await screen.findByRole('option', { name: 'B' })) expect(mockSetInputs).toHaveBeenCalledWith({ choice: 'B' }) }) - it('should ignore empty select updates', () => { - mockUseContext.mockReturnValue( - createContextValue({ - modelConfig: createModelConfig([ - createPromptVariable({ - key: 'choice', - name: 'Choice', - type: 'select', - options: ['A', 'B', 'C'], - }), - ]), - }), - ) - - render() - fireEvent.click(screen.getByTestId('select-empty')) - - expect(mockSetInputs).not.toHaveBeenCalled() - }) - it('should call setInputs when number input changes', () => { mockUseContext.mockReturnValue( createContextValue({ @@ -676,7 +592,7 @@ describe('ChatUserInput', () => { ) render() - expect(screen.getByTestId('select-input')).toBeDisabled() + expect(screen.getByRole('combobox')).toBeDisabled() }) it('should disable checkbox when configuration is readonly and test/run is denied', () => { @@ -862,7 +778,8 @@ describe('ChatUserInput', () => { }) describe('Edge Cases', () => { - it('should handle select with empty options', () => { + it('should show no options when a select has no configured choices', async () => { + const user = userEvent.setup() mockUseContext.mockReturnValue( createContextValue({ modelConfig: createModelConfig([ @@ -872,25 +789,11 @@ describe('ChatUserInput', () => { ) render() - const select = screen.getByTestId('select-input') - expect(select).toBeInTheDocument() + const select = screen.getByRole('combobox') + await user.click(select) expect(screen.queryAllByRole('option')).toHaveLength(0) }) - it('should handle select with undefined options', () => { - mockUseContext.mockReturnValue( - createContextValue({ - modelConfig: createModelConfig([ - createPromptVariable({ key: 'choice', name: 'Choice', type: 'select' }), - ]), - }), - ) - - render() - const select = screen.getByTestId('select-input') - expect(select).toBeInTheDocument() - }) - it('should preserve other input values when updating one field', () => { mockUseContext.mockReturnValue( createContextValue({ diff --git a/web/app/components/app/configuration/debug/debug-with-multiple-model/__tests__/chat-item.spec.tsx b/web/app/components/app/configuration/debug/debug-with-multiple-model/__tests__/chat-item.spec.tsx index 8d88c235a5d..1779b60ec58 100644 --- a/web/app/components/app/configuration/debug/debug-with-multiple-model/__tests__/chat-item.spec.tsx +++ b/web/app/components/app/configuration/debug/debug-with-multiple-model/__tests__/chat-item.spec.tsx @@ -102,10 +102,6 @@ vi.mock('@/app/components/base/chat/chat', () => ({ }, })) -vi.mock('@langgenius/dify-ui/avatar', () => ({ - Avatar: ({ name }: { name: string }) =>
{name}
, -})) - const createModelAndParameter = ( overrides: Partial = {}, ): ModelAndParameter => ({ diff --git a/web/app/components/app/configuration/prompt-value-panel/__tests__/index.spec.tsx b/web/app/components/app/configuration/prompt-value-panel/__tests__/index.spec.tsx index 4fcb36ba44a..303781a233d 100644 --- a/web/app/components/app/configuration/prompt-value-panel/__tests__/index.spec.tsx +++ b/web/app/components/app/configuration/prompt-value-panel/__tests__/index.spec.tsx @@ -1,6 +1,7 @@ /* oxlint-disable typescript/no-explicit-any */ import type { IPromptValuePanelProps } from '../index' import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' import * as React from 'react' import { beforeEach, describe, expect, it, vi } from 'vitest' import ConfigContext from '@/context/debug-configuration' @@ -9,29 +10,6 @@ import PromptValuePanel from '../index' const mockSetShowAppConfigureFeaturesModal = vi.fn() -vi.mock('@langgenius/dify-ui/button', () => ({ - Button: ({ - children, - onClick, - disabled, - className, - }: { - children: React.ReactNode - onClick?: () => void - disabled?: boolean - className?: string - }) => ( - - ), -})) - vi.mock('@/app/components/app/store', () => ({ useStore: ( selector: (state: { @@ -65,60 +43,6 @@ vi.mock('@/app/components/base/features/new-feature-panel/feature-bar', () => ({ ), })) -vi.mock('@langgenius/dify-ui/select', async () => { - const React = await import('react') - const SelectContext = React.createContext<{ - onValueChange?: (value: string) => void - value?: string | null - }>({}) - - return { - Select: ({ - children, - onValueChange, - value, - }: { - children: React.ReactNode - onValueChange?: (value: string) => void - value?: string | null - }) => ( - -
{children}
-
- ), - SelectValue: ({ placeholder }: { placeholder?: React.ReactNode }) => { - const context = React.use(SelectContext) - return <>{context.value || placeholder} - }, - SelectTrigger: ({ children }: { children: React.ReactNode }) => { - const context = React.useContext(SelectContext) - return ( -
- - -
- ) - }, - SelectContent: ({ children }: { children: React.ReactNode }) =>
{children}
, - SelectItem: ({ children, value }: { children: React.ReactNode; value: string }) => { - const context = React.useContext(SelectContext) - return ( - - ) - }, - SelectItemText: ({ children }: { children: React.ReactNode }) => <>{children}, - SelectItemIndicator: () => null, - } -}) - vi.mock('@/app/components/workflow/nodes/_base/components/before-run-form/bool-input', () => ({ default: ({ name, onChange }: { name: string; onChange: (value: boolean) => void }) => ( -
- ), -})) - describe('Logs', () => { beforeEach(() => { vi.clearAllMocks() @@ -179,7 +171,7 @@ describe('Logs', () => { />, ) - fireEvent.click(screen.getByText('go-to-page-2')) + fireEvent.click(screen.getByRole('button', { name: 'Go to page 2' })) expect(mockReplace).toHaveBeenCalledWith('/apps/app-1/logs?page=2', { scroll: false }) }) diff --git a/web/app/components/app/log/__tests__/model-info.spec.tsx b/web/app/components/app/log/__tests__/model-info.spec.tsx index b3b20187579..c759f13125e 100644 --- a/web/app/components/app/log/__tests__/model-info.spec.tsx +++ b/web/app/components/app/log/__tests__/model-info.spec.tsx @@ -1,4 +1,5 @@ import { fireEvent, render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' import ModelInfo from '../model-info' vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({ @@ -30,62 +31,6 @@ vi.mock('@/app/components/header/account-setting/model-provider-page/model-name' ), })) -vi.mock('@langgenius/dify-ui/popover', async () => { - const React = await import('react') - const PopoverContext = React.createContext<{ - open: boolean - onOpenChange?: (open: boolean) => void - } | null>(null) - - return { - Popover: ({ - children, - open, - onOpenChange, - }: { - children: React.ReactNode - open: boolean - onOpenChange?: (open: boolean) => void - }) => ( - -
- {children} -
-
- ), - PopoverTrigger: ({ - children, - render, - }: { - children?: React.ReactNode - render?: React.ReactNode - }) => { - const context = React.useContext(PopoverContext) - const content = render ?? children - const handleClick = () => { - context?.onOpenChange?.(!context.open) - } - - if (React.isValidElement(content)) { - const element = content as React.ReactElement<{ onClick?: () => void }> - return React.cloneElement(element, { onClick: handleClick }) - } - - return ( - - ) - }, - PopoverContent: ({ children }: { children: React.ReactNode }) => { - const context = React.useContext(PopoverContext) - if (!context?.open) return null - - return
{children}
- }, - } -}) - describe('ModelInfo', () => { const defaultModel = { name: 'gpt-4', @@ -132,32 +77,32 @@ describe('ModelInfo', () => { it('should be closed by default', () => { render() - expect(screen.getByTestId('popover-root')).toHaveAttribute('data-open', 'false') - expect(screen.queryByTestId('popover-content')).not.toBeInTheDocument() + expect(screen.queryByText(/(?:^|\.)detail\.modelParams(?=$|:)/)).not.toBeInTheDocument() }) - it('should open when info button is clicked', () => { + it('should open when info button is clicked', async () => { + const user = userEvent.setup() render() const trigger = screen.getByRole('button') - fireEvent.click(trigger) + await user.click(trigger) - expect(screen.getByTestId('popover-root')).toHaveAttribute('data-open', 'true') - expect(screen.getByTestId('popover-content')).toBeInTheDocument() + expect(screen.getByText(/(?:^|\.)detail\.modelParams(?=$|:)/)).toBeInTheDocument() + expect(trigger).toHaveAttribute('aria-expanded', 'true') }) - it('should close when info button is clicked again', () => { + it('should close when info button is clicked again', async () => { + const user = userEvent.setup() render() const trigger = screen.getByRole('button') - // Open - fireEvent.click(trigger) - expect(screen.getByTestId('popover-root')).toHaveAttribute('data-open', 'true') + await user.click(trigger) + expect(screen.getByText(/(?:^|\.)detail\.modelParams(?=$|:)/)).toBeInTheDocument() - // Close - fireEvent.click(trigger) - expect(screen.getByTestId('popover-root')).toHaveAttribute('data-open', 'false') + await user.click(trigger) + expect(screen.queryByText(/(?:^|\.)detail\.modelParams(?=$|:)/)).not.toBeInTheDocument() + expect(trigger).toHaveAttribute('aria-expanded', 'false') }) }) diff --git a/web/app/components/app/overview/__tests__/trigger-card.spec.tsx b/web/app/components/app/overview/__tests__/trigger-card.spec.tsx index 3e0d268af7a..7a66e4a479b 100644 --- a/web/app/components/app/overview/__tests__/trigger-card.spec.tsx +++ b/web/app/components/app/overview/__tests__/trigger-card.spec.tsx @@ -48,9 +48,23 @@ vi.mock('@/service/use-triggers', () => ({ })) vi.mock('@/utils', () => ({ + asyncRunSafe: async (promise: Promise) => { + try { + return [null, await promise] + } catch (error) { + return [error] + } + }, canFindTool: () => false, })) +vi.mock('@/context/permission-state', async () => { + const { createPermissionStateModuleMock } = await import('@/test/console/state-fixture') + return createPermissionStateModuleMock(() => ({ + workspacePermissionKeys: ['app.create_and_management'], + })) +}) + vi.mock('@/app/components/workflow/block-icon', () => ({ default: ({ type }: { type: string }) => (
@@ -59,28 +73,6 @@ vi.mock('@/app/components/workflow/block-icon', () => ({ ), })) -vi.mock('@langgenius/dify-ui/switch', () => ({ - Switch: ({ - checked, - onCheckedChange, - disabled, - }: { - checked: boolean - onCheckedChange: (v: boolean) => void - disabled: boolean - }) => ( - - ), -})) - describe('TriggerCard', () => { const mockAppInfo = { id: 'test-app-id', @@ -210,7 +202,7 @@ describe('TriggerCard', () => { it('should render switches for each trigger', () => { render() - const switches = screen.getAllByTestId('switch') + const switches = screen.getAllByRole('switch') expect(switches.length).toBe(2) }) }) @@ -231,7 +223,7 @@ describe('TriggerCard', () => { it('should call updateTriggerStatus when toggle is clicked', async () => { render() - const switchBtn = screen.getByTestId('switch') + const switchBtn = screen.getByRole('switch') fireEvent.click(switchBtn) await waitFor(() => { @@ -246,7 +238,7 @@ describe('TriggerCard', () => { it('should update trigger status in store optimistically', async () => { render() - const switchBtn = screen.getByTestId('switch') + const switchBtn = screen.getByRole('switch') fireEvent.click(switchBtn) await waitFor(() => { @@ -257,7 +249,7 @@ describe('TriggerCard', () => { it('should invalidate app triggers after successful update', async () => { render() - const switchBtn = screen.getByTestId('switch') + const switchBtn = screen.getByRole('switch') fireEvent.click(switchBtn) await waitFor(() => { @@ -268,7 +260,7 @@ describe('TriggerCard', () => { it('should call onToggleResult with null on success', async () => { render() - const switchBtn = screen.getByTestId('switch') + const switchBtn = screen.getByRole('switch') fireEvent.click(switchBtn) await waitFor(() => { @@ -282,7 +274,7 @@ describe('TriggerCard', () => { render() - const switchBtn = screen.getByTestId('switch') + const switchBtn = screen.getByRole('switch') fireEvent.click(switchBtn) await waitFor(() => { @@ -360,7 +352,7 @@ describe('TriggerCard', () => { render() - const switchBtn = screen.getByTestId('switch') + const switchBtn = screen.getByRole('switch') expect(switchBtn).toBeInTheDocument() }) @@ -383,8 +375,8 @@ describe('TriggerCard', () => { , ) - const switchBtn = screen.getByTestId('switch') - expect(switchBtn).toHaveAttribute('data-disabled', 'true') + const switchBtn = screen.getByRole('switch') + expect(switchBtn).toHaveAttribute('aria-disabled', 'true') fireEvent.click(switchBtn) diff --git a/web/app/components/apps/__tests__/app-card.spec.tsx b/web/app/components/apps/__tests__/app-card.spec.tsx index 421b09fa9b4..4f4a9dcca9c 100644 --- a/web/app/components/apps/__tests__/app-card.spec.tsx +++ b/web/app/components/apps/__tests__/app-card.spec.tsx @@ -41,6 +41,9 @@ const render = (ui: React.ReactElement) => }, }) +const getOperationsTrigger = () => + screen.getByRole('button', { name: /common\.operation\.moreActionsFor/ }) + // Mock next/navigation const mockPush = vi.fn() vi.mock('@/next/navigation', () => ({ @@ -358,123 +361,6 @@ vi.mock('@/next/dynamic', () => ({ }, })) -vi.mock('@langgenius/dify-ui/dropdown-menu', () => { - type DropdownMenuContextValue = { - isOpen: boolean - setOpen: (open: boolean) => void - } - const DropdownMenuContext = React.createContext(null) - - const useDropdownMenuContext = () => { - const context = React.use(DropdownMenuContext) - if (!context) throw new Error('DropdownMenu components must be wrapped in DropdownMenu') - return context - } - - return { - DropdownMenu: ({ - children, - open = false, - modal, - onOpenChange, - }: { - children: React.ReactNode - open?: boolean - modal?: boolean - onOpenChange?: (open: boolean) => void - }) => ( - -
- {children} -
-
- ), - DropdownMenuTrigger: ({ - children, - className, - onClick, - ...props - }: React.ButtonHTMLAttributes) => { - const { isOpen, setOpen } = useDropdownMenuContext() - return ( - - ) - }, - DropdownMenuContent: ({ - children, - className, - popupClassName, - popupProps, - positionerProps, - }: { - children: React.ReactNode - className?: string - popupClassName?: string - popupProps?: React.HTMLAttributes - positionerProps?: React.HTMLAttributes - }) => { - const { isOpen } = useDropdownMenuContext() - if (!isOpen) return null - - return ( -
-
- {children} -
-
- ) - }, - DropdownMenuItem: ({ - children, - className, - onClick, - destructive, - disabled, - }: { - children: React.ReactNode - className?: string - onClick?: React.MouseEventHandler - destructive?: boolean - disabled?: boolean - }) => { - const { setOpen } = useDropdownMenuContext() - return ( - - ) - }, - DropdownMenuSeparator: () =>
, - } -}) - // AppCardTags has tag API dependencies - mock for isolated testing vi.mock('@/features/tag-management/components/app-card-tags', () => ({ AppCardTags: ({ @@ -839,37 +725,24 @@ describe('AppCard', () => { }) describe('Operations Menu', () => { - it('should render operations dropdown menu', () => { - render() - expect(screen.getByTestId('dropdown-menu')).toBeInTheDocument() - }) - - it('should render dropdown menu as non-modal', () => { - render() - expect(screen.getByTestId('dropdown-menu')).toHaveAttribute('data-modal', 'false') - }) - it('should reveal operations trigger when card receives keyboard focus', () => { render() - const operationsTriggerWrapper = screen - .getByTestId('dropdown-menu-trigger') - .closest('.absolute') + const operationsTrigger = getOperationsTrigger() + const operationsTriggerWrapper = operationsTrigger.closest('.absolute') expect(operationsTriggerWrapper).toHaveClass('top-2') expect(operationsTriggerWrapper).toHaveClass('right-2') expect(operationsTriggerWrapper).toHaveClass('group-focus-within:pointer-events-auto') expect(operationsTriggerWrapper).toHaveClass('group-focus-within:opacity-100') expect(operationsTriggerWrapper).not.toHaveClass('w-[120px]') - expect(screen.getByTestId('dropdown-menu-trigger')).toHaveClass('focus-visible:ring-2') - expect(screen.getByTestId('dropdown-menu-trigger')).toHaveClass( - 'focus-visible:ring-state-accent-solid', - ) + expect(operationsTrigger).toHaveClass('focus-visible:ring-2') + expect(operationsTrigger).toHaveClass('focus-visible:ring-state-accent-solid') }) it('should show edit option when dropdown menu is opened', async () => { render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { expect(screen.getByText('app.editApp')).toBeInTheDocument() @@ -879,7 +752,7 @@ describe('AppCard', () => { it('should show duplicate option when dropdown menu is opened', async () => { render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { expect(screen.getByText('app.duplicate')).toBeInTheDocument() @@ -894,7 +767,7 @@ describe('AppCard', () => { }) render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { expect(screen.getByText('app.duplicate')).toBeInTheDocument() @@ -910,7 +783,7 @@ describe('AppCard', () => { }) render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { expect(screen.getByText('app.duplicate')).toBeInTheDocument() @@ -921,7 +794,7 @@ describe('AppCard', () => { it('should show export option when dropdown menu is opened', async () => { render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { expect(screen.getByText('app.export')).toBeInTheDocument() @@ -931,7 +804,7 @@ describe('AppCard', () => { it('should show delete option when dropdown menu is opened', async () => { render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { expect(screen.getByText('common.operation.delete')).toBeInTheDocument() @@ -942,7 +815,7 @@ describe('AppCard', () => { const chatApp = { ...mockApp, mode: AppModeEnum.CHAT } render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { expect(screen.getByText(/switch/i)).toBeInTheDocument() @@ -959,7 +832,7 @@ describe('AppCard', () => { }) render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { expect(screen.getByText(/switch/i)).toBeInTheDocument() @@ -971,7 +844,7 @@ describe('AppCard', () => { const completionApp = { ...mockApp, mode: AppModeEnum.COMPLETION } render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { expect(screen.getByText(/switch/i)).toBeInTheDocument() @@ -982,7 +855,7 @@ describe('AppCard', () => { const workflowApp = { ...mockApp, mode: AppModeEnum.WORKFLOW } render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { expect(screen.queryByText(/switch/i)).not.toBeInTheDocument() @@ -994,7 +867,7 @@ describe('AppCard', () => { it('should open edit modal when edit button is clicked', async () => { render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { const editButton = screen.getByText('app.editApp') @@ -1009,7 +882,7 @@ describe('AppCard', () => { it('should open duplicate modal when duplicate button is clicked', async () => { render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { const duplicateButton = screen.getByText('app.duplicate') @@ -1024,7 +897,7 @@ describe('AppCard', () => { it('should open confirm dialog when delete button is clicked', async () => { render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) fireEvent.click(await screen.findByRole('menuitem', { name: 'common.operation.delete' })) expect(await screen.findByRole('alertdialog')).toBeInTheDocument() }) @@ -1032,7 +905,7 @@ describe('AppCard', () => { it('should close confirm dialog when cancel is clicked', async () => { render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) fireEvent.click(await screen.findByRole('menuitem', { name: 'common.operation.delete' })) expect(await screen.findByRole('alertdialog')).toBeInTheDocument() fireEvent.click(screen.getByRole('button', { name: 'common.operation.cancel' })) @@ -1044,7 +917,7 @@ describe('AppCard', () => { it('should not submit delete when confirmation text does not match', async () => { render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) fireEvent.click(await screen.findByRole('menuitem', { name: 'common.operation.delete' })) const form = (await screen.findByRole('alertdialog')).querySelector('form') @@ -1057,7 +930,7 @@ describe('AppCard', () => { it('should close edit modal when onHide is called', async () => { render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { fireEvent.click(screen.getByText('app.editApp')) }) @@ -1077,7 +950,7 @@ describe('AppCard', () => { it('should close duplicate modal when onHide is called', async () => { render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { fireEvent.click(screen.getByText('app.duplicate')) }) @@ -1097,7 +970,7 @@ describe('AppCard', () => { it('should clear delete confirmation input after closing the dialog', async () => { render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) fireEvent.click(await screen.findByRole('menuitem', { name: 'common.operation.delete' })) const deleteInput = await screen.findByRole('textbox') @@ -1108,7 +981,7 @@ describe('AppCard', () => { expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument() }) - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) fireEvent.click(await screen.findByRole('menuitem', { name: 'common.operation.delete' })) await waitFor(() => { @@ -1122,7 +995,7 @@ describe('AppCard', () => { render() // Open dropdown menu and click delete - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) fireEvent.click(await screen.findByRole('menuitem', { name: 'common.operation.delete' })) expect(await screen.findByRole('alertdialog')).toBeInTheDocument() @@ -1140,7 +1013,7 @@ describe('AppCard', () => { it('should not call onRefresh after successful delete', async () => { render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) fireEvent.click(await screen.findByRole('menuitem', { name: 'common.operation.delete' })) expect(await screen.findByRole('alertdialog')).toBeInTheDocument() @@ -1161,7 +1034,7 @@ describe('AppCard', () => { render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) fireEvent.click(await screen.findByRole('menuitem', { name: 'common.operation.delete' })) expect(await screen.findByRole('alertdialog')).toBeInTheDocument() @@ -1185,7 +1058,7 @@ describe('AppCard', () => { render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) fireEvent.click(await screen.findByRole('menuitem', { name: 'common.operation.delete' })) expect(await screen.findByRole('alertdialog')).toBeInTheDocument() @@ -1204,7 +1077,7 @@ describe('AppCard', () => { it('should call updateAppInfo API when editing app', async () => { render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { fireEvent.click(screen.getByText('app.editApp')) }) @@ -1223,7 +1096,7 @@ describe('AppCard', () => { it('should edit successfully without onRefresh callback', async () => { render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { fireEvent.click(screen.getByText('app.editApp')) }) @@ -1243,7 +1116,7 @@ describe('AppCard', () => { it('should call copyApp API when duplicating app', async () => { render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { fireEvent.click(screen.getByText('app.duplicate')) }) @@ -1262,7 +1135,7 @@ describe('AppCard', () => { it('should call onPlanInfoChanged after successful duplication', async () => { render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { fireEvent.click(screen.getByText('app.duplicate')) }) @@ -1281,7 +1154,7 @@ describe('AppCard', () => { it('should duplicate successfully without onRefresh callback', async () => { render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { fireEvent.click(screen.getByText('app.duplicate')) }) @@ -1304,7 +1177,7 @@ describe('AppCard', () => { render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { fireEvent.click(screen.getByText('app.duplicate')) }) @@ -1327,7 +1200,7 @@ describe('AppCard', () => { it('should export the app DSL when exporting', async () => { render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { fireEvent.click(screen.getByText('app.export')) }) @@ -1352,7 +1225,7 @@ describe('AppCard', () => { const chatApp = { ...mockApp, mode: AppModeEnum.CHAT } render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { fireEvent.click(screen.getByText('app.switch')) }) @@ -1366,7 +1239,7 @@ describe('AppCard', () => { const chatApp = { ...mockApp, mode: AppModeEnum.CHAT } render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { fireEvent.click(screen.getByText('app.switch')) }) @@ -1386,7 +1259,7 @@ describe('AppCard', () => { const chatApp = { ...mockApp, mode: AppModeEnum.CHAT } render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { fireEvent.click(screen.getByText('app.switch')) }) @@ -1406,7 +1279,7 @@ describe('AppCard', () => { const chatApp = { ...mockApp, mode: AppModeEnum.CHAT } render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { fireEvent.click(screen.getByText('app.switch')) }) @@ -1426,7 +1299,7 @@ describe('AppCard', () => { const completionApp = { ...mockApp, mode: AppModeEnum.COMPLETION } render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { fireEvent.click(screen.getByText('app.switch')) }) @@ -1441,7 +1314,7 @@ describe('AppCard', () => { it('should show open in explore option when dropdown menu is opened', async () => { render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { expect(screen.getByText('app.openInExplore')).toBeInTheDocument() @@ -1454,7 +1327,7 @@ describe('AppCard', () => { const workflowApp = { ...mockApp, mode: AppModeEnum.WORKFLOW } render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { fireEvent.click(screen.getByText('app.export')) }) @@ -1477,7 +1350,7 @@ describe('AppCard', () => { const workflowApp = { ...mockApp, mode: AppModeEnum.WORKFLOW } render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { fireEvent.click(screen.getByText('app.export')) }) @@ -1491,7 +1364,7 @@ describe('AppCard', () => { const workflowApp = { ...mockApp, mode: AppModeEnum.WORKFLOW } render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { fireEvent.click(screen.getByText('app.export')) }) @@ -1510,7 +1383,7 @@ describe('AppCard', () => { const advancedChatApp = { ...mockApp, mode: AppModeEnum.ADVANCED_CHAT } render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { fireEvent.click(screen.getByText('app.export')) }) @@ -1532,7 +1405,7 @@ describe('AppCard', () => { const workflowApp = { ...mockApp, mode: AppModeEnum.WORKFLOW } render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { fireEvent.click(screen.getByText('app.export')) }) @@ -1623,7 +1496,7 @@ describe('AppCard', () => { render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { fireEvent.click(screen.getByText('app.editApp')) }) @@ -1648,7 +1521,7 @@ describe('AppCard', () => { render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { fireEvent.click(screen.getByText('app.editApp')) }) @@ -1668,7 +1541,7 @@ describe('AppCard', () => { it('should close edit modal after successful edit', async () => { render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { fireEvent.click(screen.getByText('app.editApp')) }) @@ -1710,7 +1583,7 @@ describe('AppCard', () => { const chatApp = createMockApp({ mode: AppModeEnum.CHAT }) render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { fireEvent.click(screen.getByText('app.switch')) }) @@ -1732,7 +1605,7 @@ describe('AppCard', () => { const completionApp = createMockApp({ mode: AppModeEnum.COMPLETION }) const { unmount } = render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { expect(screen.getByText('app.editApp')).toBeInTheDocument() }) @@ -1743,7 +1616,7 @@ describe('AppCard', () => { const workflowApp = createMockApp({ mode: AppModeEnum.WORKFLOW }) render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { expect(screen.getByText('app.editApp')).toBeInTheDocument() }) @@ -1767,15 +1640,11 @@ describe('AppCard', () => { it('should close operations menu after selecting an item', async () => { render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) - await waitFor(() => { - expect(screen.getByTestId('dropdown-menu-content')).toBeInTheDocument() - }) - - fireEvent.click(screen.getByText('app.editApp')) + fireEvent.click(getOperationsTrigger()) + fireEvent.click(await screen.findByRole('menuitem', { name: 'app.editApp' })) await waitFor(() => { - expect(screen.queryByTestId('dropdown-menu-content')).not.toBeInTheDocument() + expect(getOperationsTrigger()).toHaveAttribute('aria-expanded', 'false') expect(screen.getByTestId('edit-app-modal')).toBeInTheDocument() }) }) @@ -1783,7 +1652,7 @@ describe('AppCard', () => { it('should click open in explore button', async () => { render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { const openInExploreBtn = screen.getByText('app.openInExplore') fireEvent.click(openInExploreBtn) @@ -1807,7 +1676,7 @@ describe('AppCard', () => { render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { const openInExploreBtn = screen.getByText('app.openInExplore') fireEvent.click(openInExploreBtn) @@ -1835,7 +1704,7 @@ describe('AppCard', () => { render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { const openInExploreBtn = screen.getByText('app.openInExplore') fireEvent.click(openInExploreBtn) @@ -1858,7 +1727,7 @@ describe('AppCard', () => { render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { fireEvent.click(screen.getByText('app.openInExplore')) }) @@ -1873,7 +1742,7 @@ describe('AppCard', () => { it('should render operations menu correctly', async () => { render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { expect(screen.getByText('app.editApp')).toBeInTheDocument() expect(screen.getByText('app.duplicate')).toBeInTheDocument() @@ -1897,12 +1766,16 @@ describe('AppCard', () => { expect( screen.getByRole('menuitem', { name: 'app.editApp', hidden: true }), ).toBeInTheDocument() - expect(screen.getByTestId('dropdown-menu-positioner')).toHaveAttribute( + expect( + document.querySelector( + `[data-step-by-step-tour-highlight-part="${STEP_BY_STEP_TOUR_TARGETS.studioWithAppsFirstAppCardActionsMenu}"]`, + ), + ).toHaveAttribute( 'data-step-by-step-tour-highlight-part', STEP_BY_STEP_TOUR_TARGETS.studioWithAppsFirstAppCardActionsMenu, ) - expect(screen.getByTestId('dropdown-menu-content')).toHaveAttribute('aria-hidden', 'true') - expect(screen.getByTestId('dropdown-menu-content')).toHaveClass('pointer-events-none') + expect(screen.getByRole('menu', { hidden: true })).toHaveAttribute('aria-hidden', 'true') + expect(screen.getByRole('menu', { hidden: true })).toHaveClass('pointer-events-none') }) }) @@ -1914,7 +1787,7 @@ describe('AppCard', () => { }) render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { expect(screen.getByText('app.openInExplore')).toBeInTheDocument() }) @@ -1945,7 +1818,7 @@ describe('AppCard', () => { render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { const openInExploreBtn = screen.getByText('app.openInExplore') fireEvent.click(openInExploreBtn) @@ -1972,7 +1845,7 @@ describe('AppCard', () => { render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { const openInExploreBtn = screen.getByText('app.openInExplore') fireEvent.click(openInExploreBtn) @@ -1989,7 +1862,7 @@ describe('AppCard', () => { const draftTriggerApp = createMockApp({ has_draft_trigger: true }) render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { expect(screen.getByText('app.editApp')).toBeInTheDocument() // openInExplore should not be shown for draft trigger apps @@ -2014,7 +1887,7 @@ describe('AppCard', () => { it('should show access control option when webapp_auth is enabled', async () => { render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { expect(screen.getByText('app.accessControl')).toBeInTheDocument() }) @@ -2028,7 +1901,7 @@ describe('AppCard', () => { }) render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { expect(screen.getByText('app.accessControl')).toBeInTheDocument() @@ -2043,7 +1916,7 @@ describe('AppCard', () => { }) render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { expect(screen.getByText('common.operation.delete')).toBeInTheDocument() @@ -2061,7 +1934,7 @@ describe('AppCard', () => { }) render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { expect(screen.getByText('common.operation.delete')).toBeInTheDocument() @@ -2077,7 +1950,7 @@ describe('AppCard', () => { }) render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { fireEvent.click(screen.getByText('common.settings.resourceAccess')) }) @@ -2088,7 +1961,7 @@ describe('AppCard', () => { it('should click access control button', async () => { render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { const accessControlBtn = screen.getByText('app.accessControl') fireEvent.click(accessControlBtn) @@ -2102,7 +1975,7 @@ describe('AppCard', () => { it('should close access control modal and call onRefresh', async () => { render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { fireEvent.click(screen.getByText('app.accessControl')) }) @@ -2122,7 +1995,7 @@ describe('AppCard', () => { it('should close access control modal after confirm without onRefresh callback', async () => { render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { fireEvent.click(screen.getByText('app.accessControl')) }) @@ -2141,7 +2014,7 @@ describe('AppCard', () => { it('should show open in explore when userCanAccessApp is true', async () => { render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { expect(screen.getByText('app.openInExplore')).toBeInTheDocument() }) @@ -2157,7 +2030,7 @@ describe('AppCard', () => { render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { expect(screen.getByText('app.openInExplore')).toBeInTheDocument() }) @@ -2175,7 +2048,7 @@ describe('AppCard', () => { it('should close access control modal when onClose is called', async () => { render() - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) + fireEvent.click(getOperationsTrigger()) await waitFor(() => { fireEvent.click(screen.getByText('app.accessControl')) }) @@ -2192,120 +2065,4 @@ describe('AppCard', () => { }) }) }) - - describe('Delete dialog guards', () => { - const createMockAlertDialogModule = () => ({ - AlertDialog: ({ - open, - onOpenChange, - children, - }: { - open: boolean - onOpenChange?: (open: boolean) => void - children: React.ReactNode - }) => - open ? ( -
- - - {children} -
- ) : null, - AlertDialogContent: ({ children }: { children: React.ReactNode }) =>
{children}
, - AlertDialogTitle: ({ children }: { children: React.ReactNode }) =>
{children}
, - AlertDialogDescription: ({ children }: { children: React.ReactNode }) => ( -
{children}
- ), - AlertDialogActions: ({ children }: { children: React.ReactNode }) =>
{children}
, - AlertDialogCancelButton: ({ - children, - ...props - }: React.ButtonHTMLAttributes) => ( - - ), - AlertDialogConfirmButton: ({ - children, - ...props - }: React.ButtonHTMLAttributes & { loading?: boolean }) => ( - - ), - }) - - it('should reset delete input when dialog closes', async () => { - vi.resetModules() - vi.doMock('@langgenius/dify-ui/alert-dialog', createMockAlertDialogModule) - - const { AppCard: IsolatedAppCard } = await import('../app-card') - render() - - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) - fireEvent.click(await screen.findByRole('menuitem', { name: 'common.operation.delete' })) - fireEvent.change(await screen.findByRole('textbox'), { target: { value: 'partial name' } }) - - fireEvent.click(screen.getByTestId('force-close-dialog')) - expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument() - - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) - fireEvent.click(await screen.findByRole('menuitem', { name: 'common.operation.delete' })) - - expect(await screen.findByRole('textbox')).toHaveValue('') - - vi.doUnmock('@langgenius/dify-ui/alert-dialog') - }) - - it('should keep delete input when dialog remains open', async () => { - vi.resetModules() - vi.doMock('@langgenius/dify-ui/alert-dialog', createMockAlertDialogModule) - - const { AppCard: IsolatedAppCard } = await import('../app-card') - render() - - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) - fireEvent.click(await screen.findByRole('menuitem', { name: 'common.operation.delete' })) - fireEvent.change(await screen.findByRole('textbox'), { target: { value: 'partial name' } }) - - fireEvent.click(screen.getByTestId('keep-open-dialog')) - - expect(await screen.findByRole('alertdialog')).toBeInTheDocument() - expect(await screen.findByRole('textbox')).toHaveValue('partial name') - - vi.doUnmock('@langgenius/dify-ui/alert-dialog') - }) - - it('should keep delete dialog open when close is requested during deletion', async () => { - vi.resetModules() - mockDeleteMutationPending = true - vi.doMock('@langgenius/dify-ui/alert-dialog', createMockAlertDialogModule) - - const { AppCard: IsolatedAppCard } = await import('../app-card') - render() - - fireEvent.click(screen.getByTestId('dropdown-menu-trigger')) - fireEvent.click(await screen.findByRole('menuitem', { name: 'common.operation.delete' })) - expect(await screen.findByRole('alertdialog')).toBeInTheDocument() - - fireEvent.click(screen.getByTestId('force-close-dialog')) - - expect(await screen.findByRole('alertdialog')).toBeInTheDocument() - - vi.doUnmock('@langgenius/dify-ui/alert-dialog') - mockDeleteMutationPending = false - }) - }) }) diff --git a/web/app/components/base/chat/chat-with-history/__tests__/header-in-mobile.spec.tsx b/web/app/components/base/chat/chat-with-history/__tests__/header-in-mobile.spec.tsx index 843adb7f5d7..d0f33161106 100644 --- a/web/app/components/base/chat/chat-with-history/__tests__/header-in-mobile.spec.tsx +++ b/web/app/components/base/chat/chat-with-history/__tests__/header-in-mobile.spec.tsx @@ -37,23 +37,6 @@ vi.mock('@/next/navigation', () => ({ useParams: vi.fn(() => ({})), })) -vi.mock('@langgenius/dify-ui/dropdown-menu', () => import('@/__mocks__/base-ui-dropdown-menu')) -vi.mock('@langgenius/dify-ui/tooltip', () => import('@/__mocks__/base-ui-tooltip')) - -// Mock Dialog to avoid Base UI focus/portal behavior in tests -vi.mock('@langgenius/dify-ui/dialog', () => ({ - Dialog: ({ children, open }: { children: React.ReactNode; open?: boolean }) => { - if (!open) return null - return
{children}
- }, - DialogContent: ({ children }: { children: React.ReactNode }) => ( -
- {children} -
- ), - DialogTitle: ({ children }: { children: React.ReactNode }) =>
{children}
, -})) - // Sidebar mock removed to use real component const mockAppData: AppData = { diff --git a/web/app/components/base/chat/chat-with-history/header/__tests__/index.spec.tsx b/web/app/components/base/chat/chat-with-history/header/__tests__/index.spec.tsx index b1335a13981..9756de3530e 100644 --- a/web/app/components/base/chat/chat-with-history/header/__tests__/index.spec.tsx +++ b/web/app/components/base/chat/chat-with-history/header/__tests__/index.spec.tsx @@ -16,23 +16,6 @@ vi.mock('@/app/components/base/chat/chat-with-history/inputs-form/content', () = default: () =>
InputsFormContent
, })) -vi.mock('@langgenius/dify-ui/dropdown-menu', () => import('@/__mocks__/base-ui-dropdown-menu')) -vi.mock('@langgenius/dify-ui/tooltip', () => import('@/__mocks__/base-ui-tooltip')) - -// Mock Dialog to avoid Base UI focus/portal behavior in tests -vi.mock('@langgenius/dify-ui/dialog', () => ({ - Dialog: ({ children, open }: { children: React.ReactNode; open?: boolean }) => { - if (!open) return null - return
{children}
- }, - DialogContent: ({ children }: { children: React.ReactNode }) => ( -
- {children} -
- ), - DialogTitle: ({ children }: { children: React.ReactNode }) =>
{children}
, -})) - const mockAppData: AppData = { app_id: 'app-1', site: { diff --git a/web/app/components/base/chat/chat-with-history/sidebar/__tests__/index.spec.tsx b/web/app/components/base/chat/chat-with-history/sidebar/__tests__/index.spec.tsx index d4f909963e1..23e7430dcf2 100644 --- a/web/app/components/base/chat/chat-with-history/sidebar/__tests__/index.spec.tsx +++ b/web/app/components/base/chat/chat-with-history/sidebar/__tests__/index.spec.tsx @@ -85,17 +85,6 @@ vi.mock('@/next/navigation', () => ({ usePathname: () => '/test', })) -vi.mock('@langgenius/dify-ui/dialog', () => ({ - Dialog: ({ children, open }: { children: React.ReactNode; open?: boolean }) => - open === false ? null : <>{children}, - DialogContent: ({ children }: { children: React.ReactNode }) => ( -
{children}
- ), - DialogTitle: ({ children }: { children: React.ReactNode }) => ( -
{children}
- ), -})) - describe('Sidebar Index', () => { const mockContextValue = { isInstalledApp: false, diff --git a/web/app/components/base/chat/chat-with-history/sidebar/__tests__/rename-modal.spec.tsx b/web/app/components/base/chat/chat-with-history/sidebar/__tests__/rename-modal.spec.tsx index 3a9c3c97e67..aff1a1bc7bc 100644 --- a/web/app/components/base/chat/chat-with-history/sidebar/__tests__/rename-modal.spec.tsx +++ b/web/app/components/base/chat/chat-with-history/sidebar/__tests__/rename-modal.spec.tsx @@ -1,4 +1,3 @@ -import type { ReactNode } from 'react' import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import * as ReactI18next from 'react-i18next' @@ -6,13 +5,6 @@ import { expectLoadingButton } from '@/test/button' import { withSelectorKey } from '@/test/i18n-mock' import RenameModal from '../rename-modal' -vi.mock('@langgenius/dify-ui/dialog', () => ({ - Dialog: ({ children, open }: { children: ReactNode; open?: boolean }) => - open === false ? null : <>{children}, - DialogContent: ({ children }: { children: ReactNode }) =>
{children}
, - DialogTitle: ({ children }: { children: ReactNode }) =>

{children}

, -})) - describe('RenameModal', () => { const defaultProps = { isShow: true, diff --git a/web/app/components/base/chat/chat/answer/human-input-content/__tests__/field-renderer.spec.tsx b/web/app/components/base/chat/chat/answer/human-input-content/__tests__/field-renderer.spec.tsx index b450277debf..5e7a12920c2 100644 --- a/web/app/components/base/chat/chat/answer/human-input-content/__tests__/field-renderer.spec.tsx +++ b/web/app/components/base/chat/chat/answer/human-input-content/__tests__/field-renderer.spec.tsx @@ -5,80 +5,6 @@ import { InputVarType, SupportUploadFileTypes } from '@/app/components/workflow/ import { TransferMethod } from '@/types/app' import HumanInputFieldRenderer from '../field-renderer' -function MockTextarea({ - value, - onChange, - onValueChange, - ...props -}: { - value: string - onChange?: (event: { target: { value: string } }) => void - onValueChange?: (value: string) => void -} & React.TextareaHTMLAttributes) { - return ( -