mirror of
https://github.com/langgenius/dify.git
synced 2026-08-03 03:26:35 +08:00
test(web): remove Base UI component mocks (#39877)
This commit is contained in:
parent
02a9c51a3e
commit
802fe70423
@ -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
|
||||
|
||||
@ -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 (
|
||||
<div>
|
||||
<div data-testid="outside-area">outside</div>
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger
|
||||
render={
|
||||
useRenderElement ? (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="custom-trigger"
|
||||
onClick={(event) => {
|
||||
if (preventDefaultOnTrigger) event.preventDefault()
|
||||
}}
|
||||
>
|
||||
toggle
|
||||
</button>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
fallback trigger
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="custom-content"
|
||||
placement="bottom-start"
|
||||
sideOffset={4}
|
||||
alignOffset={8}
|
||||
positionerProps={
|
||||
{ 'data-positioner': 'true' } as unknown as React.HTMLAttributes<HTMLDivElement>
|
||||
}
|
||||
popupProps={{ 'data-popup': 'true' } as unknown as React.HTMLAttributes<HTMLDivElement>}
|
||||
>
|
||||
<div>popover body</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<div data-testid="open-state">{open ? 'open' : 'closed'}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
describe('base-ui-popover mock', () => {
|
||||
it('should toggle popover content from the fallback trigger and expose content props', () => {
|
||||
render(<PopoverHarness />)
|
||||
|
||||
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(<PopoverHarness useRenderElement />)
|
||||
|
||||
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(<PopoverHarness useRenderElement preventDefaultOnTrigger />)
|
||||
|
||||
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<HTMLElement>) => {
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
render(
|
||||
<div>
|
||||
<Popover open={false} onOpenChange={vi.fn()}>
|
||||
<PopoverTrigger onClick={handleClick}>fallback trigger</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<div>popover body</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('popover-trigger'))
|
||||
|
||||
expect(screen.queryByTestId('popover-content')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -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<HTMLElement> & {
|
||||
'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<HTMLDivElement> & {
|
||||
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 (
|
||||
<DropdownMenuContext.Provider value={{ open: resolvedOpen, onOpenChange: handleOpenChange }}>
|
||||
<div data-testid="dropdown-menu" data-open={String(resolvedOpen)}>
|
||||
{children}
|
||||
</div>
|
||||
</DropdownMenuContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const DropdownMenuTrigger = ({
|
||||
children,
|
||||
render,
|
||||
nativeButton: _nativeButton,
|
||||
onClick,
|
||||
disabled,
|
||||
...props
|
||||
}: DropdownMenuTriggerProps) => {
|
||||
const { open, onOpenChange } = React.useContext(DropdownMenuContext)
|
||||
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
|
||||
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<Record<string, unknown>>
|
||||
const childProps = (triggerElement.props ?? {}) as React.HTMLAttributes<HTMLElement> & {
|
||||
'data-testid'?: string
|
||||
}
|
||||
const triggerProps = props as React.HTMLAttributes<HTMLElement> & { '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<HTMLElement>) => {
|
||||
childProps.onClick?.(event)
|
||||
handleClick(event)
|
||||
},
|
||||
},
|
||||
render ? (children ?? childProps.children) : childProps.children,
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="dropdown-menu-trigger"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={handleClick}
|
||||
{...props}
|
||||
>
|
||||
{node}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const DropdownMenuContent = ({
|
||||
children,
|
||||
className,
|
||||
popupClassName,
|
||||
placement,
|
||||
sideOffset,
|
||||
alignOffset,
|
||||
...props
|
||||
}: DropdownMenuContentProps) => {
|
||||
const { open } = React.useContext(DropdownMenuContext)
|
||||
if (!open) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="dropdown-menu-content"
|
||||
data-placement={placement}
|
||||
data-side-offset={sideOffset}
|
||||
data-align-offset={alignOffset}
|
||||
className={className || popupClassName}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const DropdownMenuItem = ({
|
||||
children,
|
||||
onClick,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement> & { children?: ReactNode }) => (
|
||||
<div role="menuitem" onClick={onClick} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
export const DropdownMenuRadioGroup = ({
|
||||
children,
|
||||
onValueChange,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement> & {
|
||||
children?: ReactNode
|
||||
value?: unknown
|
||||
onValueChange?: (value: unknown) => void
|
||||
}) => (
|
||||
<div role="radiogroup" {...props} data-on-value-change={onValueChange ? 'true' : undefined}>
|
||||
{React.Children.map(children, (child) => {
|
||||
if (!React.isValidElement(child)) return child
|
||||
return React.cloneElement(
|
||||
child as React.ReactElement<{ __onValueChange?: (value: unknown) => void }>,
|
||||
{ __onValueChange: onValueChange },
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
|
||||
export const DropdownMenuRadioItem = ({
|
||||
children,
|
||||
value,
|
||||
onClick,
|
||||
__onValueChange,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement> & {
|
||||
children?: ReactNode
|
||||
value?: unknown
|
||||
__onValueChange?: (value: unknown) => void
|
||||
}) => (
|
||||
<div
|
||||
role="radio"
|
||||
onClick={(event) => {
|
||||
onClick?.(event)
|
||||
__onValueChange?.(value)
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
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<HTMLDivElement>) => (
|
||||
<div role="separator" {...props} />
|
||||
)
|
||||
export const DropdownMenuSub = ({ children }: { children?: ReactNode }) => <>{children}</>
|
||||
export const DropdownMenuSubTrigger = DropdownMenuItem
|
||||
export const DropdownMenuSubContent = ({ children }: { children?: ReactNode }) => <>{children}</>
|
||||
@ -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<HTMLElement> & {
|
||||
'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<HTMLDivElement> & {
|
||||
children?: ReactNode
|
||||
placement?: string
|
||||
sideOffset?: number
|
||||
alignOffset?: number
|
||||
popupClassName?: string
|
||||
positionerProps?: React.HTMLAttributes<HTMLDivElement>
|
||||
popupProps?: React.HTMLAttributes<HTMLDivElement>
|
||||
}
|
||||
|
||||
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 (
|
||||
<PopoverContext.Provider
|
||||
value={{
|
||||
open: resolvedOpen,
|
||||
onOpenChange: handleOpenChange,
|
||||
}}
|
||||
>
|
||||
<div data-testid="popover" data-open={String(resolvedOpen)}>
|
||||
{children}
|
||||
</div>
|
||||
</PopoverContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
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<HTMLElement>) => {
|
||||
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<Record<string, unknown>>
|
||||
const childProps = (triggerElement.props ?? {}) as React.HTMLAttributes<HTMLElement> & {
|
||||
'data-testid'?: string
|
||||
}
|
||||
const triggerProps = props as React.HTMLAttributes<HTMLElement> & { '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<HTMLElement>) => {
|
||||
childProps.onClick?.(event)
|
||||
onClick?.(event)
|
||||
if (event.defaultPrevented) return
|
||||
onOpenChange(!open)
|
||||
},
|
||||
},
|
||||
render ? (children ?? childProps.children) : childProps.children,
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="popover-trigger"
|
||||
data-popover-trigger="true"
|
||||
data-popup-open={open ? '' : undefined}
|
||||
onClick={(event) => {
|
||||
onClick?.(event)
|
||||
if (event.defaultPrevented) return
|
||||
onOpenChange(!open)
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{node}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const PopoverContent = ({
|
||||
children,
|
||||
className,
|
||||
placement,
|
||||
sideOffset,
|
||||
alignOffset,
|
||||
popupClassName,
|
||||
positionerProps,
|
||||
popupProps,
|
||||
...props
|
||||
}: PopoverContentProps) => {
|
||||
const { open } = React.useContext(PopoverContext)
|
||||
|
||||
if (!open) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="popover-content"
|
||||
data-popover-content="true"
|
||||
data-placement={placement}
|
||||
data-side-offset={sideOffset}
|
||||
data-align-offset={alignOffset}
|
||||
className={className || popupClassName}
|
||||
{...positionerProps}
|
||||
{...popupProps}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const PopoverClose = ({ children }: { children?: ReactNode }) => <>{children}</>
|
||||
export const PopoverTitle = ({ children }: { children?: ReactNode }) => <>{children}</>
|
||||
export const PopoverDescription = ({ children }: { children?: ReactNode }) => <>{children}</>
|
||||
@ -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) => (
|
||||
<SelectContext.Provider value={{ value, onValueChange: onValueChange ?? (() => {}) }}>
|
||||
<div data-testid="select-root">{children}</div>
|
||||
</SelectContext.Provider>
|
||||
)
|
||||
|
||||
export const SelectTrigger = ({
|
||||
children,
|
||||
...props
|
||||
}: React.ButtonHTMLAttributes<HTMLButtonElement> & { children?: ReactNode }) => (
|
||||
<button type="button" role="combobox" {...props}>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
|
||||
export const SelectValue = ({ placeholder }: { placeholder?: ReactNode }) => <>{placeholder}</>
|
||||
|
||||
export const SelectContent = ({
|
||||
children,
|
||||
popupClassName,
|
||||
}: {
|
||||
children?: ReactNode
|
||||
popupClassName?: string
|
||||
}) => (
|
||||
<div data-side="bottom" data-testid="select-content" className={popupClassName}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
export const SelectItem = ({
|
||||
children,
|
||||
value,
|
||||
onClick,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement> & { children?: ReactNode; value?: unknown }) => {
|
||||
const select = React.useContext(SelectContext)
|
||||
return (
|
||||
<div
|
||||
role="option"
|
||||
onClick={(event) => {
|
||||
onClick?.(event)
|
||||
select.onValueChange(value)
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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<HTMLDivElement>) => (
|
||||
<div role="separator" {...props} />
|
||||
)
|
||||
@ -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 (
|
||||
<TooltipContext.Provider value={{ open: resolvedOpen, onOpenChange: handleOpenChange }}>
|
||||
{children}
|
||||
</TooltipContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const TooltipTrigger = ({
|
||||
children,
|
||||
render,
|
||||
nativeButton: _nativeButton,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLElement> & {
|
||||
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<Record<string, unknown>>
|
||||
const childProps = (triggerElement.props ?? {}) as React.HTMLAttributes<HTMLElement>
|
||||
|
||||
return React.cloneElement(triggerElement, {
|
||||
...props,
|
||||
...childProps,
|
||||
onMouseEnter: (event: React.MouseEvent<HTMLElement>) => {
|
||||
childProps.onMouseEnter?.(event)
|
||||
props.onMouseEnter?.(event)
|
||||
onOpenChange(true)
|
||||
},
|
||||
onMouseLeave: (event: React.MouseEvent<HTMLElement>) => {
|
||||
childProps.onMouseLeave?.(event)
|
||||
props.onMouseLeave?.(event)
|
||||
onOpenChange(false)
|
||||
},
|
||||
onClick: (event: React.MouseEvent<HTMLElement>) => {
|
||||
childProps.onClick?.(event)
|
||||
props.onClick?.(event)
|
||||
onOpenChange(!open)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
{...props}
|
||||
onMouseEnter={(event) => {
|
||||
props.onMouseEnter?.(event)
|
||||
onOpenChange(true)
|
||||
}}
|
||||
onMouseLeave={(event) => {
|
||||
props.onMouseLeave?.(event)
|
||||
onOpenChange(false)
|
||||
}}
|
||||
onClick={(event) => {
|
||||
props.onClick?.(event)
|
||||
onOpenChange(!open)
|
||||
}}
|
||||
>
|
||||
{node}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export const TooltipContent = ({
|
||||
children,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement> & { children?: ReactNode }) => {
|
||||
const { open } = React.useContext(TooltipContext)
|
||||
if (!open) return null
|
||||
return <div {...props}>{children}</div>
|
||||
}
|
||||
|
||||
export const TooltipProvider = ({ children }: { children?: ReactNode }) => <>{children}</>
|
||||
37
web/__tests__/interactive-component-mock-boundary.spec.ts
Normal file
37
web/__tests__/interactive-component-mock-boundary.spec.ts
Normal file
@ -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([])
|
||||
})
|
||||
})
|
||||
@ -58,32 +58,6 @@ vi.mock('@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view',
|
||||
default: ({ appId }: { appId: string }) => <div data-testid="card-view" data-app-id={appId} />,
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/button', () => ({
|
||||
Button: ({
|
||||
children,
|
||||
onClick,
|
||||
className,
|
||||
size,
|
||||
variant,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
onClick?: () => void
|
||||
className?: string
|
||||
size?: string
|
||||
variant?: string
|
||||
}) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={className}
|
||||
data-size={size}
|
||||
data-variant={variant}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../app-operations', () => ({
|
||||
default: ({
|
||||
primaryOperations,
|
||||
|
||||
@ -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 <div>{children}</div>
|
||||
},
|
||||
DropdownMenuTrigger: ({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}) => (
|
||||
<button
|
||||
type="button"
|
||||
className={className}
|
||||
onClick={() => mockDropdownOnOpenChange?.(!mockDropdownOpen)}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
DropdownMenuContent: ({ children }: { children: React.ReactNode }) =>
|
||||
mockDropdownOpen ? <div>{children}</div> : null,
|
||||
DropdownMenuItem: ({
|
||||
children,
|
||||
onClick,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
onClick?: () => void
|
||||
}) => (
|
||||
<button type="button" onClick={onClick}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
DropdownMenuSeparator: () => <hr />,
|
||||
}))
|
||||
|
||||
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.
|
||||
|
||||
@ -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<string, any>) => {
|
||||
sectionProps.summary = props
|
||||
|
||||
@ -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<typeof import('react')>('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
|
||||
}) => (
|
||||
<OpenContext.Provider value={{ open, setOpen: onOpenChange ?? vi.fn() }}>
|
||||
<div data-testid="portal-root">{children}</div>
|
||||
</OpenContext.Provider>
|
||||
),
|
||||
DropdownMenuTrigger: ({
|
||||
children,
|
||||
render,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
render?: React.ReactElement
|
||||
}) => {
|
||||
const { open, setOpen } = useOpenContext()
|
||||
|
||||
if (render) {
|
||||
return ReactModule.cloneElement(
|
||||
render,
|
||||
{
|
||||
onClick: () => setOpen(!open),
|
||||
} as Record<string, unknown>,
|
||||
children,
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<button type="button" onClick={() => setOpen(!open)}>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
},
|
||||
DropdownMenuContent: ({
|
||||
children,
|
||||
popupClassName,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
popupClassName?: string
|
||||
}) => {
|
||||
const context = useOpenContext()
|
||||
return context.open ? <div className={popupClassName}>{children}</div> : null
|
||||
},
|
||||
DropdownMenuItem: ({
|
||||
children,
|
||||
onClick,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
onClick?: React.MouseEventHandler<HTMLButtonElement>
|
||||
}) => {
|
||||
const { setOpen } = useOpenContext()
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(event) => {
|
||||
onClick?.(event)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
describe('PublishWithMultipleModel', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
@ -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 }) => <div>{children}</div>,
|
||||
DialogTitle: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
}))
|
||||
|
||||
describe('Conversation history edit modal', () => {
|
||||
const data: ConversationHistoriesRole = {
|
||||
user_prefix: 'user',
|
||||
|
||||
@ -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<typeof import('@langgenius/dify-ui/select')>()
|
||||
|
||||
return {
|
||||
...actual,
|
||||
Select: ({
|
||||
value,
|
||||
onValueChange,
|
||||
children,
|
||||
}: {
|
||||
value: string
|
||||
onValueChange: (value: string) => void
|
||||
children: ReactNode
|
||||
}) => (
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onValueChange(value === 'true' ? 'false' : 'beta')}
|
||||
>{`ui-select:${value}`}</button>
|
||||
<button type="button" onClick={() => onValueChange('__empty__')}>
|
||||
ui-select-empty
|
||||
</button>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
SelectTrigger: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
SelectValue: () => <span>select-value</span>,
|
||||
SelectContent: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
SelectItem: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
SelectItemText: ({ children }: { children: ReactNode }) => <span>{children}</span>,
|
||||
SelectItemIndicator: () => <span data-testid="select-item-indicator" />,
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@langgenius/dify-ui/tooltip', () => ({
|
||||
Tooltip: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
TooltipTrigger: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
TooltipContent: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
}))
|
||||
|
||||
vi.mock('../field', () => ({
|
||||
default: ({ children, title }: { children: ReactNode; title: string }) => (
|
||||
<div>
|
||||
@ -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(<ConfigModalFormFields {...checkboxProps} />)
|
||||
fireEvent.click(screen.getByText('ui-select:true'))
|
||||
const checkboxView = render(<ConfigModalFormFields {...checkboxProps} />)
|
||||
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(<ConfigModalFormFields {...selectProps} />)
|
||||
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(<ConfigModalFormFields {...selectProps} />)
|
||||
const selectView = render(<ConfigModalFormFields {...selectProps} />)
|
||||
|
||||
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(<ConfigModalFormFields {...selectWithoutOptionsProps} />)
|
||||
|
||||
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(<ConfigModalFormFields {...selectProps} />)
|
||||
|
||||
expect(screen.getByText('ui-select:__empty__')).toBeInTheDocument()
|
||||
|
||||
it('should preserve existing file defaults when present', () => {
|
||||
const existingFile = {
|
||||
fileId: 'existing-file',
|
||||
type: 'local_file',
|
||||
|
||||
@ -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 }) => <span>{type}</span>,
|
||||
}))
|
||||
|
||||
@ -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
|
||||
}) => (
|
||||
<input
|
||||
type="range"
|
||||
className={`slider ${props.className ?? ''}`}
|
||||
min={props.min}
|
||||
max={props.max}
|
||||
value={props.value}
|
||||
onChange={(e) => props.onValueChange(Number(e.target.value))}
|
||||
/>
|
||||
),
|
||||
}))
|
||||
|
||||
const basePayload = {
|
||||
enabled: true,
|
||||
strategy: 'react',
|
||||
|
||||
@ -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
|
||||
}) => <PopoverContext value={{ open: !!open, onOpenChange }}>{children}</PopoverContext>,
|
||||
PopoverTrigger: ({ render }: { render?: ReactNode }) => {
|
||||
const { open, onOpenChange } = React.use(PopoverContext)
|
||||
return (
|
||||
<button type="button" aria-label="choose variable" onClick={() => onOpenChange?.(!open)}>
|
||||
{render}
|
||||
</button>
|
||||
)
|
||||
},
|
||||
PopoverContent: ({ children }: { children: ReactNode }) => {
|
||||
const { open } = React.use(PopoverContext)
|
||||
return open ? (
|
||||
<div role="dialog" aria-label="variable options">
|
||||
{children}
|
||||
</div>
|
||||
) : 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(<VarPicker value="var1" options={options} onChange={vi.fn()} />)
|
||||
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(<VarPicker value="var1" options={options} onChange={onChange} />)
|
||||
|
||||
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(<VarPicker value={undefined} options={[]} onChange={vi.fn()} />)
|
||||
await userEvent.click(screen.getByRole('button', { name: 'choose variable' }))
|
||||
await user.click(screen.getByRole('button'))
|
||||
expect(screen.getByText('appDebug.feature.dataSet.queryVariable.noVar')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -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
|
||||
}) => (
|
||||
<SelectContext.Provider value={{ disabled, onValueChange, value }}>
|
||||
<div>{children}</div>
|
||||
</SelectContext.Provider>
|
||||
),
|
||||
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 (
|
||||
<div>
|
||||
<button
|
||||
data-testid="select-input"
|
||||
type="button"
|
||||
disabled={context.disabled}
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
<button
|
||||
data-testid="select-empty"
|
||||
type="button"
|
||||
onClick={() => context.onValueChange?.('')}
|
||||
>
|
||||
empty select value
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
SelectContent: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
SelectItem: ({ children, value }: { children: React.ReactNode; value: string }) => {
|
||||
const context = React.useContext(SelectContext)
|
||||
return (
|
||||
<button
|
||||
data-testid={`select-${value}`}
|
||||
type="button"
|
||||
role="option"
|
||||
onClick={() => context.onValueChange?.(value)}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
},
|
||||
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(<ChatUserInput inputs={{}} />)
|
||||
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(<ChatUserInput inputs={{}} />)
|
||||
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(<ChatUserInput inputs={{ choice: 'A' }} />)
|
||||
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(<ChatUserInput inputs={{}} />)
|
||||
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(<ChatUserInput inputs={{}} />)
|
||||
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(<ChatUserInput inputs={{}} />)
|
||||
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(<ChatUserInput inputs={{}} />)
|
||||
const select = screen.getByTestId('select-input')
|
||||
expect(select).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should preserve other input values when updating one field', () => {
|
||||
mockUseContext.mockReturnValue(
|
||||
createContextValue({
|
||||
|
||||
@ -102,10 +102,6 @@ vi.mock('@/app/components/base/chat/chat', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/avatar', () => ({
|
||||
Avatar: ({ name }: { name: string }) => <div data-testid="avatar">{name}</div>,
|
||||
}))
|
||||
|
||||
const createModelAndParameter = (
|
||||
overrides: Partial<ModelAndParameter> = {},
|
||||
): ModelAndParameter => ({
|
||||
|
||||
@ -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
|
||||
}) => (
|
||||
<button
|
||||
type="button"
|
||||
data-disabled={disabled ? 'true' : 'false'}
|
||||
className={className}
|
||||
onClick={() => onClick?.()}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
|
||||
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
|
||||
}) => (
|
||||
<SelectContext.Provider value={{ onValueChange, value }}>
|
||||
<div>{children}</div>
|
||||
</SelectContext.Provider>
|
||||
),
|
||||
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 (
|
||||
<div>
|
||||
<button type="button">{children}</button>
|
||||
<button
|
||||
data-testid="select-empty"
|
||||
type="button"
|
||||
onClick={() => context.onValueChange?.('')}
|
||||
>
|
||||
empty select value
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
SelectContent: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
SelectItem: ({ children, value }: { children: React.ReactNode; value: string }) => {
|
||||
const context = React.useContext(SelectContext)
|
||||
return (
|
||||
<button type="button" onClick={() => context.onValueChange?.(value)}>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
},
|
||||
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 }) => (
|
||||
<button type="button" data-testid={`bool-input-${name}`} onClick={() => onChange(true)}>
|
||||
@ -221,7 +145,7 @@ describe('PromptValuePanel', () => {
|
||||
})
|
||||
|
||||
const runButton = screen.getByRole('button', { name: 'appDebug.inputs.run' })
|
||||
expect(runButton).toHaveAttribute('data-disabled', 'false')
|
||||
expect(runButton).toBeEnabled()
|
||||
fireEvent.click(runButton)
|
||||
await waitFor(() => expect(mockOnSend).toHaveBeenCalledTimes(1))
|
||||
})
|
||||
@ -237,22 +161,7 @@ describe('PromptValuePanel', () => {
|
||||
})
|
||||
|
||||
const runButton = screen.getByRole('button', { name: 'appDebug.inputs.run' })
|
||||
expect(runButton).toHaveAttribute('data-disabled', 'true')
|
||||
})
|
||||
|
||||
it('invokes the tooltip-branch run handler when the click callback is triggered', () => {
|
||||
renderPanel({
|
||||
context: {
|
||||
mode: AppModeEnum.CHAT,
|
||||
},
|
||||
props: {
|
||||
appType: AppModeEnum.CHAT,
|
||||
},
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'appDebug.inputs.run' }))
|
||||
|
||||
expect(mockOnSend).toHaveBeenCalledTimes(1)
|
||||
expect(runButton).toBeDisabled()
|
||||
})
|
||||
|
||||
it('hydrates default values, supports advanced prompt gating, and toggles the feature panel', () => {
|
||||
@ -282,10 +191,7 @@ describe('PromptValuePanel', () => {
|
||||
})
|
||||
|
||||
expect(mockSetInputs).toHaveBeenCalledWith({ textVar: 'default text' })
|
||||
expect(screen.getByRole('button', { name: 'appDebug.inputs.run' })).toHaveAttribute(
|
||||
'data-disabled',
|
||||
'true',
|
||||
)
|
||||
expect(screen.getByRole('button', { name: 'appDebug.inputs.run' })).toBeDisabled()
|
||||
|
||||
fireEvent.click(screen.getByText('feature bar'))
|
||||
expect(mockSetShowAppConfigureFeaturesModal).toHaveBeenCalled()
|
||||
@ -309,13 +215,11 @@ describe('PromptValuePanel', () => {
|
||||
},
|
||||
})
|
||||
|
||||
expect(screen.getByRole('button', { name: 'appDebug.inputs.run' })).toHaveAttribute(
|
||||
'data-disabled',
|
||||
'true',
|
||||
)
|
||||
expect(screen.getByRole('button', { name: 'appDebug.inputs.run' })).toBeDisabled()
|
||||
})
|
||||
|
||||
it('renders paragraph, select, number, checkbox, and vision inputs', () => {
|
||||
it('renders paragraph, select, number, checkbox, and vision inputs', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onVisionFilesChange = vi.fn()
|
||||
renderPanel({
|
||||
context: {
|
||||
@ -357,7 +261,8 @@ describe('PromptValuePanel', () => {
|
||||
fireEvent.change(screen.getByPlaceholderText('Paragraph Var'), {
|
||||
target: { value: 'updated paragraph' },
|
||||
})
|
||||
fireEvent.click(screen.getByText('b'))
|
||||
await user.click(screen.getByRole('combobox'))
|
||||
await user.click(await screen.findByRole('option', { name: 'b' }))
|
||||
fireEvent.change(screen.getByDisplayValue('1'), { target: { value: '2' } })
|
||||
fireEvent.click(screen.getByText('bool-input'))
|
||||
fireEvent.click(screen.getByText('image-uploader'))
|
||||
@ -378,36 +283,6 @@ describe('PromptValuePanel', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('ignores empty select values when choosing prompt options', () => {
|
||||
renderPanel({
|
||||
context: {
|
||||
modelConfig: {
|
||||
configs: {
|
||||
prompt_template: 'prompt template',
|
||||
prompt_variables: [
|
||||
{
|
||||
key: 'selectVar',
|
||||
name: 'Select Var',
|
||||
type: 'select',
|
||||
options: ['a', 'b'],
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
props: {
|
||||
inputs: {
|
||||
selectVar: 'a',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByTestId('select-empty'))
|
||||
|
||||
expect(mockSetInputs).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('ignores updates when the rendered field is not tracked in the prompt variable lookup', () => {
|
||||
const filteredPromptVariables = {
|
||||
length: 1,
|
||||
@ -508,14 +383,8 @@ describe('PromptValuePanel', () => {
|
||||
},
|
||||
})
|
||||
|
||||
expect(screen.getByRole('button', { name: 'common.operation.clear' })).toHaveAttribute(
|
||||
'data-disabled',
|
||||
'false',
|
||||
)
|
||||
expect(screen.getByRole('button', { name: 'appDebug.inputs.run' })).toHaveAttribute(
|
||||
'data-disabled',
|
||||
'false',
|
||||
)
|
||||
expect(screen.getByRole('button', { name: 'common.operation.clear' })).toBeEnabled()
|
||||
expect(screen.getByRole('button', { name: 'appDebug.inputs.run' })).toBeEnabled()
|
||||
expect(screen.getByRole('button', { name: 'feature bar' })).toBeDisabled()
|
||||
expect(screen.getByRole('button', { name: 'feature bar' })).toHaveAttribute(
|
||||
'data-hide-edit-entrance',
|
||||
@ -532,14 +401,8 @@ describe('PromptValuePanel', () => {
|
||||
})
|
||||
|
||||
expect(screen.getByPlaceholderText('Text Var')).toHaveAttribute('readonly')
|
||||
expect(screen.getByRole('button', { name: 'common.operation.clear' })).toHaveAttribute(
|
||||
'data-disabled',
|
||||
'true',
|
||||
)
|
||||
expect(screen.getByRole('button', { name: 'appDebug.inputs.run' })).toHaveAttribute(
|
||||
'data-disabled',
|
||||
'true',
|
||||
)
|
||||
expect(screen.getByRole('button', { name: 'common.operation.clear' })).toBeDisabled()
|
||||
expect(screen.getByRole('button', { name: 'appDebug.inputs.run' })).toBeDisabled()
|
||||
})
|
||||
|
||||
it('marks debug inputs and actions as disabled when configuration is readonly and test/run permission is missing', () => {
|
||||
@ -551,14 +414,8 @@ describe('PromptValuePanel', () => {
|
||||
})
|
||||
|
||||
expect(screen.getByPlaceholderText('Text Var')).toHaveAttribute('readonly')
|
||||
expect(screen.getByRole('button', { name: 'common.operation.clear' })).toHaveAttribute(
|
||||
'data-disabled',
|
||||
'true',
|
||||
)
|
||||
expect(screen.getByRole('button', { name: 'appDebug.inputs.run' })).toHaveAttribute(
|
||||
'data-disabled',
|
||||
'true',
|
||||
)
|
||||
expect(screen.getByRole('button', { name: 'common.operation.clear' })).toBeDisabled()
|
||||
expect(screen.getByRole('button', { name: 'appDebug.inputs.run' })).toBeDisabled()
|
||||
})
|
||||
|
||||
it('collapses the user input panel and hides the clear and run actions', () => {
|
||||
|
||||
@ -87,14 +87,6 @@ vi.mock('@/app/components/base/loading', () => ({
|
||||
default: () => <div>loading-logs</div>,
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/pagination', () => ({
|
||||
Pagination: ({ onPageChange }: { onPageChange: (page: number) => void }) => (
|
||||
<div>
|
||||
<button onClick={() => onPageChange(2)}>go-to-page-2</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
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 })
|
||||
})
|
||||
|
||||
@ -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
|
||||
}) => (
|
||||
<PopoverContext.Provider value={{ open, onOpenChange }}>
|
||||
<div data-testid="popover-root" data-open={open ? 'true' : 'false'}>
|
||||
{children}
|
||||
</div>
|
||||
</PopoverContext.Provider>
|
||||
),
|
||||
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 (
|
||||
<button type="button" data-testid="popover-trigger" onClick={handleClick}>
|
||||
{content}
|
||||
</button>
|
||||
)
|
||||
},
|
||||
PopoverContent: ({ children }: { children: React.ReactNode }) => {
|
||||
const context = React.useContext(PopoverContext)
|
||||
if (!context?.open) return null
|
||||
|
||||
return <div data-testid="popover-content">{children}</div>
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
describe('ModelInfo', () => {
|
||||
const defaultModel = {
|
||||
name: 'gpt-4',
|
||||
@ -132,32 +77,32 @@ describe('ModelInfo', () => {
|
||||
it('should be closed by default', () => {
|
||||
render(<ModelInfo model={defaultModel} />)
|
||||
|
||||
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(<ModelInfo model={defaultModel} />)
|
||||
|
||||
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(<ModelInfo model={defaultModel} />)
|
||||
|
||||
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')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -48,9 +48,23 @@ vi.mock('@/service/use-triggers', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/utils', () => ({
|
||||
asyncRunSafe: async <T,>(promise: Promise<T>) => {
|
||||
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 }) => (
|
||||
<div data-testid="block-icon" data-type={type}>
|
||||
@ -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
|
||||
}) => (
|
||||
<button
|
||||
data-testid="switch"
|
||||
data-checked={checked ? 'true' : 'false'}
|
||||
data-disabled={disabled ? 'true' : 'false'}
|
||||
disabled={disabled}
|
||||
onClick={() => !disabled && onCheckedChange(!checked)}
|
||||
>
|
||||
Switch
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
|
||||
describe('TriggerCard', () => {
|
||||
const mockAppInfo = {
|
||||
id: 'test-app-id',
|
||||
@ -210,7 +202,7 @@ describe('TriggerCard', () => {
|
||||
it('should render switches for each trigger', () => {
|
||||
render(<TriggerCard appInfo={mockAppInfo} onToggleResult={mockOnToggleResult} />)
|
||||
|
||||
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(<TriggerCard appInfo={mockAppInfo} onToggleResult={mockOnToggleResult} />)
|
||||
|
||||
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(<TriggerCard appInfo={mockAppInfo} onToggleResult={mockOnToggleResult} />)
|
||||
|
||||
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(<TriggerCard appInfo={mockAppInfo} onToggleResult={mockOnToggleResult} />)
|
||||
|
||||
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(<TriggerCard appInfo={mockAppInfo} onToggleResult={mockOnToggleResult} />)
|
||||
|
||||
const switchBtn = screen.getByTestId('switch')
|
||||
const switchBtn = screen.getByRole('switch')
|
||||
fireEvent.click(switchBtn)
|
||||
|
||||
await waitFor(() => {
|
||||
@ -282,7 +274,7 @@ describe('TriggerCard', () => {
|
||||
|
||||
render(<TriggerCard appInfo={mockAppInfo} onToggleResult={mockOnToggleResult} />)
|
||||
|
||||
const switchBtn = screen.getByTestId('switch')
|
||||
const switchBtn = screen.getByRole('switch')
|
||||
fireEvent.click(switchBtn)
|
||||
|
||||
await waitFor(() => {
|
||||
@ -360,7 +352,7 @@ describe('TriggerCard', () => {
|
||||
|
||||
render(<TriggerCard appInfo={mockAppInfo} onToggleResult={mockOnToggleResult} />)
|
||||
|
||||
const switchBtn = screen.getByTestId('switch')
|
||||
const switchBtn = screen.getByRole('switch')
|
||||
expect(switchBtn).toBeInTheDocument()
|
||||
})
|
||||
|
||||
@ -383,8 +375,8 @@ describe('TriggerCard', () => {
|
||||
<TriggerCard appInfo={appInfoWithoutEditPermission} onToggleResult={mockOnToggleResult} />,
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@ -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<DropdownMenuContextValue | null>(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
|
||||
}) => (
|
||||
<DropdownMenuContext value={{ isOpen: open, setOpen: onOpenChange ?? vi.fn() }}>
|
||||
<div data-testid="dropdown-menu" data-open={open} data-modal={modal}>
|
||||
{children}
|
||||
</div>
|
||||
</DropdownMenuContext>
|
||||
),
|
||||
DropdownMenuTrigger: ({
|
||||
children,
|
||||
className,
|
||||
onClick,
|
||||
...props
|
||||
}: React.ButtonHTMLAttributes<HTMLButtonElement>) => {
|
||||
const { isOpen, setOpen } = useDropdownMenuContext()
|
||||
return (
|
||||
<button
|
||||
data-testid="dropdown-menu-trigger"
|
||||
type="button"
|
||||
className={className}
|
||||
onClick={(e) => {
|
||||
onClick?.(e)
|
||||
setOpen(!isOpen)
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
},
|
||||
DropdownMenuContent: ({
|
||||
children,
|
||||
className,
|
||||
popupClassName,
|
||||
popupProps,
|
||||
positionerProps,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
popupClassName?: string
|
||||
popupProps?: React.HTMLAttributes<HTMLDivElement>
|
||||
positionerProps?: React.HTMLAttributes<HTMLDivElement>
|
||||
}) => {
|
||||
const { isOpen } = useDropdownMenuContext()
|
||||
if (!isOpen) return null
|
||||
|
||||
return (
|
||||
<div data-testid="dropdown-menu-positioner" {...positionerProps}>
|
||||
<div
|
||||
data-testid="dropdown-menu-content"
|
||||
role="menu"
|
||||
className={[className, popupClassName].filter(Boolean).join(' ')}
|
||||
{...popupProps}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
DropdownMenuItem: ({
|
||||
children,
|
||||
className,
|
||||
onClick,
|
||||
destructive,
|
||||
disabled,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
onClick?: React.MouseEventHandler<HTMLButtonElement>
|
||||
destructive?: boolean
|
||||
disabled?: boolean
|
||||
}) => {
|
||||
const { setOpen } = useDropdownMenuContext()
|
||||
return (
|
||||
<button
|
||||
data-testid="dropdown-menu-item"
|
||||
role="menuitem"
|
||||
type="button"
|
||||
className={className}
|
||||
data-destructive={destructive}
|
||||
disabled={disabled}
|
||||
onClick={(e) => {
|
||||
onClick?.(e)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
},
|
||||
DropdownMenuSeparator: () => <hr data-testid="dropdown-menu-separator" />,
|
||||
}
|
||||
})
|
||||
|
||||
// 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(<AppCard app={mockApp} />)
|
||||
expect(screen.getByTestId('dropdown-menu')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render dropdown menu as non-modal', () => {
|
||||
render(<AppCard app={mockApp} />)
|
||||
expect(screen.getByTestId('dropdown-menu')).toHaveAttribute('data-modal', 'false')
|
||||
})
|
||||
|
||||
it('should reveal operations trigger when card receives keyboard focus', () => {
|
||||
render(<AppCard app={mockApp} />)
|
||||
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(<AppCard app={mockApp} />)
|
||||
|
||||
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(<AppCard app={mockApp} />)
|
||||
|
||||
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(<AppCard app={appWithoutImportExportPermission} />)
|
||||
|
||||
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(<StarredAppCard app={appWithoutImportExportPermission} />)
|
||||
|
||||
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(<AppCard app={mockApp} />)
|
||||
|
||||
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(<AppCard app={mockApp} />)
|
||||
|
||||
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(<AppCard app={chatApp} />)
|
||||
|
||||
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(<AppCard app={editableChatApp} />)
|
||||
|
||||
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(<AppCard app={completionApp} />)
|
||||
|
||||
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(<AppCard app={workflowApp} />)
|
||||
|
||||
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(<AppCard app={mockApp} />)
|
||||
|
||||
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(<AppCard app={mockApp} />)
|
||||
|
||||
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(<AppCard app={mockApp} />)
|
||||
|
||||
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(<AppCard app={mockApp} />)
|
||||
|
||||
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(<AppCard app={mockApp} />)
|
||||
|
||||
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(<AppCard app={mockApp} />)
|
||||
|
||||
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(<AppCard app={mockApp} />)
|
||||
|
||||
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(<AppCard app={mockApp} />)
|
||||
|
||||
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(<AppCard app={mockApp} onRefresh={mockOnRefresh} />)
|
||||
|
||||
// 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(<AppCard app={mockApp} onRefresh={mockOnRefresh} />)
|
||||
|
||||
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(<AppCard app={mockApp} onRefresh={mockOnRefresh} />)
|
||||
|
||||
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(<AppCard app={mockApp} />)
|
||||
|
||||
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(<AppCard app={mockApp} onRefresh={mockOnRefresh} />)
|
||||
|
||||
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(<AppCard app={mockApp} />)
|
||||
|
||||
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(<AppCard app={mockApp} onRefresh={mockOnRefresh} />)
|
||||
|
||||
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(<AppCard app={mockApp} onRefresh={mockOnRefresh} />)
|
||||
|
||||
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(<AppCard app={mockApp} />)
|
||||
|
||||
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(<AppCard app={mockApp} onRefresh={mockOnRefresh} />)
|
||||
|
||||
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(<AppCard app={mockApp} />)
|
||||
|
||||
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(<AppCard app={chatApp} />)
|
||||
|
||||
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(<AppCard app={chatApp} />)
|
||||
|
||||
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(<AppCard app={chatApp} onRefresh={mockOnRefresh} />)
|
||||
|
||||
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(<AppCard app={chatApp} />)
|
||||
|
||||
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(<AppCard app={completionApp} />)
|
||||
|
||||
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(<AppCard app={mockApp} />)
|
||||
|
||||
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(<AppCard app={workflowApp} />)
|
||||
|
||||
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(<AppCard app={workflowApp} />)
|
||||
|
||||
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(<AppCard app={workflowApp} />)
|
||||
|
||||
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(<AppCard app={advancedChatApp} />)
|
||||
|
||||
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(<AppCard app={workflowApp} />)
|
||||
|
||||
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(<AppCard app={mockApp} onRefresh={mockOnRefresh} />)
|
||||
|
||||
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(<AppCard app={mockApp} />)
|
||||
|
||||
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(<AppCard app={mockApp} onRefresh={mockOnRefresh} />)
|
||||
|
||||
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(<AppCard app={chatApp} onRefresh={mockOnRefresh} />)
|
||||
|
||||
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(<AppCard app={completionApp} />)
|
||||
|
||||
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(<AppCard app={workflowApp} />)
|
||||
|
||||
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(<AppCard app={mockApp} />)
|
||||
|
||||
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(<AppCard app={mockApp} />)
|
||||
|
||||
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(<AppCard app={mockApp} />)
|
||||
|
||||
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(<AppCard app={mockApp} />)
|
||||
|
||||
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(<AppCard app={mockApp} />)
|
||||
|
||||
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(<AppCard app={mockApp} />)
|
||||
|
||||
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(<AppCard app={workflowApp} />)
|
||||
|
||||
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(<AppCard app={mockApp} />)
|
||||
|
||||
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(<AppCard app={mockApp} />)
|
||||
|
||||
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(<AppCard app={draftTriggerApp} />)
|
||||
|
||||
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(<AppCard app={mockApp} />)
|
||||
|
||||
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(<AppCard app={appWithReleasePermission} />)
|
||||
|
||||
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(<AppCard app={appWithAccessConfigPermission} />)
|
||||
|
||||
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(<AppCard app={appWithAccessConfigPermission} />)
|
||||
|
||||
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(<AppCard app={appWithAccessConfigPermission} />)
|
||||
|
||||
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(<AppCard app={mockApp} />)
|
||||
|
||||
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(<AppCard app={mockApp} onRefresh={mockOnRefresh} />)
|
||||
|
||||
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(<AppCard app={mockApp} />)
|
||||
|
||||
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(<AppCard app={mockApp} />)
|
||||
|
||||
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(<AppCard app={workflowApp} />)
|
||||
|
||||
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(<AppCard app={mockApp} />)
|
||||
|
||||
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 ? (
|
||||
<div role="alertdialog">
|
||||
<button
|
||||
type="button"
|
||||
data-testid="keep-open-dialog"
|
||||
onClick={() => onOpenChange?.(true)}
|
||||
>
|
||||
Keep open
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="force-close-dialog"
|
||||
onClick={() => onOpenChange?.(false)}
|
||||
>
|
||||
Force close
|
||||
</button>
|
||||
{children}
|
||||
</div>
|
||||
) : null,
|
||||
AlertDialogContent: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
AlertDialogTitle: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
AlertDialogDescription: ({ children }: { children: React.ReactNode }) => (
|
||||
<div>{children}</div>
|
||||
),
|
||||
AlertDialogActions: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
AlertDialogCancelButton: ({
|
||||
children,
|
||||
...props
|
||||
}: React.ButtonHTMLAttributes<HTMLButtonElement>) => (
|
||||
<button type="button" {...props}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
AlertDialogConfirmButton: ({
|
||||
children,
|
||||
...props
|
||||
}: React.ButtonHTMLAttributes<HTMLButtonElement> & { loading?: boolean }) => (
|
||||
<button type="button" {...props}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
})
|
||||
|
||||
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(<IsolatedAppCard app={mockApp} />)
|
||||
|
||||
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(<IsolatedAppCard app={mockApp} />)
|
||||
|
||||
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(<IsolatedAppCard app={mockApp} />)
|
||||
|
||||
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
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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 <div data-testid="modal">{children}</div>
|
||||
},
|
||||
DialogContent: ({ children }: { children: React.ReactNode }) => (
|
||||
<div role="dialog" data-testid="modal-content">
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
DialogTitle: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
}))
|
||||
|
||||
// Sidebar mock removed to use real component
|
||||
|
||||
const mockAppData: AppData = {
|
||||
|
||||
@ -16,23 +16,6 @@ vi.mock('@/app/components/base/chat/chat-with-history/inputs-form/content', () =
|
||||
default: () => <div data-testid="inputs-form-content">InputsFormContent</div>,
|
||||
}))
|
||||
|
||||
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 <div data-testid="modal">{children}</div>
|
||||
},
|
||||
DialogContent: ({ children }: { children: React.ReactNode }) => (
|
||||
<div role="dialog" data-testid="modal-content">
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
DialogTitle: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
}))
|
||||
|
||||
const mockAppData: AppData = {
|
||||
app_id: 'app-1',
|
||||
site: {
|
||||
|
||||
@ -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 }) => (
|
||||
<div data-testid="modal">{children}</div>
|
||||
),
|
||||
DialogTitle: ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="modal-title">{children}</div>
|
||||
),
|
||||
}))
|
||||
|
||||
describe('Sidebar Index', () => {
|
||||
const mockContextValue = {
|
||||
isInstalledApp: false,
|
||||
|
||||
@ -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 }) => <div role="dialog">{children}</div>,
|
||||
DialogTitle: ({ children }: { children: ReactNode }) => <h2>{children}</h2>,
|
||||
}))
|
||||
|
||||
describe('RenameModal', () => {
|
||||
const defaultProps = {
|
||||
isShow: true,
|
||||
|
||||
@ -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<HTMLTextAreaElement>) {
|
||||
return (
|
||||
<textarea
|
||||
data-testid="content-item-textarea"
|
||||
value={value}
|
||||
onChange={(event) => {
|
||||
onChange?.({ target: { value: event.target.value } })
|
||||
onValueChange?.(event.target.value)
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
vi.mock('@langgenius/dify-ui/textarea', () => ({
|
||||
Textarea: MockTextarea,
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/select', async () => {
|
||||
const React = await import('react')
|
||||
const SelectValueContext = React.createContext<string | null>(null)
|
||||
|
||||
return {
|
||||
Select: ({
|
||||
children,
|
||||
onValueChange,
|
||||
value,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
onValueChange: (value: string | null) => void
|
||||
value: string | null
|
||||
}) => (
|
||||
<SelectValueContext value={value}>
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="content-item-select-root"
|
||||
onClick={() => onValueChange('alice')}
|
||||
>
|
||||
select alice
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="content-item-select-null"
|
||||
onClick={() => onValueChange(null)}
|
||||
>
|
||||
select null
|
||||
</button>
|
||||
{children}
|
||||
</div>
|
||||
</SelectValueContext>
|
||||
),
|
||||
SelectValue: () => <>{React.use(SelectValueContext)}</>,
|
||||
SelectTrigger: ({ children }: { children: React.ReactNode }) => (
|
||||
<button type="button" data-testid="content-item-select">
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
SelectContent: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
SelectItem: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
SelectItemText: ({ children }: { children: React.ReactNode }) => <span>{children}</span>,
|
||||
SelectItemIndicator: () => <span>selected</span>,
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/app/components/base/file-uploader', () => ({
|
||||
FileUploaderInAttachmentWrapper: ({
|
||||
value,
|
||||
@ -191,34 +117,12 @@ describe('HumanInputFieldRenderer', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
await user.click(screen.getByTestId('content-item-select-root'))
|
||||
await user.click(screen.getByRole('combobox', { name: 'reviewer' }))
|
||||
await user.click(await screen.findByRole('option', { name: 'alice' }))
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith('alice')
|
||||
})
|
||||
|
||||
it('ignores null select values', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onChange = vi.fn()
|
||||
|
||||
render(
|
||||
<HumanInputFieldRenderer
|
||||
field={{
|
||||
type: InputVarType.select,
|
||||
output_variable_name: 'reviewer',
|
||||
option_source: { type: 'constant', selector: [], value: ['alice', 'bob'] },
|
||||
}}
|
||||
value={null}
|
||||
onChange={onChange}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('content-item-select')).toHaveTextContent('')
|
||||
|
||||
await user.click(screen.getByTestId('content-item-select-null'))
|
||||
|
||||
expect(onChange).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('renders single-file input and emits one file', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onChange = vi.fn()
|
||||
|
||||
@ -1,12 +1,10 @@
|
||||
import type { Resources } from '../index'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import { render, screen, waitFor, within } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { useDocumentDownload } from '@/service/knowledge/use-document'
|
||||
import { downloadUrl } from '@/utils/download'
|
||||
import Popup from '../popup'
|
||||
|
||||
vi.mock('@langgenius/dify-ui/popover', async () => await import('@/__mocks__/base-ui-popover'))
|
||||
|
||||
vi.mock('@/service/knowledge/use-document', () => ({
|
||||
useDocumentDownload: vi.fn(),
|
||||
}))
|
||||
@ -65,8 +63,12 @@ const makeData = (overrides: Partial<Resources> = {}): Resources => ({
|
||||
const openPopup = async (user: ReturnType<typeof userEvent.setup>) => {
|
||||
await user.click(screen.getByTestId('popup-trigger'))
|
||||
}
|
||||
const getDownloadButton = (name = 'report.pdf') => screen.getByRole('button', { name })
|
||||
const queryDownloadButton = (name = 'report.pdf') => screen.queryByRole('button', { name })
|
||||
const getDownloadButton = (name = 'report.pdf') =>
|
||||
within(screen.getByTestId('popup-content')).getByRole('button', { name })
|
||||
const queryDownloadButton = (name = 'report.pdf') =>
|
||||
screen.queryByTestId('popup-content')
|
||||
? within(screen.getByTestId('popup-content')).queryByRole('button', { name })
|
||||
: null
|
||||
|
||||
describe('Popup', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
@ -1,31 +1,9 @@
|
||||
import type { DatePickerProps } from '../../types'
|
||||
import { act, fireEvent, render, screen, within } from '@testing-library/react'
|
||||
import { fireEvent, render, screen, within } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import dayjs from '../../utils/dayjs'
|
||||
import DatePicker from '../index'
|
||||
|
||||
vi.mock('@langgenius/dify-ui/popover', async () => await import('@/__mocks__/base-ui-popover'))
|
||||
vi.mock('@langgenius/dify-ui/button', () => ({
|
||||
Button: ({
|
||||
children,
|
||||
onClick,
|
||||
disabled,
|
||||
className,
|
||||
}: {
|
||||
children?: React.ReactNode
|
||||
onClick?: () => void
|
||||
disabled?: boolean
|
||||
className?: string
|
||||
}) => (
|
||||
<button
|
||||
onClick={onClick as (() => void) | undefined}
|
||||
disabled={disabled as boolean | undefined}
|
||||
className={className as string | undefined}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock scrollIntoView
|
||||
beforeAll(() => {
|
||||
Element.prototype.scrollIntoView = vi.fn()
|
||||
@ -131,19 +109,18 @@ describe('DatePicker', () => {
|
||||
expect(screen.getByText(/2024/))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should close when clicking outside the container', () => {
|
||||
it('should close when clicking outside the container', async () => {
|
||||
const user = userEvent.setup()
|
||||
const props = createDatePickerProps()
|
||||
render(<DatePicker {...props} />)
|
||||
|
||||
openPicker()
|
||||
expect(screen.getByTestId('popover')).toHaveAttribute('data-open', 'true')
|
||||
expect(screen.getAllByText(/daysInWeek/).length).toBeGreaterThan(0)
|
||||
|
||||
act(() => {
|
||||
document.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }))
|
||||
})
|
||||
await user.click(document.body)
|
||||
|
||||
expect(screen.getByTestId('popover')).toHaveAttribute('data-open', 'false')
|
||||
expect(screen.getByRole('textbox'))!.toBeInTheDocument()
|
||||
expect(screen.queryAllByText(/daysInWeek/)).toHaveLength(0)
|
||||
expect(screen.getByRole('textbox')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -1,31 +1,9 @@
|
||||
import type { TimePickerProps } from '../../types'
|
||||
import { fireEvent, render, screen, within } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import dayjs, { isDayjsObject } from '../../utils/dayjs'
|
||||
import TimePicker from '../index'
|
||||
|
||||
vi.mock('@langgenius/dify-ui/popover', async () => await import('@/__mocks__/base-ui-popover'))
|
||||
vi.mock('@langgenius/dify-ui/button', () => ({
|
||||
Button: ({
|
||||
children,
|
||||
onClick,
|
||||
disabled,
|
||||
className,
|
||||
}: {
|
||||
children?: React.ReactNode
|
||||
onClick?: () => void
|
||||
disabled?: boolean
|
||||
className?: string
|
||||
}) => (
|
||||
<button
|
||||
onClick={onClick as (() => void) | undefined}
|
||||
disabled={disabled as boolean | undefined}
|
||||
className={className as string | undefined}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock scrollIntoView since the test DOM runtime doesn't implement it
|
||||
beforeAll(() => {
|
||||
Element.prototype.scrollIntoView = vi.fn()
|
||||
@ -103,15 +81,16 @@ describe('TimePicker', () => {
|
||||
expect(input)!.toHaveValue('10:00 AM')
|
||||
})
|
||||
|
||||
it('should handle document mousedown listener while picker is open', () => {
|
||||
it('should close when clicking outside while the picker is open', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<TimePicker {...baseProps} value="10:00 AM" timezone="UTC" />)
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
fireEvent.click(input)
|
||||
expect(input)!.toHaveValue('')
|
||||
|
||||
fireEvent.mouseDown(document.body)
|
||||
expect(input)!.toHaveValue('10:00 AM')
|
||||
await user.click(document.body)
|
||||
expect(input).toHaveValue('10:00 AM')
|
||||
})
|
||||
|
||||
it('should call onClear when clear is clicked while picker is closed', () => {
|
||||
|
||||
@ -3,8 +3,6 @@ import type { Features } from '../../../types'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { FeaturesProvider } from '../../../context'
|
||||
import VoiceSettings from '../voice-settings'
|
||||
|
||||
vi.mock('@langgenius/dify-ui/popover', () => import('@/__mocks__/base-ui-popover'))
|
||||
vi.mock('@langgenius/dify-ui/toast', () => ({
|
||||
toast: {
|
||||
success: vi.fn(),
|
||||
@ -25,25 +23,6 @@ vi.mock('@/service/use-apps', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/switch', () => ({
|
||||
Switch: ({
|
||||
checked,
|
||||
onCheckedChange,
|
||||
...props
|
||||
}: {
|
||||
checked?: boolean
|
||||
onCheckedChange?: (checked: boolean) => void
|
||||
}) => (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="switch"
|
||||
data-checked={String(checked)}
|
||||
onClick={() => onCheckedChange?.(!checked)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
}))
|
||||
|
||||
const defaultFeatures: Features = {
|
||||
moreLikeThis: { enabled: false },
|
||||
opening: { enabled: false },
|
||||
@ -123,16 +102,4 @@ describe('VoiceSettings', () => {
|
||||
|
||||
expect(onOpen).toHaveBeenCalledWith(false)
|
||||
})
|
||||
|
||||
it('should use top placement and mainAxis 4 when placementLeft is false', () => {
|
||||
renderWithProvider(
|
||||
<VoiceSettings open={true} onOpen={vi.fn()} placementLeft={false}>
|
||||
<button>Settings</button>
|
||||
</VoiceSettings>,
|
||||
)
|
||||
|
||||
const content = screen.getByTestId('popover-content')
|
||||
expect(content).toHaveAttribute('data-placement', 'top')
|
||||
expect(content).toHaveAttribute('data-side-offset', '4')
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,57 +1,29 @@
|
||||
import type { DeclaredOutputConfig } from '@dify/contracts/api/console/apps/types.gen'
|
||||
import type { ButtonHTMLAttributes, ReactNode } from 'react'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { $getNodeByKey } from 'lexical'
|
||||
import AgentOutputBlockComponent from '../component'
|
||||
|
||||
const { mockEditorFocus, mockEditorUpdate, mockGetRootText, mockSelectNext, mockSetOutput } =
|
||||
vi.hoisted(() => ({
|
||||
mockEditorFocus: vi.fn(),
|
||||
mockEditorUpdate: vi.fn((callback: () => void) => callback()),
|
||||
mockGetRootText: {
|
||||
value: '[§output:summary:summary§]',
|
||||
},
|
||||
mockSelectNext: vi.fn(),
|
||||
mockSetOutput: vi.fn(),
|
||||
}))
|
||||
const {
|
||||
mockEditorFocus,
|
||||
mockEditorUpdate,
|
||||
mockGetRootText,
|
||||
mockSelectNext,
|
||||
mockSetOpenTypeSelectOnEdit,
|
||||
mockSetOutput,
|
||||
} = vi.hoisted(() => ({
|
||||
mockEditorFocus: vi.fn(),
|
||||
mockEditorUpdate: vi.fn((callback: () => void) => callback()),
|
||||
mockGetRootText: {
|
||||
value: '[§output:summary:summary§]',
|
||||
},
|
||||
mockSelectNext: vi.fn(),
|
||||
mockSetOpenTypeSelectOnEdit: vi.fn(),
|
||||
mockSetOutput: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@lexical/react/LexicalComposerContext')
|
||||
vi.mock('@langgenius/dify-ui/select', () => ({
|
||||
Select: ({
|
||||
children,
|
||||
onValueChange,
|
||||
open,
|
||||
}: {
|
||||
children: ReactNode
|
||||
onValueChange: (value: string) => void
|
||||
open?: boolean
|
||||
}) => (
|
||||
<div>
|
||||
<span data-testid="type-select-state">{open ? 'open' : 'closed'}</span>
|
||||
{children}
|
||||
<button type="button" onClick={() => onValueChange('file')}>
|
||||
Select file
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
SelectContent: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
SelectItem: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
SelectItemIndicator: () => <span />,
|
||||
SelectItemText: ({ children }: { children: ReactNode }) => <span>{children}</span>,
|
||||
SelectLabel: ({ children }: { children: ReactNode }) => <span>{children}</span>,
|
||||
SelectTrigger: ({
|
||||
children,
|
||||
onClick,
|
||||
onMouseDown,
|
||||
...props
|
||||
}: ButtonHTMLAttributes<HTMLButtonElement>) => (
|
||||
<button type="button" onClick={onClick} onMouseDown={onMouseDown} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
vi.mock('lexical', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('lexical')>()
|
||||
|
||||
@ -69,7 +41,7 @@ vi.mock('lexical', async (importOriginal) => {
|
||||
})
|
||||
|
||||
vi.mock('../node', () => ({
|
||||
$isAgentOutputBlockNode: () => true,
|
||||
$isAgentOutputBlockNode: (node: unknown) => Boolean(node),
|
||||
}))
|
||||
|
||||
const outputs: DeclaredOutputConfig[] = [
|
||||
@ -93,6 +65,7 @@ describe('AgentOutputBlockComponent', () => {
|
||||
] as unknown as ReturnType<typeof useLexicalComposerContext>)
|
||||
vi.mocked($getNodeByKey).mockReturnValue({
|
||||
selectNext: mockSelectNext,
|
||||
setOpenTypeSelectOnEdit: mockSetOpenTypeSelectOnEdit,
|
||||
setOutput: mockSetOutput,
|
||||
} as never)
|
||||
})
|
||||
@ -151,7 +124,9 @@ describe('AgentOutputBlockComponent', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('type-select-state')).toHaveTextContent('open')
|
||||
expect(
|
||||
screen.getByRole('combobox', { name: 'workflow.nodes.agent.outputVars.typeLabel' }),
|
||||
).toHaveAttribute('aria-expanded', 'true')
|
||||
})
|
||||
|
||||
it('does not update the Lexical node while typing an output name and commits on blur', async () => {
|
||||
@ -248,7 +223,9 @@ describe('AgentOutputBlockComponent', () => {
|
||||
false,
|
||||
)
|
||||
expect(mockSelectNext).toHaveBeenCalledTimes(1)
|
||||
expect(screen.getByTestId('type-select-state')).toHaveTextContent('closed')
|
||||
expect(
|
||||
screen.getByRole('combobox', { name: 'workflow.nodes.agent.outputVars.typeLabel' }),
|
||||
).toHaveAttribute('aria-expanded', 'false')
|
||||
expect(onChange).toHaveBeenCalledWith(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
@ -300,7 +277,9 @@ describe('AgentOutputBlockComponent', () => {
|
||||
true,
|
||||
)
|
||||
expect(mockSelectNext).not.toHaveBeenCalled()
|
||||
expect(screen.getByTestId('type-select-state')).toHaveTextContent('open')
|
||||
expect(
|
||||
screen.getByRole('combobox', { name: 'workflow.nodes.agent.outputVars.typeLabel' }),
|
||||
).toHaveAttribute('aria-expanded', 'true')
|
||||
expect(input).not.toHaveFocus()
|
||||
expect((input as HTMLInputElement).selectionStart).toBe('summary'.length)
|
||||
expect((input as HTMLInputElement).selectionEnd).toBe('summary'.length)
|
||||
@ -374,7 +353,8 @@ describe('AgentOutputBlockComponent', () => {
|
||||
expect(onChange).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not commit the name blur before selecting an output type', () => {
|
||||
it('does not commit the name blur before selecting an output type', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onChange = vi.fn()
|
||||
mockGetRootText.value = '[§output:summary:summary§]'
|
||||
|
||||
@ -390,17 +370,16 @@ describe('AgentOutputBlockComponent', () => {
|
||||
)
|
||||
|
||||
const input = screen.getByRole('textbox', { name: 'workflow.nodes.agent.outputVars.nameLabel' })
|
||||
const typeTrigger = screen.getByRole('button', {
|
||||
const typeTrigger = screen.getByRole('combobox', {
|
||||
name: 'workflow.nodes.agent.outputVars.typeLabel',
|
||||
})
|
||||
|
||||
fireEvent.change(input, { target: { value: 'summary' } })
|
||||
fireEvent.mouseDown(typeTrigger)
|
||||
fireEvent.blur(input)
|
||||
await user.click(typeTrigger)
|
||||
|
||||
expect(onChange).not.toHaveBeenCalled()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Select file' }))
|
||||
await user.click(await screen.findByRole('option', { name: 'file' }))
|
||||
|
||||
expect(mockSetOutput).toHaveBeenCalledWith(
|
||||
'summary',
|
||||
@ -461,7 +440,7 @@ describe('AgentOutputBlockComponent', () => {
|
||||
screen.queryByRole('textbox', { name: 'workflow.nodes.agent.outputVars.nameLabel' }),
|
||||
).not.toBeInTheDocument()
|
||||
expect(
|
||||
screen.queryByRole('button', { name: 'workflow.nodes.agent.outputVars.typeLabel' }),
|
||||
screen.queryByRole('combobox', { name: 'workflow.nodes.agent.outputVars.typeLabel' }),
|
||||
).not.toBeInTheDocument()
|
||||
expect(screen.getByText('qna_report_pdf')).toBeInTheDocument()
|
||||
expect(screen.getByText('file')).toBeInTheDocument()
|
||||
|
||||
@ -3,8 +3,6 @@ import userEvent from '@testing-library/user-event'
|
||||
import { UPDATE_DATASETS_EVENT_EMITTER } from '../../../constants'
|
||||
import ContextBlockComponent from '../component'
|
||||
|
||||
vi.mock('@langgenius/dify-ui/popover', async () => await import('@/__mocks__/base-ui-popover'))
|
||||
|
||||
// Mock the hooks used by ContextBlockComponent
|
||||
const mockUseSelectOrDelete = vi.fn()
|
||||
const mockUseTrigger = vi.fn()
|
||||
@ -148,17 +146,6 @@ describe('ContextBlockComponent', () => {
|
||||
})
|
||||
|
||||
describe('User Interactions', () => {
|
||||
it('should keep the popover closed when the trigger prevents the default click', async () => {
|
||||
const user = userEvent.setup()
|
||||
const { triggerSetOpen } = defaultSetup()
|
||||
render(<ContextBlockComponent nodeKey="test-key" onAddContext={vi.fn()} />)
|
||||
|
||||
await user.click(screen.getByTestId('popover-trigger'))
|
||||
|
||||
expect(triggerSetOpen).not.toHaveBeenCalled()
|
||||
expect(screen.queryByText('common.promptEditor.context.modal.add')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call onAddContext when add button is clicked', async () => {
|
||||
defaultSetup({ open: true })
|
||||
const handleAddContext = vi.fn()
|
||||
|
||||
@ -6,8 +6,6 @@ import { UPDATE_HISTORY_EVENT_EMITTER } from '../../../constants'
|
||||
import HistoryBlockComponent from '../component'
|
||||
import { DELETE_HISTORY_BLOCK_COMMAND } from '../index'
|
||||
|
||||
vi.mock('@langgenius/dify-ui/popover', async () => await import('@/__mocks__/base-ui-popover'))
|
||||
|
||||
type HistoryEventPayload = {
|
||||
type?: string
|
||||
payload?: RoleName
|
||||
@ -99,19 +97,6 @@ describe('HistoryBlockComponent', () => {
|
||||
expect(screen.getByText('common.promptEditor.history.modal.assistant')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should keep the popover closed when the trigger prevents the default click', async () => {
|
||||
const user = userEvent.setup()
|
||||
const setOpen = vi.fn() as unknown as Dispatch<SetStateAction<boolean>>
|
||||
mockUseTrigger.mockReturnValue(createTriggerHookReturn(false, setOpen))
|
||||
|
||||
render(<HistoryBlockComponent nodeKey="history-node-trigger" onEditRole={vi.fn()} />)
|
||||
|
||||
await user.click(screen.getByTestId('popover-trigger'))
|
||||
|
||||
expect(setOpen).not.toHaveBeenCalled()
|
||||
expect(screen.queryByText('common.promptEditor.history.modal.edit')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call onEditRole when edit action is clicked', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onEditRole = vi.fn()
|
||||
|
||||
@ -1,31 +1,15 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { Mock } from 'vitest'
|
||||
import type { UsagePlanInfo } from '../../type'
|
||||
import { screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { useGetPricingPageLanguage } from '@/context/i18n'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { render } from '@/test/console/render'
|
||||
import { Plan } from '../../type'
|
||||
import Pricing from '../index'
|
||||
|
||||
type DialogProps = {
|
||||
children: ReactNode
|
||||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
}
|
||||
|
||||
let latestOnOpenChange: DialogProps['onOpenChange']
|
||||
let mockConsoleState: Record<string, unknown> = {}
|
||||
|
||||
vi.mock('@langgenius/dify-ui/dialog', () => ({
|
||||
Dialog: ({ children, onOpenChange }: DialogProps) => {
|
||||
latestOnOpenChange = onOpenChange
|
||||
return <div>{children}</div>
|
||||
},
|
||||
DialogContent: ({ children, className }: { children: ReactNode; className?: string }) => (
|
||||
<div className={className}>{children}</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../header', () => ({
|
||||
default: ({ onClose }: { onClose: () => void }) => (
|
||||
<button type="button" onClick={onClose}>
|
||||
@ -72,7 +56,6 @@ const buildUsage = (): UsagePlanInfo => ({
|
||||
describe('Pricing dialog lifecycle', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
latestOnOpenChange = undefined
|
||||
mockConsoleState = {
|
||||
isCurrentWorkspaceManager: true,
|
||||
}
|
||||
@ -86,12 +69,12 @@ describe('Pricing dialog lifecycle', () => {
|
||||
;(useGetPricingPageLanguage as Mock).mockReturnValue('en')
|
||||
})
|
||||
|
||||
it('should only call onCancel when the dialog requests closing', () => {
|
||||
it('should call onCancel when the pricing dialog is closed', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onCancel = vi.fn()
|
||||
render(<Pricing onCancel={onCancel} />)
|
||||
|
||||
latestOnOpenChange?.(true)
|
||||
latestOnOpenChange?.(false)
|
||||
await user.click(screen.getByRole('button', { name: 'close' }))
|
||||
|
||||
expect(onCancel).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
@ -1,515 +1,47 @@
|
||||
import type { DocumentItem } from '@/models/datasets'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import PreviewDocumentPicker from '../preview-document-picker'
|
||||
|
||||
vi.mock('@langgenius/dify-ui/popover', () => import('@/__mocks__/base-ui-popover'))
|
||||
|
||||
// Factory function to create mock DocumentItem
|
||||
const createMockDocumentItem = (overrides: Partial<DocumentItem> = {}): DocumentItem => ({
|
||||
id: `doc-${Math.random().toString(36).substr(2, 9)}`,
|
||||
name: 'Test Document',
|
||||
extension: 'txt',
|
||||
...overrides,
|
||||
})
|
||||
|
||||
// Factory function to create multiple document items
|
||||
const createMockDocumentList = (count: number): DocumentItem[] => {
|
||||
return Array.from({ length: count }, (_, index) =>
|
||||
createMockDocumentItem({
|
||||
id: `doc-${index + 1}`,
|
||||
name: `Document ${index + 1}`,
|
||||
extension: index % 2 === 0 ? 'pdf' : 'txt',
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// Factory function to create default props
|
||||
const createDefaultProps = (
|
||||
overrides: Partial<React.ComponentProps<typeof PreviewDocumentPicker>> = {},
|
||||
) => ({
|
||||
value: createMockDocumentItem({ id: 'selected-doc', name: 'Selected Document' }),
|
||||
files: createMockDocumentList(3),
|
||||
onChange: vi.fn(),
|
||||
...overrides,
|
||||
})
|
||||
|
||||
// Helper to render component with default props
|
||||
const renderComponent = (
|
||||
props: Partial<React.ComponentProps<typeof PreviewDocumentPicker>> = {},
|
||||
) => {
|
||||
const defaultProps = createDefaultProps(props)
|
||||
return {
|
||||
...render(<PreviewDocumentPicker {...defaultProps} />),
|
||||
props: defaultProps,
|
||||
}
|
||||
}
|
||||
|
||||
const openPopover = () => {
|
||||
fireEvent.click(screen.getByTestId('popover-trigger'))
|
||||
}
|
||||
const documents: DocumentItem[] = [
|
||||
{ id: 'document-1', name: 'First document', extension: 'pdf' } as DocumentItem,
|
||||
{ id: 'document-2', name: 'Second document', extension: 'txt' } as DocumentItem,
|
||||
]
|
||||
|
||||
describe('PreviewDocumentPicker', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
it('renders the selected document name', () => {
|
||||
render(<PreviewDocumentPicker value={documents[0]} files={documents} onChange={vi.fn()} />)
|
||||
|
||||
expect(screen.getByRole('button', { name: /First document/ })).toHaveAttribute(
|
||||
'aria-expanded',
|
||||
'false',
|
||||
)
|
||||
})
|
||||
|
||||
// Tests for basic rendering
|
||||
describe('Rendering', () => {
|
||||
it('should render document name from value prop', () => {
|
||||
renderComponent({
|
||||
value: createMockDocumentItem({ name: 'My Document' }),
|
||||
})
|
||||
it('opens the document list and selects a document', async () => {
|
||||
const onChange = vi.fn()
|
||||
render(<PreviewDocumentPicker value={documents[0]} files={documents} onChange={onChange} />)
|
||||
|
||||
expect(screen.getByText('My Document')).toBeInTheDocument()
|
||||
})
|
||||
fireEvent.click(screen.getByRole('button', { name: /First document/ }))
|
||||
fireEvent.click(await screen.findByRole('button', { name: 'Second document' }))
|
||||
|
||||
it('should render placeholder when name is empty', () => {
|
||||
renderComponent({
|
||||
value: createMockDocumentItem({ name: '' }),
|
||||
})
|
||||
|
||||
expect(screen.getByText('--')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render placeholder when name is undefined', () => {
|
||||
renderComponent({
|
||||
value: { id: 'doc-1', extension: 'txt' } as DocumentItem,
|
||||
})
|
||||
|
||||
expect(screen.getByText('--')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render file icon', () => {
|
||||
renderComponent({
|
||||
value: createMockDocumentItem({ extension: 'txt' }),
|
||||
files: [], // Use empty files to avoid duplicate icons
|
||||
})
|
||||
|
||||
const trigger = screen.getByTestId('popover-trigger')
|
||||
expect(trigger.querySelector('svg')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render pdf icon for pdf extension', () => {
|
||||
renderComponent({
|
||||
value: createMockDocumentItem({ extension: 'pdf' }),
|
||||
files: [], // Use empty files to avoid duplicate icons
|
||||
})
|
||||
|
||||
const trigger = screen.getByTestId('popover-trigger')
|
||||
expect(trigger.querySelector('svg')).toBeInTheDocument()
|
||||
})
|
||||
expect(onChange).toHaveBeenCalledWith(documents[1])
|
||||
expect(screen.getByRole('button', { name: /First document/ })).toHaveAttribute(
|
||||
'aria-expanded',
|
||||
'false',
|
||||
)
|
||||
})
|
||||
|
||||
// Tests for props handling
|
||||
describe('Props', () => {
|
||||
it('should accept required props', () => {
|
||||
const props = createDefaultProps()
|
||||
render(<PreviewDocumentPicker {...props} />)
|
||||
it('renders a placeholder without a selected document', () => {
|
||||
render(<PreviewDocumentPicker files={documents} onChange={vi.fn()} />)
|
||||
|
||||
expect(screen.getByTestId('popover')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle single file', () => {
|
||||
// Component should accept single file
|
||||
renderComponent({
|
||||
files: [createMockDocumentItem({ id: 'single-doc', name: 'Single File' })],
|
||||
})
|
||||
|
||||
expect(screen.getByTestId('popover')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle multiple files', () => {
|
||||
// Component should accept multiple files
|
||||
renderComponent({
|
||||
files: createMockDocumentList(5),
|
||||
})
|
||||
|
||||
expect(screen.getByTestId('popover')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should use value.extension for file icon', () => {
|
||||
renderComponent({
|
||||
value: createMockDocumentItem({ name: 'test.docx', extension: 'docx' }),
|
||||
})
|
||||
|
||||
const trigger = screen.getByTestId('popover-trigger')
|
||||
expect(trigger.querySelector('svg')).toBeInTheDocument()
|
||||
})
|
||||
expect(screen.getByRole('button', { name: /--/ })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Tests for state management
|
||||
describe('State Management', () => {
|
||||
it('should initialize with popup closed', () => {
|
||||
renderComponent()
|
||||
it('shows loading content when the file list is empty', async () => {
|
||||
render(<PreviewDocumentPicker value={documents[0]} files={[]} onChange={vi.fn()} />)
|
||||
|
||||
expect(screen.getByTestId('popover')).toHaveAttribute('data-open', 'false')
|
||||
})
|
||||
fireEvent.click(screen.getByRole('button', { name: /First document/ }))
|
||||
|
||||
it('should toggle popup when trigger is clicked', () => {
|
||||
renderComponent()
|
||||
|
||||
const trigger = screen.getByTestId('popover-trigger')
|
||||
fireEvent.click(trigger)
|
||||
|
||||
expect(trigger).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render portal content for document selection', () => {
|
||||
renderComponent()
|
||||
openPopover()
|
||||
|
||||
// Popover content is rendered after opening the trigger in our mock
|
||||
expect(screen.getByTestId('popover-content')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Tests for callback stability and memoization
|
||||
describe('Callback Stability', () => {
|
||||
it('should maintain stable onChange callback when value changes', () => {
|
||||
const onChange = vi.fn()
|
||||
const value1 = createMockDocumentItem({ id: 'doc-1', name: 'Doc 1' })
|
||||
const value2 = createMockDocumentItem({ id: 'doc-2', name: 'Doc 2' })
|
||||
|
||||
const { rerender } = render(
|
||||
<PreviewDocumentPicker
|
||||
value={value1}
|
||||
files={createMockDocumentList(3)}
|
||||
onChange={onChange}
|
||||
/>,
|
||||
)
|
||||
|
||||
rerender(
|
||||
<PreviewDocumentPicker
|
||||
value={value2}
|
||||
files={createMockDocumentList(3)}
|
||||
onChange={onChange}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('Doc 2')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should use updated onChange callback after rerender', () => {
|
||||
const onChange1 = vi.fn()
|
||||
const onChange2 = vi.fn()
|
||||
const value = createMockDocumentItem()
|
||||
const files = createMockDocumentList(3)
|
||||
|
||||
const { rerender } = render(
|
||||
<PreviewDocumentPicker value={value} files={files} onChange={onChange1} />,
|
||||
)
|
||||
|
||||
rerender(<PreviewDocumentPicker value={value} files={files} onChange={onChange2} />)
|
||||
|
||||
expect(screen.getByTestId('popover')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Tests for component memoization
|
||||
describe('Component Memoization', () => {
|
||||
it('should not re-render when props are the same', () => {
|
||||
const onChange = vi.fn()
|
||||
const value = createMockDocumentItem()
|
||||
const files = createMockDocumentList(3)
|
||||
|
||||
const { rerender } = render(
|
||||
<PreviewDocumentPicker value={value} files={files} onChange={onChange} />,
|
||||
)
|
||||
|
||||
rerender(<PreviewDocumentPicker value={value} files={files} onChange={onChange} />)
|
||||
|
||||
expect(screen.getByTestId('popover')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Tests for user interactions
|
||||
describe('User Interactions', () => {
|
||||
it('should toggle popup when trigger is clicked', () => {
|
||||
renderComponent()
|
||||
|
||||
const trigger = screen.getByTestId('popover-trigger')
|
||||
fireEvent.click(trigger)
|
||||
|
||||
expect(trigger).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render document list with files', () => {
|
||||
const files = createMockDocumentList(3)
|
||||
renderComponent({ files })
|
||||
openPopover()
|
||||
|
||||
// Documents should be visible in the list
|
||||
expect(screen.getByText('Document 1')).toBeInTheDocument()
|
||||
expect(screen.getByText('Document 2')).toBeInTheDocument()
|
||||
expect(screen.getByText('Document 3')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call onChange when document is selected', () => {
|
||||
const onChange = vi.fn()
|
||||
const files = createMockDocumentList(3)
|
||||
|
||||
renderComponent({ files, onChange })
|
||||
openPopover()
|
||||
|
||||
fireEvent.click(screen.getByText('Document 2'))
|
||||
|
||||
// handleChange should call onChange with the selected item
|
||||
expect(onChange).toHaveBeenCalledTimes(1)
|
||||
expect(onChange).toHaveBeenCalledWith(files[1])
|
||||
})
|
||||
|
||||
it('should handle rapid toggle clicks', () => {
|
||||
renderComponent()
|
||||
|
||||
const trigger = screen.getByTestId('popover-trigger')
|
||||
|
||||
// Rapid clicks
|
||||
fireEvent.click(trigger)
|
||||
fireEvent.click(trigger)
|
||||
fireEvent.click(trigger)
|
||||
fireEvent.click(trigger)
|
||||
|
||||
expect(trigger).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Tests for edge cases
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle null value properties gracefully', () => {
|
||||
renderComponent({
|
||||
value: { id: 'doc-1', name: '', extension: '' },
|
||||
})
|
||||
|
||||
expect(screen.getByText('--')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render when value prop is omitted (optional)', () => {
|
||||
const files = createMockDocumentList(2)
|
||||
const onChange = vi.fn()
|
||||
// Do not pass `value` at all to verify optional behavior
|
||||
render(<PreviewDocumentPicker files={files} onChange={onChange} />)
|
||||
|
||||
// Renders placeholder for missing name
|
||||
expect(screen.getByText('--')).toBeInTheDocument()
|
||||
// Portal wrapper renders
|
||||
expect(screen.getByTestId('popover')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle very long document names', () => {
|
||||
const longName = 'A'.repeat(500)
|
||||
renderComponent({
|
||||
value: createMockDocumentItem({ name: longName }),
|
||||
})
|
||||
|
||||
expect(screen.getByText(longName)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle special characters in document name', () => {
|
||||
const specialName = '<script>alert("xss")</script>'
|
||||
renderComponent({
|
||||
value: createMockDocumentItem({ name: specialName }),
|
||||
})
|
||||
|
||||
expect(screen.getByText(specialName)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle large number of files', () => {
|
||||
const manyFiles = createMockDocumentList(100)
|
||||
renderComponent({ files: manyFiles })
|
||||
|
||||
// Component should accept large files array
|
||||
expect(screen.getByTestId('popover')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle files with same name but different extensions', () => {
|
||||
const files = [
|
||||
createMockDocumentItem({ id: 'doc-1', name: 'document', extension: 'pdf' }),
|
||||
createMockDocumentItem({ id: 'doc-2', name: 'document', extension: 'txt' }),
|
||||
]
|
||||
renderComponent({ files })
|
||||
|
||||
// Component should handle duplicate names
|
||||
expect(screen.getByTestId('popover')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Tests for prop variations
|
||||
describe('Prop Variations', () => {
|
||||
describe('value variations', () => {
|
||||
it('should handle value with all fields', () => {
|
||||
renderComponent({
|
||||
value: {
|
||||
id: 'full-doc',
|
||||
name: 'Full Document',
|
||||
extension: 'pdf',
|
||||
},
|
||||
})
|
||||
|
||||
expect(screen.getByText('Full Document')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle value with minimal fields', () => {
|
||||
renderComponent({
|
||||
value: { id: 'minimal', name: '', extension: '' },
|
||||
})
|
||||
|
||||
expect(screen.getByText('--')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('files variations', () => {
|
||||
it('should handle single file', () => {
|
||||
renderComponent({
|
||||
files: [createMockDocumentItem({ name: 'Single' })],
|
||||
})
|
||||
|
||||
expect(screen.getByTestId('popover')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle two files', () => {
|
||||
renderComponent({
|
||||
files: createMockDocumentList(2),
|
||||
})
|
||||
|
||||
expect(screen.getByTestId('popover')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle many files', () => {
|
||||
renderComponent({
|
||||
files: createMockDocumentList(50),
|
||||
})
|
||||
|
||||
expect(screen.getByTestId('popover')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('extension variations', () => {
|
||||
const extensions = ['txt', 'pdf', 'docx', 'xlsx', 'md']
|
||||
|
||||
it.each(extensions)('should render icon for %s extension', (ext) => {
|
||||
renderComponent({
|
||||
value: createMockDocumentItem({ extension: ext }),
|
||||
files: [], // Use empty files to avoid duplicate icons
|
||||
})
|
||||
|
||||
const trigger = screen.getByTestId('popover-trigger')
|
||||
expect(trigger.querySelector('svg')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Tests for document list rendering
|
||||
describe('Document List Rendering', () => {
|
||||
it('should render all documents in the list', () => {
|
||||
const files = createMockDocumentList(5)
|
||||
renderComponent({ files })
|
||||
openPopover()
|
||||
|
||||
// All documents should be visible
|
||||
files.forEach((file) => {
|
||||
expect(screen.getByText(file.name)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should pass onChange handler to DocumentList', () => {
|
||||
const onChange = vi.fn()
|
||||
const files = createMockDocumentList(3)
|
||||
|
||||
renderComponent({ files, onChange })
|
||||
openPopover()
|
||||
|
||||
fireEvent.click(screen.getByText('Document 1'))
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(files[0])
|
||||
})
|
||||
|
||||
it('should show count header only for multiple files', () => {
|
||||
// Single file - no header
|
||||
const { rerender } = render(
|
||||
<PreviewDocumentPicker
|
||||
value={createMockDocumentItem()}
|
||||
files={[createMockDocumentItem({ name: 'Single File' })]}
|
||||
onChange={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
expect(screen.queryByText(/files/)).not.toBeInTheDocument()
|
||||
|
||||
// Multiple files - show header
|
||||
rerender(
|
||||
<PreviewDocumentPicker
|
||||
value={createMockDocumentItem()}
|
||||
files={createMockDocumentList(3)}
|
||||
onChange={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
openPopover()
|
||||
expect(screen.getByText(/dataset\.preprocessDocument/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Tests for visual states
|
||||
describe('Visual States', () => {
|
||||
it('should have max-width on name element', () => {
|
||||
renderComponent({
|
||||
value: createMockDocumentItem({ name: 'Test' }),
|
||||
})
|
||||
|
||||
const nameElement = screen.getByText('Test')
|
||||
expect(nameElement).toHaveClass('max-w-50')
|
||||
})
|
||||
})
|
||||
|
||||
// Tests for handleChange callback
|
||||
describe('handleChange Callback', () => {
|
||||
it('should call onChange with selected document item', () => {
|
||||
const onChange = vi.fn()
|
||||
const files = createMockDocumentList(3)
|
||||
|
||||
renderComponent({ files, onChange })
|
||||
openPopover()
|
||||
|
||||
fireEvent.click(screen.getByText('Document 1'))
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(files[0])
|
||||
})
|
||||
|
||||
it('should handle different document items in files', () => {
|
||||
const onChange = vi.fn()
|
||||
const customFiles = [
|
||||
{ id: 'custom-1', name: 'Custom File 1', extension: 'pdf' },
|
||||
{ id: 'custom-2', name: 'Custom File 2', extension: 'txt' },
|
||||
]
|
||||
|
||||
renderComponent({ files: customFiles, onChange })
|
||||
openPopover()
|
||||
|
||||
fireEvent.click(screen.getByText('Custom File 1'))
|
||||
expect(onChange).toHaveBeenCalledWith(customFiles[0])
|
||||
|
||||
openPopover()
|
||||
fireEvent.click(screen.getByText('Custom File 2'))
|
||||
expect(onChange).toHaveBeenCalledWith(customFiles[1])
|
||||
})
|
||||
|
||||
it('should work with multiple sequential selections', () => {
|
||||
const onChange = vi.fn()
|
||||
const files = createMockDocumentList(3)
|
||||
|
||||
renderComponent({ files, onChange })
|
||||
|
||||
// Select multiple documents sequentially
|
||||
openPopover()
|
||||
fireEvent.click(screen.getByText('Document 1'))
|
||||
openPopover()
|
||||
fireEvent.click(screen.getByText('Document 3'))
|
||||
openPopover()
|
||||
fireEvent.click(screen.getByText('Document 2'))
|
||||
|
||||
expect(onChange).toHaveBeenCalledTimes(3)
|
||||
expect(onChange).toHaveBeenNthCalledWith(1, files[0])
|
||||
expect(onChange).toHaveBeenNthCalledWith(2, files[2])
|
||||
expect(onChange).toHaveBeenNthCalledWith(3, files[1])
|
||||
})
|
||||
expect(await screen.findByRole('dialog')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -115,24 +115,6 @@ vi.mock('@/app/components/base/param-item/score-threshold-item', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/switch', () => ({
|
||||
Switch: ({
|
||||
checked,
|
||||
onCheckedChange,
|
||||
}: {
|
||||
checked: boolean
|
||||
onCheckedChange?: (v: boolean) => void
|
||||
}) => (
|
||||
<button
|
||||
data-testid="rerank-switch"
|
||||
data-checked={checked}
|
||||
onClick={() => onCheckedChange?.(!checked)}
|
||||
>
|
||||
Switch
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
|
||||
describe('RetrievalParamConfig', () => {
|
||||
const createDefaultConfig = (overrides?: Partial<RetrievalConfig>): RetrievalConfig => ({
|
||||
search_method: RETRIEVE_METHOD.semantic,
|
||||
@ -166,7 +148,7 @@ describe('RetrievalParamConfig', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('rerank-switch'))!.toBeInTheDocument()
|
||||
expect(screen.getByRole('switch')).toBeChecked()
|
||||
})
|
||||
|
||||
it('should render model selector when reranking is enabled', () => {
|
||||
@ -232,7 +214,7 @@ describe('RetrievalParamConfig', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('rerank-switch'))
|
||||
fireEvent.click(screen.getByRole('switch'))
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith({
|
||||
...config,
|
||||
@ -251,7 +233,7 @@ describe('RetrievalParamConfig', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('rerank-switch'))
|
||||
fireEvent.click(screen.getByRole('switch'))
|
||||
|
||||
expect(mockNotify).toHaveBeenCalledWith('workflow.errorMsg.rerankModelRequired')
|
||||
})
|
||||
@ -375,7 +357,7 @@ describe('RetrievalParamConfig', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('rerank-switch'))!.toBeInTheDocument()
|
||||
expect(screen.getByRole('switch')).toBeChecked()
|
||||
})
|
||||
|
||||
it('should hide score threshold when reranking is disabled for full text search', () => {
|
||||
@ -422,7 +404,7 @@ describe('RetrievalParamConfig', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.queryByTestId('rerank-switch')).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('switch')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render model selector for keyword search', () => {
|
||||
@ -762,7 +744,7 @@ describe('RetrievalParamConfig', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.queryByTestId('rerank-switch')).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('switch')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should update model selection for hybrid search', () => {
|
||||
|
||||
@ -110,30 +110,6 @@ describe('LanguageSelect', () => {
|
||||
|
||||
it('should ignore null values emitted by the select control', async () => {
|
||||
vi.resetModules()
|
||||
vi.doMock('@langgenius/dify-ui/select', () => ({
|
||||
Select: ({
|
||||
onValueChange,
|
||||
children,
|
||||
}: {
|
||||
onValueChange?: (value: string | null) => void
|
||||
children: React.ReactNode
|
||||
}) => {
|
||||
React.useEffect(() => {
|
||||
onValueChange?.(null)
|
||||
}, [onValueChange])
|
||||
return <div>{children}</div>
|
||||
},
|
||||
SelectTrigger: ({ children, ...props }: React.ButtonHTMLAttributes<HTMLButtonElement>) => (
|
||||
<button type="button" {...props}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
SelectValue: ({ placeholder }: { placeholder?: React.ReactNode }) => <>{placeholder}</>,
|
||||
SelectContent: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
SelectItem: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
SelectItemText: ({ children }: { children: React.ReactNode }) => <span>{children}</span>,
|
||||
SelectItemIndicator: () => null,
|
||||
}))
|
||||
|
||||
const { default: IsolatedLanguageSelect } = await import('../index')
|
||||
const onSelect = vi.fn()
|
||||
|
||||
@ -1,830 +1,106 @@
|
||||
import type { CredentialSelectorProps } from '../index'
|
||||
import type { DataSourceCredential } from '@/types/pipeline'
|
||||
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import CredentialSelector from '../index'
|
||||
|
||||
// Mock CredentialTypeEnum to avoid deep import chain issues
|
||||
const MockCredentialTypeEnum = {
|
||||
OAUTH2: 'oauth2',
|
||||
API_KEY: 'api_key',
|
||||
} as const
|
||||
|
||||
// Mock plugin-auth module to avoid deep import chain issues
|
||||
vi.mock('@/app/components/plugins/plugin-auth', () => ({
|
||||
CredentialTypeEnum: {
|
||||
OAUTH2: 'oauth2',
|
||||
API_KEY: 'api_key',
|
||||
},
|
||||
CredentialTypeEnum: { OAUTH2: 'oauth2', API_KEY: 'api_key' },
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/popover', () => import('@/__mocks__/base-ui-popover'))
|
||||
|
||||
// CredentialIcon - imported directly (not mocked)
|
||||
// This is a simple UI component with no external dependencies
|
||||
|
||||
const createMockCredential = (overrides?: Partial<DataSourceCredential>): DataSourceCredential => ({
|
||||
id: 'cred-1',
|
||||
name: 'Test Credential',
|
||||
avatar_url: 'https://example.com/avatar.png',
|
||||
credential: { key: 'value' },
|
||||
is_default: false,
|
||||
type: MockCredentialTypeEnum.OAUTH2 as unknown as DataSourceCredential['type'],
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const createMockCredentials = (count: number = 3): DataSourceCredential[] =>
|
||||
Array.from({ length: count }, (_, i) =>
|
||||
createMockCredential({
|
||||
id: `cred-${i + 1}`,
|
||||
name: `Credential ${i + 1}`,
|
||||
avatar_url: `https://example.com/avatar-${i + 1}.png`,
|
||||
is_default: i === 0,
|
||||
}),
|
||||
)
|
||||
|
||||
const createDefaultProps = (
|
||||
overrides?: Partial<CredentialSelectorProps>,
|
||||
): CredentialSelectorProps => ({
|
||||
currentCredentialId: 'cred-1',
|
||||
onCredentialChange: vi.fn(),
|
||||
credentials: createMockCredentials(),
|
||||
...overrides,
|
||||
})
|
||||
const credentials = [
|
||||
{
|
||||
id: 'credential-1',
|
||||
name: 'First credential',
|
||||
avatar_url: 'https://example.com/first.png',
|
||||
type: 'oauth2',
|
||||
},
|
||||
{
|
||||
id: 'credential-2',
|
||||
name: 'Second credential',
|
||||
avatar_url: 'https://example.com/second.png',
|
||||
type: 'oauth2',
|
||||
},
|
||||
] as DataSourceCredential[]
|
||||
|
||||
describe('CredentialSelector', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
it('renders the current credential as an accessible trigger', () => {
|
||||
render(
|
||||
<CredentialSelector
|
||||
currentCredentialId="credential-1"
|
||||
credentials={credentials}
|
||||
onCredentialChange={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('button', { name: /First credential/ })).toHaveAttribute(
|
||||
'aria-expanded',
|
||||
'false',
|
||||
)
|
||||
expect(screen.getByRole('img')).toHaveAttribute('src', 'https://example.com/first.png')
|
||||
})
|
||||
|
||||
// Rendering Tests - Verify component renders correctly
|
||||
describe('Rendering', () => {
|
||||
it('should render current credential name in trigger', () => {
|
||||
const props = createDefaultProps()
|
||||
it('selects the first credential when the current id is invalid', async () => {
|
||||
const onCredentialChange = vi.fn()
|
||||
render(
|
||||
<CredentialSelector
|
||||
currentCredentialId="missing"
|
||||
credentials={credentials}
|
||||
onCredentialChange={onCredentialChange}
|
||||
/>,
|
||||
)
|
||||
|
||||
render(<CredentialSelector {...props} />)
|
||||
|
||||
expect(screen.getByText('Credential 1'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render credential icon with correct props', () => {
|
||||
const props = createDefaultProps()
|
||||
|
||||
const { container } = render(<CredentialSelector {...props} />)
|
||||
|
||||
// Assert - CredentialIcon renders an img when avatarUrl is provided
|
||||
const iconImg = container.querySelector('img')
|
||||
expect(iconImg)!.toBeInTheDocument()
|
||||
expect(iconImg)!.toHaveAttribute('src', 'https://example.com/avatar-1.png')
|
||||
})
|
||||
|
||||
it('should render dropdown arrow icon', () => {
|
||||
const props = createDefaultProps()
|
||||
|
||||
const { container } = render(<CredentialSelector {...props} />)
|
||||
|
||||
const svgIcon = container.querySelector('svg')
|
||||
expect(svgIcon)!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render dropdown content initially', () => {
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<CredentialSelector {...props} />)
|
||||
|
||||
expect(screen.queryByTestId('popover-content')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render all credentials in dropdown when opened', () => {
|
||||
const props = createDefaultProps()
|
||||
render(<CredentialSelector {...props} />)
|
||||
|
||||
// Act - Click trigger to open dropdown
|
||||
const trigger = screen.getByTestId('popover-trigger')
|
||||
expect(trigger).not.toHaveAttribute('data-popup-open')
|
||||
fireEvent.click(trigger)
|
||||
|
||||
expect(trigger).toHaveAttribute('data-popup-open', '')
|
||||
expect(trigger.firstElementChild).toHaveClass('bg-state-base-hover')
|
||||
// Assert - All credentials should be visible (current credential appears in both trigger and list)
|
||||
// Assert - All credentials should be visible (current credential appears in both trigger and list)
|
||||
expect(screen.getByTestId('popover-content'))!.toBeInTheDocument()
|
||||
// 3 in dropdown list + 1 in trigger (current) = 4 total
|
||||
expect(screen.getAllByText(/Credential \d/)).toHaveLength(4)
|
||||
})
|
||||
await waitFor(() => expect(onCredentialChange).toHaveBeenCalledWith('credential-1'))
|
||||
})
|
||||
|
||||
// Props Testing - Verify all prop variations
|
||||
describe('Props', () => {
|
||||
describe('currentCredentialId prop', () => {
|
||||
it('should display first credential when currentCredentialId matches first', () => {
|
||||
const props = createDefaultProps({ currentCredentialId: 'cred-1' })
|
||||
it('does not select a fallback for an empty credential list', () => {
|
||||
const onCredentialChange = vi.fn()
|
||||
render(
|
||||
<CredentialSelector
|
||||
currentCredentialId="missing"
|
||||
credentials={[]}
|
||||
onCredentialChange={onCredentialChange}
|
||||
/>,
|
||||
)
|
||||
|
||||
render(<CredentialSelector {...props} />)
|
||||
|
||||
expect(screen.getByText('Credential 1'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should display second credential when currentCredentialId matches second', () => {
|
||||
const props = createDefaultProps({ currentCredentialId: 'cred-2' })
|
||||
|
||||
render(<CredentialSelector {...props} />)
|
||||
|
||||
expect(screen.getByText('Credential 2'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should display third credential when currentCredentialId matches third', () => {
|
||||
const props = createDefaultProps({ currentCredentialId: 'cred-3' })
|
||||
|
||||
render(<CredentialSelector {...props} />)
|
||||
|
||||
expect(screen.getByText('Credential 3'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it.each([
|
||||
['cred-1', 'Credential 1'],
|
||||
['cred-2', 'Credential 2'],
|
||||
['cred-3', 'Credential 3'],
|
||||
])(
|
||||
'should display %s credential name when currentCredentialId is %s',
|
||||
(credId, expectedName) => {
|
||||
const props = createDefaultProps({ currentCredentialId: credId })
|
||||
|
||||
render(<CredentialSelector {...props} />)
|
||||
|
||||
expect(screen.getByText(expectedName))!.toBeInTheDocument()
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
describe('credentials prop', () => {
|
||||
it('should render single credential correctly', () => {
|
||||
const props = createDefaultProps({
|
||||
credentials: [createMockCredential()],
|
||||
currentCredentialId: 'cred-1',
|
||||
})
|
||||
|
||||
render(<CredentialSelector {...props} />)
|
||||
|
||||
expect(screen.getByText('Test Credential'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render multiple credentials in dropdown', () => {
|
||||
const props = createDefaultProps({
|
||||
credentials: createMockCredentials(5),
|
||||
currentCredentialId: 'cred-1',
|
||||
})
|
||||
render(<CredentialSelector {...props} />)
|
||||
|
||||
const trigger = screen.getByTestId('popover-trigger')
|
||||
fireEvent.click(trigger)
|
||||
|
||||
// Assert - 5 in dropdown + 1 in trigger (current credential appears twice)
|
||||
expect(screen.getAllByText(/Credential \d/).length).toBe(6)
|
||||
})
|
||||
|
||||
it('should handle credentials with special characters in name', () => {
|
||||
const props = createDefaultProps({
|
||||
credentials: [
|
||||
createMockCredential({ id: 'cred-special', name: 'Test & Credential <special>' }),
|
||||
],
|
||||
currentCredentialId: 'cred-special',
|
||||
})
|
||||
|
||||
render(<CredentialSelector {...props} />)
|
||||
|
||||
expect(screen.getByText('Test & Credential <special>'))!.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('onCredentialChange prop', () => {
|
||||
it('should be called when selecting a credential', () => {
|
||||
const mockOnChange = vi.fn()
|
||||
const props = createDefaultProps({ onCredentialChange: mockOnChange })
|
||||
render(<CredentialSelector {...props} />)
|
||||
|
||||
// Act - Open dropdown
|
||||
const trigger = screen.getByTestId('popover-trigger')
|
||||
fireEvent.click(trigger)
|
||||
|
||||
const credential2 = screen.getByText('Credential 2')
|
||||
fireEvent.click(credential2)
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith('cred-2')
|
||||
})
|
||||
|
||||
it.each([
|
||||
['cred-2', 'Credential 2'],
|
||||
['cred-3', 'Credential 3'],
|
||||
])('should call onCredentialChange with %s when selecting %s', (credId, credentialName) => {
|
||||
const mockOnChange = vi.fn()
|
||||
const props = createDefaultProps({ onCredentialChange: mockOnChange })
|
||||
render(<CredentialSelector {...props} />)
|
||||
|
||||
// Act - Open dropdown and select credential
|
||||
const trigger = screen.getByTestId('popover-trigger')
|
||||
fireEvent.click(trigger)
|
||||
|
||||
// Get the dropdown item using within() to scope query to portal content
|
||||
const portalContent = screen.getByTestId('popover-content')
|
||||
const credentialOption = within(portalContent).getByText(credentialName)
|
||||
fireEvent.click(credentialOption)
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith(credId)
|
||||
})
|
||||
|
||||
it('should call onCredentialChange with cred-1 when selecting Credential 1 in dropdown', () => {
|
||||
// Arrange - Start with cred-2 selected so cred-1 is only in dropdown
|
||||
const mockOnChange = vi.fn()
|
||||
const props = createDefaultProps({
|
||||
onCredentialChange: mockOnChange,
|
||||
currentCredentialId: 'cred-2',
|
||||
})
|
||||
render(<CredentialSelector {...props} />)
|
||||
|
||||
// Act - Open dropdown and select Credential 1
|
||||
const trigger = screen.getByTestId('popover-trigger')
|
||||
fireEvent.click(trigger)
|
||||
|
||||
const credential1 = screen.getByText('Credential 1')
|
||||
fireEvent.click(credential1)
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith('cred-1')
|
||||
})
|
||||
})
|
||||
expect(onCredentialChange).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
// User Interactions - Test event handlers
|
||||
describe('User Interactions', () => {
|
||||
it('should toggle dropdown open when trigger is clicked', () => {
|
||||
const props = createDefaultProps()
|
||||
render(<CredentialSelector {...props} />)
|
||||
it('opens the real popover and selects a credential', async () => {
|
||||
const onCredentialChange = vi.fn()
|
||||
render(
|
||||
<CredentialSelector
|
||||
currentCredentialId="credential-1"
|
||||
credentials={credentials}
|
||||
onCredentialChange={onCredentialChange}
|
||||
/>,
|
||||
)
|
||||
const trigger = screen.getByRole('button', { name: /First credential/ })
|
||||
|
||||
// Assert - Initially closed
|
||||
// Assert - Initially closed
|
||||
// Assert - Initially closed
|
||||
// Assert - Initially closed
|
||||
// Assert - Initially closed
|
||||
// Assert - Initially closed
|
||||
// Assert - Initially closed
|
||||
// Assert - Initially closed
|
||||
// Assert - Initially closed
|
||||
// Assert - Initially closed
|
||||
// Assert - Initially closed
|
||||
// Assert - Initially closed
|
||||
// Assert - Initially closed
|
||||
// Assert - Initially closed
|
||||
// Assert - Initially closed
|
||||
// Assert - Initially closed
|
||||
// Assert - Initially closed
|
||||
// Assert - Initially closed
|
||||
// Assert - Initially closed
|
||||
// Assert - Initially closed
|
||||
// Assert - Initially closed
|
||||
// Assert - Initially closed
|
||||
// Assert - Initially closed
|
||||
// Assert - Initially closed
|
||||
// Assert - Initially closed
|
||||
// Assert - Initially closed
|
||||
// Assert - Initially closed
|
||||
// Assert - Initially closed
|
||||
// Assert - Initially closed
|
||||
// Assert - Initially closed
|
||||
// Assert - Initially closed
|
||||
// Assert - Initially closed
|
||||
expect(screen.queryByTestId('popover-content')).not.toBeInTheDocument()
|
||||
fireEvent.click(trigger)
|
||||
expect(trigger).toHaveAttribute('aria-expanded', 'true')
|
||||
fireEvent.click(await screen.findByText('Second credential'))
|
||||
|
||||
// Act - Click trigger
|
||||
const trigger = screen.getByTestId('popover-trigger')
|
||||
fireEvent.click(trigger)
|
||||
|
||||
// Assert - Now open
|
||||
// Assert - Now open
|
||||
expect(screen.getByTestId('popover-content'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call onCredentialChange when clicking a credential item', () => {
|
||||
const mockOnChange = vi.fn()
|
||||
const props = createDefaultProps({ onCredentialChange: mockOnChange })
|
||||
render(<CredentialSelector {...props} />)
|
||||
|
||||
const trigger = screen.getByTestId('popover-trigger')
|
||||
fireEvent.click(trigger)
|
||||
const credential2 = screen.getByText('Credential 2')
|
||||
fireEvent.click(credential2)
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledTimes(1)
|
||||
expect(mockOnChange).toHaveBeenCalledWith('cred-2')
|
||||
})
|
||||
|
||||
it('should close dropdown after selecting a credential', () => {
|
||||
const mockOnChange = vi.fn()
|
||||
const props = createDefaultProps({ onCredentialChange: mockOnChange })
|
||||
render(<CredentialSelector {...props} />)
|
||||
|
||||
// Act - Open and select
|
||||
const trigger = screen.getByTestId('popover-trigger')
|
||||
fireEvent.click(trigger)
|
||||
|
||||
expect(screen.getByTestId('popover-content'))!.toBeInTheDocument()
|
||||
|
||||
const credential2 = screen.getByText('Credential 2')
|
||||
fireEvent.click(credential2)
|
||||
|
||||
// Assert - The handleCredentialChange calls toggle(), which should change the open state
|
||||
expect(mockOnChange).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should allow selecting credentials multiple times', () => {
|
||||
// Arrange - Start with cred-2 selected so we can select other credentials
|
||||
const mockOnChange = vi.fn()
|
||||
const props = createDefaultProps({
|
||||
onCredentialChange: mockOnChange,
|
||||
currentCredentialId: 'cred-2',
|
||||
})
|
||||
|
||||
render(<CredentialSelector {...props} />)
|
||||
|
||||
// Act & Assert - Select Credential 1 (different from current)
|
||||
const trigger = screen.getByTestId('popover-trigger')
|
||||
fireEvent.click(trigger)
|
||||
|
||||
const credential1 = screen.getByText('Credential 1')
|
||||
fireEvent.click(credential1)
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith('cred-1')
|
||||
})
|
||||
expect(onCredentialChange).toHaveBeenCalledWith('credential-2')
|
||||
expect(trigger).toHaveAttribute('aria-expanded', 'false')
|
||||
})
|
||||
|
||||
// Side Effects and Cleanup - Test useEffect behavior
|
||||
describe('Side Effects and Cleanup', () => {
|
||||
it('should auto-select first credential when currentCredential is not found and credentials exist', () => {
|
||||
const mockOnChange = vi.fn()
|
||||
const props = createDefaultProps({
|
||||
currentCredentialId: 'non-existent-id',
|
||||
onCredentialChange: mockOnChange,
|
||||
})
|
||||
|
||||
render(<CredentialSelector {...props} />)
|
||||
|
||||
// Assert - Should auto-select first credential
|
||||
expect(mockOnChange).toHaveBeenCalledWith('cred-1')
|
||||
})
|
||||
|
||||
it('should not call onCredentialChange when currentCredential is found', () => {
|
||||
const mockOnChange = vi.fn()
|
||||
const props = createDefaultProps({
|
||||
currentCredentialId: 'cred-2',
|
||||
onCredentialChange: mockOnChange,
|
||||
})
|
||||
|
||||
render(<CredentialSelector {...props} />)
|
||||
|
||||
// Assert - Should not auto-select
|
||||
expect(mockOnChange).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not call onCredentialChange when credentials array is empty', () => {
|
||||
const mockOnChange = vi.fn()
|
||||
const props = createDefaultProps({
|
||||
currentCredentialId: 'cred-1',
|
||||
credentials: [],
|
||||
onCredentialChange: mockOnChange,
|
||||
})
|
||||
|
||||
render(<CredentialSelector {...props} />)
|
||||
|
||||
// Assert - Should not call since no credentials to select
|
||||
expect(mockOnChange).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should auto-select when credentials change and currentCredential becomes invalid', async () => {
|
||||
const mockOnChange = vi.fn()
|
||||
const initialCredentials = createMockCredentials(3)
|
||||
const props = createDefaultProps({
|
||||
currentCredentialId: 'cred-1',
|
||||
credentials: initialCredentials,
|
||||
onCredentialChange: mockOnChange,
|
||||
})
|
||||
|
||||
const { rerender } = render(<CredentialSelector {...props} />)
|
||||
expect(mockOnChange).not.toHaveBeenCalled()
|
||||
|
||||
// Act - Change credentials to not include current
|
||||
const newCredentials = [
|
||||
createMockCredential({ id: 'cred-4', name: 'New Credential 4' }),
|
||||
createMockCredential({ id: 'cred-5', name: 'New Credential 5' }),
|
||||
]
|
||||
rerender(<CredentialSelector {...props} credentials={newCredentials} />)
|
||||
|
||||
// Assert - Should auto-select first of new credentials
|
||||
await waitFor(() => {
|
||||
expect(mockOnChange).toHaveBeenCalledWith('cred-4')
|
||||
})
|
||||
})
|
||||
|
||||
it('should not trigger auto-select effect on every render with same props', () => {
|
||||
const mockOnChange = vi.fn()
|
||||
const props = createDefaultProps({ onCredentialChange: mockOnChange })
|
||||
|
||||
// Act - Render and rerender with same props
|
||||
const { rerender } = render(<CredentialSelector {...props} />)
|
||||
rerender(<CredentialSelector {...props} />)
|
||||
rerender(<CredentialSelector {...props} />)
|
||||
|
||||
// Assert - onCredentialChange should not be called for auto-selection
|
||||
expect(mockOnChange).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
// Callback Stability and Memoization - Test useCallback behavior
|
||||
describe('Callback Stability and Memoization', () => {
|
||||
it('should have stable handleCredentialChange callback', () => {
|
||||
const mockOnChange = vi.fn()
|
||||
const props = createDefaultProps({ onCredentialChange: mockOnChange })
|
||||
render(<CredentialSelector {...props} />)
|
||||
|
||||
// Act - Open dropdown and select
|
||||
const trigger = screen.getByTestId('popover-trigger')
|
||||
fireEvent.click(trigger)
|
||||
const credential = screen.getByText('Credential 2')
|
||||
fireEvent.click(credential)
|
||||
|
||||
// Assert - Callback should work correctly
|
||||
expect(mockOnChange).toHaveBeenCalledWith('cred-2')
|
||||
})
|
||||
|
||||
it('should update handleCredentialChange when onCredentialChange changes', () => {
|
||||
const mockOnChange1 = vi.fn()
|
||||
const mockOnChange2 = vi.fn()
|
||||
const props = createDefaultProps({ onCredentialChange: mockOnChange1 })
|
||||
|
||||
const { rerender } = render(<CredentialSelector {...props} />)
|
||||
|
||||
// Act - Update onCredentialChange prop
|
||||
rerender(<CredentialSelector {...props} onCredentialChange={mockOnChange2} />)
|
||||
|
||||
// Open and select
|
||||
const trigger = screen.getByTestId('popover-trigger')
|
||||
fireEvent.click(trigger)
|
||||
const credential = screen.getByText('Credential 2')
|
||||
fireEvent.click(credential)
|
||||
|
||||
// Assert - New callback should be used
|
||||
expect(mockOnChange1).not.toHaveBeenCalled()
|
||||
expect(mockOnChange2).toHaveBeenCalledWith('cred-2')
|
||||
})
|
||||
})
|
||||
|
||||
// Memoization Logic and Dependencies - Test useMemo behavior
|
||||
describe('Memoization Logic and Dependencies', () => {
|
||||
it('should find currentCredential by id', () => {
|
||||
const props = createDefaultProps({ currentCredentialId: 'cred-2' })
|
||||
|
||||
render(<CredentialSelector {...props} />)
|
||||
|
||||
// Assert - Should display credential 2
|
||||
// Assert - Should display credential 2
|
||||
expect(screen.getByText('Credential 2'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should update currentCredential when currentCredentialId changes', () => {
|
||||
const props = createDefaultProps({ currentCredentialId: 'cred-1' })
|
||||
const { rerender } = render(<CredentialSelector {...props} />)
|
||||
|
||||
// Assert initial
|
||||
// Assert initial
|
||||
expect(screen.getByText('Credential 1'))!.toBeInTheDocument()
|
||||
|
||||
// Act - Change currentCredentialId
|
||||
rerender(<CredentialSelector {...props} currentCredentialId="cred-3" />)
|
||||
|
||||
// Assert - Should now display credential 3
|
||||
// Assert - Should now display credential 3
|
||||
expect(screen.getByText('Credential 3'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should update currentCredential when credentials array changes', () => {
|
||||
const props = createDefaultProps({ currentCredentialId: 'cred-1' })
|
||||
const { rerender } = render(<CredentialSelector {...props} />)
|
||||
|
||||
// Assert initial
|
||||
// Assert initial
|
||||
expect(screen.getByText('Credential 1'))!.toBeInTheDocument()
|
||||
|
||||
// Act - Change credentials
|
||||
const newCredentials = [createMockCredential({ id: 'cred-1', name: 'Updated Credential 1' })]
|
||||
rerender(<CredentialSelector {...props} credentials={newCredentials} />)
|
||||
|
||||
// Assert - Should display updated name
|
||||
// Assert - Should display updated name
|
||||
expect(screen.getByText('Updated Credential 1'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should return undefined currentCredential when id not found', () => {
|
||||
const mockOnChange = vi.fn()
|
||||
const props = createDefaultProps({
|
||||
currentCredentialId: 'non-existent',
|
||||
onCredentialChange: mockOnChange,
|
||||
})
|
||||
|
||||
render(<CredentialSelector {...props} />)
|
||||
|
||||
// Assert - Should trigger auto-select effect
|
||||
expect(mockOnChange).toHaveBeenCalledWith('cred-1')
|
||||
})
|
||||
})
|
||||
|
||||
// Component Memoization - Test React.memo behavior
|
||||
describe('Component Memoization', () => {
|
||||
it('should not re-render when props remain the same', () => {
|
||||
const mockOnChange = vi.fn()
|
||||
const props = createDefaultProps({ onCredentialChange: mockOnChange })
|
||||
const renderSpy = vi.fn()
|
||||
|
||||
const TrackedCredentialSelector: React.FC<CredentialSelectorProps> = (trackedProps) => {
|
||||
renderSpy()
|
||||
return <CredentialSelector {...trackedProps} />
|
||||
}
|
||||
const MemoizedTracked = React.memo(TrackedCredentialSelector)
|
||||
|
||||
const { rerender } = render(<MemoizedTracked {...props} />)
|
||||
rerender(<MemoizedTracked {...props} />)
|
||||
|
||||
// Assert - Should only render once due to same props
|
||||
expect(renderSpy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should re-render when currentCredentialId changes', () => {
|
||||
const props = createDefaultProps({ currentCredentialId: 'cred-1' })
|
||||
const { rerender } = render(<CredentialSelector {...props} />)
|
||||
|
||||
// Assert initial
|
||||
// Assert initial
|
||||
expect(screen.getByText('Credential 1'))!.toBeInTheDocument()
|
||||
|
||||
rerender(<CredentialSelector {...props} currentCredentialId="cred-2" />)
|
||||
|
||||
expect(screen.getByText('Credential 2'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should re-render when credentials array reference changes', () => {
|
||||
const props = createDefaultProps()
|
||||
const { rerender } = render(<CredentialSelector {...props} />)
|
||||
|
||||
// Act - Create new credentials array with different data
|
||||
const newCredentials = [createMockCredential({ id: 'cred-1', name: 'New Name 1' })]
|
||||
rerender(<CredentialSelector {...props} credentials={newCredentials} />)
|
||||
|
||||
expect(screen.getByText('New Name 1'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should re-render when onCredentialChange reference changes', () => {
|
||||
const mockOnChange1 = vi.fn()
|
||||
const mockOnChange2 = vi.fn()
|
||||
const props = createDefaultProps({ onCredentialChange: mockOnChange1 })
|
||||
const { rerender } = render(<CredentialSelector {...props} />)
|
||||
|
||||
// Act - Change callback reference
|
||||
rerender(<CredentialSelector {...props} onCredentialChange={mockOnChange2} />)
|
||||
|
||||
// Open and select
|
||||
const trigger = screen.getByTestId('popover-trigger')
|
||||
fireEvent.click(trigger)
|
||||
const credential = screen.getByText('Credential 2')
|
||||
fireEvent.click(credential)
|
||||
|
||||
// Assert - New callback should be used
|
||||
expect(mockOnChange2).toHaveBeenCalledWith('cred-2')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases and Error Handling', () => {
|
||||
it('should handle undefined avatar_url in credential', () => {
|
||||
const credentialWithoutAvatar = createMockCredential({
|
||||
id: 'cred-no-avatar',
|
||||
name: 'No Avatar Credential',
|
||||
avatar_url: undefined,
|
||||
})
|
||||
const props = createDefaultProps({
|
||||
credentials: [credentialWithoutAvatar],
|
||||
currentCredentialId: 'cred-no-avatar',
|
||||
})
|
||||
|
||||
const { container } = render(<CredentialSelector {...props} />)
|
||||
|
||||
expect(screen.getByText('No Avatar Credential'))!.toBeInTheDocument()
|
||||
const iconImg = container.querySelector('img')
|
||||
expect(iconImg).not.toBeInTheDocument()
|
||||
expect(screen.getByText('N'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle very long credential name', () => {
|
||||
const longName = 'A'.repeat(200)
|
||||
const credentialWithLongName = createMockCredential({
|
||||
id: 'cred-long-name',
|
||||
name: longName,
|
||||
})
|
||||
const props = createDefaultProps({
|
||||
credentials: [credentialWithLongName],
|
||||
currentCredentialId: 'cred-long-name',
|
||||
})
|
||||
|
||||
render(<CredentialSelector {...props} />)
|
||||
|
||||
expect(screen.getByText(longName))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle special characters in credential name', () => {
|
||||
const specialName = '测试 Credential <script>alert("xss")</script> & "quoted"'
|
||||
const credentialWithSpecialName = createMockCredential({
|
||||
id: 'cred-special',
|
||||
name: specialName,
|
||||
})
|
||||
const props = createDefaultProps({
|
||||
credentials: [credentialWithSpecialName],
|
||||
currentCredentialId: 'cred-special',
|
||||
})
|
||||
|
||||
render(<CredentialSelector {...props} />)
|
||||
|
||||
expect(screen.getByText(specialName))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle numeric id as string', () => {
|
||||
const credentialWithNumericId = createMockCredential({
|
||||
id: '123456',
|
||||
name: 'Numeric ID Credential',
|
||||
})
|
||||
const props = createDefaultProps({
|
||||
credentials: [credentialWithNumericId],
|
||||
currentCredentialId: '123456',
|
||||
})
|
||||
|
||||
render(<CredentialSelector {...props} />)
|
||||
|
||||
expect(screen.getByText('Numeric ID Credential'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle large number of credentials', () => {
|
||||
const manyCredentials = createMockCredentials(100)
|
||||
const props = createDefaultProps({
|
||||
credentials: manyCredentials,
|
||||
currentCredentialId: 'cred-50',
|
||||
})
|
||||
|
||||
render(<CredentialSelector {...props} />)
|
||||
|
||||
expect(screen.getByText('Credential 50'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle credential selection with duplicate names', () => {
|
||||
const mockOnChange = vi.fn()
|
||||
const duplicateCredentials = [
|
||||
createMockCredential({ id: 'cred-1', name: 'Same Name' }),
|
||||
createMockCredential({ id: 'cred-2', name: 'Same Name' }),
|
||||
]
|
||||
const props = createDefaultProps({
|
||||
credentials: duplicateCredentials,
|
||||
currentCredentialId: 'cred-1',
|
||||
onCredentialChange: mockOnChange,
|
||||
})
|
||||
|
||||
render(<CredentialSelector {...props} />)
|
||||
const trigger = screen.getByTestId('popover-trigger')
|
||||
fireEvent.click(trigger)
|
||||
|
||||
// Get all "Same Name" elements
|
||||
// 1 in trigger (current) + 2 in dropdown (both credentials) = 3 total
|
||||
const sameNameElements = screen.getAllByText('Same Name')
|
||||
expect(sameNameElements.length).toBe(3)
|
||||
|
||||
fireEvent.click(sameNameElements[2]!)
|
||||
|
||||
// Assert - Should call with the correct id even with duplicate names
|
||||
expect(mockOnChange).toHaveBeenCalledWith('cred-2')
|
||||
})
|
||||
|
||||
it('ignores credential clicks after unmount', () => {
|
||||
const mockOnChange = vi.fn()
|
||||
const props = createDefaultProps({ onCredentialChange: mockOnChange })
|
||||
const { unmount } = render(<CredentialSelector {...props} />)
|
||||
|
||||
const trigger = screen.getByTestId('popover-trigger')
|
||||
fireEvent.click(trigger)
|
||||
|
||||
unmount()
|
||||
|
||||
// Assert - Should not throw
|
||||
expect(() => {
|
||||
// Any cleanup should have happened
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
// Styling and CSS Classes
|
||||
describe('Styling', () => {
|
||||
it('should configure dropdown placement through popover props', () => {
|
||||
const props = createDefaultProps()
|
||||
render(<CredentialSelector {...props} />)
|
||||
|
||||
const trigger = screen.getByTestId('popover-trigger')
|
||||
fireEvent.click(trigger)
|
||||
|
||||
const content = screen.getByTestId('popover-content')
|
||||
expect(content)!.toHaveAttribute('data-placement', 'bottom-start')
|
||||
expect(content)!.toHaveAttribute('data-side-offset', '4')
|
||||
expect(content)!.not.toHaveClass('z-10')
|
||||
})
|
||||
})
|
||||
|
||||
// Integration with Child Components
|
||||
describe('Integration with Child Components', () => {
|
||||
it('should pass currentCredential to Trigger component', () => {
|
||||
const props = createDefaultProps({ currentCredentialId: 'cred-2' })
|
||||
|
||||
render(<CredentialSelector {...props} />)
|
||||
|
||||
// Assert - Trigger should display the correct credential
|
||||
// Assert - Trigger should display the correct credential
|
||||
expect(screen.getByText('Credential 2'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should pass isOpen state to Trigger component', () => {
|
||||
const props = createDefaultProps()
|
||||
render(<CredentialSelector {...props} />)
|
||||
|
||||
// Assert - Initially closed
|
||||
const portalRoot = screen.getByTestId('popover')
|
||||
expect(portalRoot)!.toHaveAttribute('data-open', 'false')
|
||||
|
||||
// Act - Open
|
||||
const trigger = screen.getByTestId('popover-trigger')
|
||||
fireEvent.click(trigger)
|
||||
|
||||
// Assert - Now open
|
||||
// Assert - Now open
|
||||
expect(portalRoot)!.toHaveAttribute('data-open', 'true')
|
||||
})
|
||||
|
||||
it('should pass credentials to List component', () => {
|
||||
const props = createDefaultProps()
|
||||
render(<CredentialSelector {...props} />)
|
||||
|
||||
const trigger = screen.getByTestId('popover-trigger')
|
||||
fireEvent.click(trigger)
|
||||
|
||||
// Assert - All credentials should be rendered in list
|
||||
// 3 in dropdown + 1 in trigger (current credential appears twice) = 4 total
|
||||
const credentialNames = screen.getAllByText(/Credential \d/)
|
||||
expect(credentialNames.length).toBe(4)
|
||||
})
|
||||
|
||||
it('should pass currentCredentialId to List component', () => {
|
||||
const props = createDefaultProps({ currentCredentialId: 'cred-2' })
|
||||
render(<CredentialSelector {...props} />)
|
||||
|
||||
const trigger = screen.getByTestId('popover-trigger')
|
||||
fireEvent.click(trigger)
|
||||
|
||||
// Assert - Current credential (Credential 2) appears twice:
|
||||
// once in trigger and once in dropdown list
|
||||
const credential2Elements = screen.getAllByText('Credential 2')
|
||||
expect(credential2Elements.length).toBe(2)
|
||||
})
|
||||
|
||||
it('should pass handleCredentialChange to List component', () => {
|
||||
const mockOnChange = vi.fn()
|
||||
const props = createDefaultProps({ onCredentialChange: mockOnChange })
|
||||
render(<CredentialSelector {...props} />)
|
||||
|
||||
const trigger = screen.getByTestId('popover-trigger')
|
||||
fireEvent.click(trigger)
|
||||
const credential3 = screen.getByText('Credential 3')
|
||||
fireEvent.click(credential3)
|
||||
|
||||
// Assert - handleCredentialChange should propagate the call
|
||||
expect(mockOnChange).toHaveBeenCalledWith('cred-3')
|
||||
})
|
||||
})
|
||||
|
||||
// Popover Configuration
|
||||
describe('Popover Configuration', () => {
|
||||
it('should configure Popover with placement bottom-start', () => {
|
||||
// This test verifies the portal is configured correctly
|
||||
// The actual placement is handled by the mock, but we verify the component renders
|
||||
const props = createDefaultProps()
|
||||
render(<CredentialSelector {...props} />)
|
||||
|
||||
expect(screen.getByTestId('popover'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should configure Popover with offset mainAxis 4', () => {
|
||||
// This test verifies the offset configuration doesn't break rendering
|
||||
const props = createDefaultProps()
|
||||
render(<CredentialSelector {...props} />)
|
||||
|
||||
expect(screen.getByTestId('popover'))!.toBeInTheDocument()
|
||||
})
|
||||
it('reflects an updated controlled credential', () => {
|
||||
const onCredentialChange = vi.fn()
|
||||
const { rerender } = render(
|
||||
<CredentialSelector
|
||||
currentCredentialId="credential-1"
|
||||
credentials={credentials}
|
||||
onCredentialChange={onCredentialChange}
|
||||
/>,
|
||||
)
|
||||
|
||||
rerender(
|
||||
<CredentialSelector
|
||||
currentCredentialId="credential-2"
|
||||
credentials={credentials}
|
||||
onCredentialChange={onCredentialChange}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('button', { name: /Second credential/ })).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -2,24 +2,6 @@ import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import Header from '../header'
|
||||
|
||||
vi.mock('@langgenius/dify-ui/button', () => ({
|
||||
Button: ({
|
||||
children,
|
||||
onClick,
|
||||
disabled,
|
||||
variant,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
onClick: () => void
|
||||
disabled?: boolean
|
||||
variant: string
|
||||
}) => (
|
||||
<button data-testid={`btn-${variant}`} onClick={onClick} disabled={disabled}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
|
||||
describe('Header', () => {
|
||||
const defaultProps = {
|
||||
onReset: vi.fn(),
|
||||
@ -41,29 +23,35 @@ describe('Header', () => {
|
||||
|
||||
it('should render reset and preview buttons', () => {
|
||||
render(<Header {...defaultProps} />)
|
||||
expect(screen.getByTestId('btn-ghost')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('btn-secondary-accent')).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'common.operation.reset' })).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByRole('button', { name: 'datasetPipeline.addDocuments.stepTwo.previewChunks' }),
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call onReset when reset clicked', () => {
|
||||
render(<Header {...defaultProps} />)
|
||||
fireEvent.click(screen.getByTestId('btn-ghost'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.reset' }))
|
||||
expect(defaultProps.onReset).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call onPreview when preview clicked', () => {
|
||||
render(<Header {...defaultProps} />)
|
||||
fireEvent.click(screen.getByTestId('btn-secondary-accent'))
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', { name: 'datasetPipeline.addDocuments.stepTwo.previewChunks' }),
|
||||
)
|
||||
expect(defaultProps.onPreview).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should disable reset button when resetDisabled is true', () => {
|
||||
render(<Header {...defaultProps} resetDisabled={true} />)
|
||||
expect(screen.getByTestId('btn-ghost')).toBeDisabled()
|
||||
expect(screen.getByRole('button', { name: 'common.operation.reset' })).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should disable preview button when previewDisabled is true', () => {
|
||||
render(<Header {...defaultProps} previewDisabled={true} />)
|
||||
expect(screen.getByTestId('btn-secondary-accent')).toBeDisabled()
|
||||
expect(
|
||||
screen.getByRole('button', { name: 'datasetPipeline.addDocuments.stepTwo.previewChunks' }),
|
||||
).toBeDisabled()
|
||||
})
|
||||
})
|
||||
|
||||
@ -248,35 +248,6 @@ vi.mock('@/app/components/base/divider', () => ({
|
||||
default: () => <hr data-testid="divider" />,
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/pagination', () => ({
|
||||
Pagination: ({
|
||||
page,
|
||||
totalPages,
|
||||
onPageChange,
|
||||
pageSize,
|
||||
}: {
|
||||
page: number
|
||||
totalPages: number
|
||||
onPageChange: (page: number) => void
|
||||
pageSize?: {
|
||||
onValueChange: (limit: number) => void
|
||||
}
|
||||
}) => (
|
||||
<div data-testid="pagination">
|
||||
<span data-testid="current-page">{page - 1}</span>
|
||||
<span data-testid="total-pages">{totalPages}</span>
|
||||
<button data-testid="next-page" onClick={() => onPageChange(page + 1)}>
|
||||
Next
|
||||
</button>
|
||||
{pageSize && (
|
||||
<button data-testid="change-limit" onClick={() => pageSize.onValueChange(20)}>
|
||||
Change Limit
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
const createMockSegmentDetail = (
|
||||
overrides: Partial<SegmentDetailModel> = {},
|
||||
): SegmentDetailModel => ({
|
||||
@ -500,7 +471,7 @@ describe('Completed Component', () => {
|
||||
it('should render Pagination component', () => {
|
||||
render(<Completed {...defaultProps} />, { wrapper: createWrapper() })
|
||||
|
||||
expect(screen.getByTestId('pagination'))!.toBeInTheDocument()
|
||||
expect(screen.getByRole('navigation', { name: 'Pagination' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render Divider component', () => {
|
||||
@ -525,32 +496,35 @@ describe('Completed Component', () => {
|
||||
})
|
||||
|
||||
describe('Pagination', () => {
|
||||
it('should start with page 0 (current - 1)', () => {
|
||||
it('should start on the first page', () => {
|
||||
render(<Completed {...defaultProps} />, { wrapper: createWrapper() })
|
||||
|
||||
expect(screen.getByTestId('current-page'))!.toHaveTextContent('0')
|
||||
expect(screen.getByRole('navigation', { name: 'Pagination' })).toHaveAttribute(
|
||||
'data-page',
|
||||
'1',
|
||||
)
|
||||
})
|
||||
|
||||
it('should update page when pagination changes', async () => {
|
||||
mockSegmentListData.total = 30
|
||||
render(<Completed {...defaultProps} />, { wrapper: createWrapper() })
|
||||
|
||||
const nextPageButton = screen.getByTestId('next-page')
|
||||
const nextPageButton = screen.getByRole('button', { name: 'common.pagination.next' })
|
||||
fireEvent.click(nextPageButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('current-page'))!.toHaveTextContent('1')
|
||||
expect(screen.getByRole('navigation', { name: 'Pagination' })).toHaveAttribute(
|
||||
'data-page',
|
||||
'2',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('should update limit when limit changes', async () => {
|
||||
it('should expose page-size controls', () => {
|
||||
render(<Completed {...defaultProps} />, { wrapper: createWrapper() })
|
||||
|
||||
const changeLimitButton = screen.getByTestId('change-limit')
|
||||
fireEvent.click(changeLimitButton)
|
||||
|
||||
// Limit change is handled internally
|
||||
// Limit change is handled internally
|
||||
expect(changeLimitButton)!.toBeInTheDocument()
|
||||
expect(screen.getByRole('group', { name: 'common.pagination.perPage' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: '10' })).toHaveAttribute('aria-pressed', 'true')
|
||||
})
|
||||
})
|
||||
|
||||
@ -651,7 +625,7 @@ describe('Edge Cases', () => {
|
||||
|
||||
const { unmount } = render(<Completed {...defaultProps} />, { wrapper: createWrapper() })
|
||||
|
||||
expect(screen.getByTestId('pagination'))!.toBeInTheDocument()
|
||||
expect(screen.getByRole('navigation', { name: 'Pagination' })).toBeInTheDocument()
|
||||
|
||||
unmount()
|
||||
})
|
||||
@ -667,7 +641,7 @@ describe('Edge Cases', () => {
|
||||
|
||||
const { unmount } = render(<Completed {...defaultProps} />, { wrapper: createWrapper() })
|
||||
|
||||
expect(screen.getByTestId('pagination'))!.toBeInTheDocument()
|
||||
expect(screen.getByRole('navigation', { name: 'Pagination' })).toBeInTheDocument()
|
||||
|
||||
unmount()
|
||||
})
|
||||
@ -696,7 +670,7 @@ describe('Integration Tests', () => {
|
||||
// All components should render without errors
|
||||
expect(screen.getByTestId('menu-bar'))!.toBeInTheDocument()
|
||||
expect(screen.getByTestId('general-mode-content'))!.toBeInTheDocument()
|
||||
expect(screen.getByTestId('pagination'))!.toBeInTheDocument()
|
||||
expect(screen.getByRole('navigation', { name: 'Pagination' })).toBeInTheDocument()
|
||||
expect(screen.getByTestId('drawer-group'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
@ -1103,17 +1077,24 @@ describe('Inline callback and hook initialization coverage', () => {
|
||||
|
||||
// Covers lines 56-58: useSearchFilter({ onPageChange: setCurrentPage })
|
||||
it('should reset current page when status filter changes', async () => {
|
||||
mockSegmentListData.total = 30
|
||||
render(<Completed {...defaultProps} />, { wrapper: createWrapper() })
|
||||
|
||||
fireEvent.click(screen.getByTestId('next-page'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.pagination.next' }))
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('current-page'))!.toHaveTextContent('1')
|
||||
expect(screen.getByRole('navigation', { name: 'Pagination' })).toHaveAttribute(
|
||||
'data-page',
|
||||
'2',
|
||||
)
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByTestId('status-enabled'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('current-page'))!.toHaveTextContent('0')
|
||||
expect(screen.getByRole('navigation', { name: 'Pagination' })).toHaveAttribute(
|
||||
'data-page',
|
||||
'1',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@ -1228,16 +1209,23 @@ describe('Inline callback and hook initialization coverage', () => {
|
||||
|
||||
// Covers line 133-135: handlePageChange
|
||||
it('should handle multiple page changes', async () => {
|
||||
mockSegmentListData.total = 30
|
||||
render(<Completed {...defaultProps} />, { wrapper: createWrapper() })
|
||||
|
||||
fireEvent.click(screen.getByTestId('next-page'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.pagination.next' }))
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('current-page'))!.toHaveTextContent('1')
|
||||
expect(screen.getByRole('navigation', { name: 'Pagination' })).toHaveAttribute(
|
||||
'data-page',
|
||||
'2',
|
||||
)
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByTestId('next-page'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.pagination.next' }))
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('current-page'))!.toHaveTextContent('2')
|
||||
expect(screen.getByRole('navigation', { name: 'Pagination' })).toHaveAttribute(
|
||||
'data-page',
|
||||
'3',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@ -1248,7 +1236,10 @@ describe('Inline callback and hook initialization coverage', () => {
|
||||
|
||||
render(<Completed {...defaultProps} />, { wrapper: createWrapper() })
|
||||
|
||||
expect(screen.getByTestId('total-pages'))!.toHaveTextContent('5')
|
||||
expect(screen.getByRole('navigation', { name: 'Pagination' })).toHaveAttribute(
|
||||
'data-totalpages',
|
||||
'5',
|
||||
)
|
||||
})
|
||||
|
||||
// Covers search input change
|
||||
|
||||
@ -1,25 +1,27 @@
|
||||
import { Popover } from '@langgenius/dify-ui/popover'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import Card from '../card'
|
||||
|
||||
vi.mock('@langgenius/dify-ui/popover', () => ({
|
||||
PopoverClose: ({ render }: { render: React.ReactNode }) => render,
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-api-access-url', () => ({
|
||||
useDatasetApiAccessUrl: () => 'https://docs.dify.ai/api-reference/datasets',
|
||||
}))
|
||||
|
||||
describe('Service API card', () => {
|
||||
it('shows the service endpoint and API reference', () => {
|
||||
const renderCard = (props: React.ComponentProps<typeof Card>) =>
|
||||
render(
|
||||
<Card
|
||||
apiBaseUrl="https://api.example.com"
|
||||
canManageSecretKey
|
||||
onOpenSecretKeyModal={vi.fn()}
|
||||
/>,
|
||||
<Popover>
|
||||
<Card {...props} />
|
||||
</Popover>,
|
||||
)
|
||||
|
||||
it('shows the service endpoint and API reference', () => {
|
||||
renderCard({
|
||||
apiBaseUrl: 'https://api.example.com',
|
||||
canManageSecretKey: true,
|
||||
onOpenSecretKeyModal: vi.fn(),
|
||||
})
|
||||
|
||||
expect(screen.getByText('https://api.example.com')).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByRole('link', { name: 'dataset.serviceApi.card.apiReference' }),
|
||||
@ -29,13 +31,11 @@ describe('Service API card', () => {
|
||||
it('opens secret-key management when allowed', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onOpenSecretKeyModal = vi.fn()
|
||||
render(
|
||||
<Card
|
||||
apiBaseUrl="https://api.example.com"
|
||||
canManageSecretKey
|
||||
onOpenSecretKeyModal={onOpenSecretKeyModal}
|
||||
/>,
|
||||
)
|
||||
renderCard({
|
||||
apiBaseUrl: 'https://api.example.com',
|
||||
canManageSecretKey: true,
|
||||
onOpenSecretKeyModal,
|
||||
})
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'dataset.serviceApi.card.apiKey' }))
|
||||
|
||||
@ -43,7 +43,7 @@ describe('Service API card', () => {
|
||||
})
|
||||
|
||||
it('disables secret-key management when it is not allowed', () => {
|
||||
render(<Card apiBaseUrl="https://api.example.com" onOpenSecretKeyModal={vi.fn()} />)
|
||||
renderCard({ apiBaseUrl: 'https://api.example.com', onOpenSecretKeyModal: vi.fn() })
|
||||
|
||||
expect(screen.getByRole('button', { name: 'dataset.serviceApi.card.apiKey' })).toBeDisabled()
|
||||
})
|
||||
|
||||
@ -9,29 +9,6 @@ import { render } from '@/test/console/render'
|
||||
import { RETRIEVE_METHOD } from '@/types/app'
|
||||
import HitTestingPage from '../index'
|
||||
|
||||
vi.mock('@langgenius/dify-ui/pagination', () => ({
|
||||
Pagination: ({
|
||||
page,
|
||||
totalPages,
|
||||
onPageChange,
|
||||
labels,
|
||||
}: {
|
||||
page: number
|
||||
totalPages: number
|
||||
onPageChange: (page: number) => void
|
||||
labels: { next: string }
|
||||
}) => (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={labels.next}
|
||||
disabled={page >= totalPages}
|
||||
onClick={() => onPageChange(page + 1)}
|
||||
>
|
||||
{page}/{totalPages}
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/datasets/common/retrieval-method-config', () => ({
|
||||
default: ({
|
||||
value,
|
||||
|
||||
@ -10,22 +10,6 @@ vi.mock('@/app/components/base/action-button', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/button', () => ({
|
||||
Button: ({
|
||||
children,
|
||||
onClick,
|
||||
variant,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
onClick: () => void
|
||||
variant?: string
|
||||
}) => (
|
||||
<button data-testid={variant === 'primary' ? 'save-button' : 'cancel-button'} onClick={onClick}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../../external-knowledge-base/create/RetrievalSettings', () => ({
|
||||
default: ({
|
||||
topK,
|
||||
@ -89,13 +73,13 @@ describe('ModifyExternalRetrievalModal', () => {
|
||||
|
||||
it('should call onClose when cancel button clicked', () => {
|
||||
render(<ModifyExternalRetrievalModal {...defaultProps} />)
|
||||
fireEvent.click(screen.getByTestId('cancel-button'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.cancel' }))
|
||||
expect(defaultProps.onClose).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call onSave with current values and close when save clicked', () => {
|
||||
render(<ModifyExternalRetrievalModal {...defaultProps} />)
|
||||
fireEvent.click(screen.getByTestId('save-button'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' }))
|
||||
expect(defaultProps.onSave).toHaveBeenCalledWith({
|
||||
top_k: 4,
|
||||
score_threshold: 0.5,
|
||||
@ -107,14 +91,14 @@ describe('ModifyExternalRetrievalModal', () => {
|
||||
it('should save updated values after settings change', () => {
|
||||
render(<ModifyExternalRetrievalModal {...defaultProps} />)
|
||||
fireEvent.click(screen.getByTestId('change-top-k'))
|
||||
fireEvent.click(screen.getByTestId('save-button'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' }))
|
||||
expect(defaultProps.onSave).toHaveBeenCalledWith(expect.objectContaining({ top_k: 10 }))
|
||||
})
|
||||
|
||||
it('should save updated score threshold', () => {
|
||||
render(<ModifyExternalRetrievalModal {...defaultProps} />)
|
||||
fireEvent.click(screen.getByTestId('change-score'))
|
||||
fireEvent.click(screen.getByTestId('save-button'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' }))
|
||||
expect(defaultProps.onSave).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ score_threshold: 0.9 }),
|
||||
)
|
||||
@ -123,7 +107,7 @@ describe('ModifyExternalRetrievalModal', () => {
|
||||
it('should save updated score threshold enabled', () => {
|
||||
render(<ModifyExternalRetrievalModal {...defaultProps} />)
|
||||
fireEvent.click(screen.getByTestId('change-enabled'))
|
||||
fireEvent.click(screen.getByTestId('save-button'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' }))
|
||||
expect(defaultProps.onSave).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ score_threshold_enabled: true }),
|
||||
)
|
||||
@ -133,7 +117,7 @@ describe('ModifyExternalRetrievalModal', () => {
|
||||
render(<ModifyExternalRetrievalModal {...defaultProps} />)
|
||||
fireEvent.click(screen.getByTestId('change-top-k'))
|
||||
fireEvent.click(screen.getByTestId('change-score'))
|
||||
fireEvent.click(screen.getByTestId('save-button'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' }))
|
||||
expect(defaultProps.onSave).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ top_k: 10, score_threshold: 0.9 }),
|
||||
)
|
||||
|
||||
@ -22,17 +22,6 @@ vi.mock('@langgenius/dify-ui/toast', () => ({
|
||||
toast: mockToast,
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/button', () => ({
|
||||
Button: ({
|
||||
children,
|
||||
onClick,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
onClick: () => void
|
||||
variant?: string
|
||||
}) => <button onClick={onClick}>{children}</button>,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/datasets/common/check-rerank-model', () => ({
|
||||
isReRankModelSelected: vi.fn(() => true),
|
||||
}))
|
||||
|
||||
@ -1,49 +1,8 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { STEP_BY_STEP_TOUR_TARGETS } from '@/app/components/step-by-step-tour/target-registry'
|
||||
import DatasetListHeader from '../header'
|
||||
|
||||
vi.mock('@langgenius/dify-ui/button', () => ({
|
||||
Button: ({ children, className, ...props }: React.ButtonHTMLAttributes<HTMLButtonElement>) => (
|
||||
<button type="button" className={className} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/dropdown-menu', () => ({
|
||||
DropdownMenu: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
DropdownMenuContent: ({
|
||||
children,
|
||||
popupProps,
|
||||
positionerProps,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
popupProps?: React.HTMLAttributes<HTMLDivElement>
|
||||
positionerProps?: React.HTMLAttributes<HTMLDivElement>
|
||||
}) => (
|
||||
<div {...positionerProps}>
|
||||
<div role="menu" {...popupProps}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
DropdownMenuItem: ({
|
||||
children,
|
||||
className,
|
||||
onClick,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
onClick?: () => void
|
||||
}) => (
|
||||
<button type="button" role="menuitem" className={className} onClick={onClick}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
DropdownMenuSeparator: () => <hr />,
|
||||
DropdownMenuTrigger: ({ render }: { render: React.ReactNode }) => render,
|
||||
}))
|
||||
|
||||
vi.mock('@/features/tag-management/components/tag-filter', () => ({
|
||||
TagFilter: () => <div />,
|
||||
}))
|
||||
@ -79,21 +38,26 @@ describe('DatasetListHeader', () => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('uses the updated create menu labels and pipeline icon', () => {
|
||||
it('shows dataset and pipeline creation actions in the create menu', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<DatasetListHeader {...defaultProps} />)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /common\.operation\.create/ }))
|
||||
|
||||
expect(
|
||||
screen.getByRole('menuitem', { name: /dataset\.firstEmpty\.createTitle/ }),
|
||||
).toBeInTheDocument()
|
||||
|
||||
const menuItem = screen.getByRole('menuitem', { name: /dataset\.firstEmpty\.pipelineTitle/ })
|
||||
|
||||
expect(menuItem.querySelector('.i-custom-vender-pipeline-pipeline-line')).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByRole('menuitem', { name: /dataset\.firstEmpty\.pipelineTitle/ }),
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('hides dataset creation actions without create permission', () => {
|
||||
it('only shows external dataset connection without create permission', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<DatasetListHeader {...defaultProps} canCreateDataset={false} />)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /common\.operation\.create/ }))
|
||||
|
||||
expect(
|
||||
screen.queryByRole('menuitem', { name: /dataset\.firstEmpty\.createTitle/ }),
|
||||
).not.toBeInTheDocument()
|
||||
|
||||
@ -2,91 +2,6 @@ import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import ItemOperation from '../index'
|
||||
|
||||
vi.mock('@langgenius/dify-ui/dropdown-menu', () => {
|
||||
const DropdownMenuContext = React.createContext<{
|
||||
isOpen: boolean
|
||||
setOpen: (open: boolean) => void
|
||||
} | null>(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, modal }: { children: React.ReactNode; modal?: boolean }) => {
|
||||
const [isOpen, setIsOpen] = React.useState(false)
|
||||
|
||||
return (
|
||||
<DropdownMenuContext value={{ isOpen, setOpen: setIsOpen }}>
|
||||
<div data-modal={modal} data-open={isOpen} data-testid="dropdown-menu">
|
||||
{children}
|
||||
</div>
|
||||
</DropdownMenuContext>
|
||||
)
|
||||
},
|
||||
DropdownMenuTrigger: ({
|
||||
children,
|
||||
onClick,
|
||||
...props
|
||||
}: React.ButtonHTMLAttributes<HTMLButtonElement>) => {
|
||||
const { isOpen, setOpen } = useDropdownMenuContext()
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
onClick?.(e)
|
||||
setOpen(!isOpen)
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
},
|
||||
DropdownMenuContent: ({
|
||||
children,
|
||||
popupProps,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
popupProps?: React.HTMLAttributes<HTMLDivElement>
|
||||
}) => {
|
||||
const { isOpen } = useDropdownMenuContext()
|
||||
if (!isOpen) return null
|
||||
|
||||
return (
|
||||
<div data-testid="dropdown-content" {...popupProps}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
DropdownMenuItem: ({
|
||||
children,
|
||||
onClick,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
onClick?: React.MouseEventHandler<HTMLButtonElement>
|
||||
className?: string
|
||||
}) => {
|
||||
const { setOpen } = useDropdownMenuContext()
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={className}
|
||||
onClick={(e) => {
|
||||
onClick?.(e)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
describe('ItemOperation', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@ -179,12 +94,6 @@ describe('ItemOperation', () => {
|
||||
expect(screen.getByText('explore.sidebar.action.pin')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render a non-modal menu', () => {
|
||||
renderComponent()
|
||||
|
||||
expect(screen.getByTestId('dropdown-menu')).toHaveAttribute('data-modal', 'false')
|
||||
})
|
||||
|
||||
it('should stop propagation when clicking menu actions', async () => {
|
||||
const onParentClick = vi.fn()
|
||||
const togglePin = vi.fn()
|
||||
|
||||
@ -1,42 +0,0 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { render } from '@testing-library/react'
|
||||
import MenuDialog from '../menu-dialog'
|
||||
|
||||
type DialogProps = {
|
||||
children: ReactNode
|
||||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
}
|
||||
|
||||
let latestOnOpenChange: DialogProps['onOpenChange']
|
||||
|
||||
vi.mock('@langgenius/dify-ui/dialog', () => ({
|
||||
Dialog: ({ children, onOpenChange }: DialogProps) => {
|
||||
latestOnOpenChange = onOpenChange
|
||||
return <div data-testid="dialog">{children}</div>
|
||||
},
|
||||
DialogContent: ({ children, className }: { children: ReactNode; className?: string }) => (
|
||||
<div className={className}>{children}</div>
|
||||
),
|
||||
}))
|
||||
|
||||
describe('MenuDialog dialog lifecycle', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
latestOnOpenChange = undefined
|
||||
})
|
||||
|
||||
it('should only call onClose when the dialog requests closing', () => {
|
||||
const onClose = vi.fn()
|
||||
render(
|
||||
<MenuDialog show={true} onClose={onClose}>
|
||||
<div>Content</div>
|
||||
</MenuDialog>,
|
||||
)
|
||||
|
||||
latestOnOpenChange?.(true)
|
||||
latestOnOpenChange?.(false)
|
||||
|
||||
expect(onClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
@ -48,10 +48,6 @@ vi.mock('react-i18next', async () => {
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/app/components/base/date-and-time-picker/time-picker', () => ({
|
||||
default: () => <div data-testid="time-picker" />,
|
||||
}))
|
||||
|
||||
vi.mock(
|
||||
'@/app/components/plugins/reference-setting-modal/auto-update-setting/plugins-picker',
|
||||
() => ({
|
||||
|
||||
@ -50,8 +50,6 @@ vi.mock('@tanstack/react-query', () => ({
|
||||
})),
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/popover', async () => await import('@/__mocks__/base-ui-popover'))
|
||||
|
||||
describe('ApiBasedExtensionSelector', () => {
|
||||
const mockOnChange = vi.fn()
|
||||
|
||||
@ -93,18 +91,12 @@ describe('ApiBasedExtensionSelector', () => {
|
||||
it('should open dropdown when clicked', async () => {
|
||||
// Act
|
||||
render(<ApiBasedExtensionSelector value="" onChange={mockOnChange} />)
|
||||
const popoverTrigger = screen.getByTestId('popover-trigger')
|
||||
const trigger = screen.getByText('common.apiBasedExtension.selector.placeholder')
|
||||
const arrow = trigger.parentElement?.querySelector('[aria-hidden="true"]')
|
||||
expect(popoverTrigger).not.toHaveAttribute('data-popup-open')
|
||||
expect(arrow).toHaveClass('opacity-60')
|
||||
|
||||
fireEvent.click(trigger)
|
||||
|
||||
// Assert
|
||||
// Assert
|
||||
expect(popoverTrigger).toHaveAttribute('data-popup-open', '')
|
||||
expect(arrow).not.toHaveClass('opacity-60')
|
||||
expect(
|
||||
await screen.findByText('common.apiBasedExtension.selector.title'),
|
||||
)!.toBeInTheDocument()
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import type { ButtonHTMLAttributes, ReactNode } from 'react'
|
||||
import type { DataSourceAuth } from '../types'
|
||||
import type { FormSchema } from '@/app/components/base/form/types'
|
||||
import type {
|
||||
@ -11,16 +10,6 @@ import { FormTypeEnum } from '@/app/components/base/form/types'
|
||||
import { AuthCategory } from '@/app/components/plugins/plugin-auth/types'
|
||||
import Configure from '../configure'
|
||||
|
||||
vi.mock('@langgenius/dify-ui/popover', () => import('@/__mocks__/base-ui-popover'))
|
||||
vi.mock('@langgenius/dify-ui/button', () => ({
|
||||
Button: ({
|
||||
children,
|
||||
...props
|
||||
}: ButtonHTMLAttributes<HTMLButtonElement> & { children?: ReactNode }) => (
|
||||
<button {...props}>{children}</button>
|
||||
),
|
||||
}))
|
||||
|
||||
/**
|
||||
* Configure Component Tests
|
||||
* Using Unit approach to ensure 100% coverage and stable tests.
|
||||
|
||||
@ -26,6 +26,10 @@ vi.mock('@/app/components/plugins/readme-panel/store', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-plugins', () => ({
|
||||
useVersionListOfPlugin: () => ({ data: { data: { versions: [] } } }),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/plugins/plugin-detail-panel/detail-header/hooks', () => ({
|
||||
useDetailHeaderState: () => ({
|
||||
modalStates: {
|
||||
@ -60,24 +64,6 @@ vi.mock('@/app/components/base/badge', () => ({
|
||||
default: ({ text }: { text: ReactNode }) => <div data-testid="badge">{text}</div>,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/plugins/update-plugin/plugin-version-picker', () => ({
|
||||
__esModule: true,
|
||||
default: ({ trigger }: { trigger: ReactNode }) => <div>{trigger}</div>,
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/button', () => ({
|
||||
Button: ({ children, onClick }: { children: ReactNode; onClick?: () => void }) => (
|
||||
<button type="button" onClick={onClick}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/tooltip', () => ({
|
||||
Tooltip: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
TooltipTrigger: ({ render }: { render: ReactNode }) => <>{render}</>,
|
||||
TooltipContent: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
}))
|
||||
vi.mock('@/hooks/use-theme', () => ({
|
||||
default: () => ({ theme: 'light' }),
|
||||
}))
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { PluginDeclaration, PluginDetail } from '@/app/components/plugins/types'
|
||||
import { act, fireEvent, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
@ -90,6 +89,10 @@ const saveUpdateSettings = () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' }))
|
||||
}
|
||||
|
||||
const openUpdateSettings = () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: /plugin\.autoUpdate\.autoUpdate/ }))
|
||||
}
|
||||
|
||||
const createPluginDeclaration = (
|
||||
overrides: Partial<PluginDeclaration> = {},
|
||||
): PluginDeclaration => ({
|
||||
@ -279,66 +282,11 @@ vi.mock('@/service/use-plugins', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/dialog', () => ({
|
||||
Dialog: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
DialogTrigger: ({ render }: { render: ReactNode }) => render,
|
||||
DialogContent: ({ children }: { children: ReactNode }) => (
|
||||
<div data-testid="update-setting-dialog">{children}</div>
|
||||
),
|
||||
DialogTitle: () => null,
|
||||
DialogCloseButton: () => <button type="button" aria-label="close" />,
|
||||
}))
|
||||
|
||||
vi.mock('nuqs', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('nuqs')>()
|
||||
return { ...actual, useQueryState: () => [null, mockSetSettingsDestination] }
|
||||
})
|
||||
|
||||
vi.mock('@/app/components/base/date-and-time-picker/time-picker', () => ({
|
||||
default: ({
|
||||
value,
|
||||
onChange,
|
||||
renderTrigger,
|
||||
}: {
|
||||
value?: string | { format: (format: string) => string }
|
||||
onChange: (value: { hour: () => number; minute: () => number }) => void
|
||||
renderTrigger: (
|
||||
props: Record<string, never>,
|
||||
state: { open: boolean },
|
||||
params: {
|
||||
inputElem: ReactNode
|
||||
onClick: () => void
|
||||
},
|
||||
) => ReactNode
|
||||
}) => {
|
||||
const displayValue = typeof value === 'string' ? value : value?.format('HH:mm')
|
||||
|
||||
return (
|
||||
<div data-testid="update-time-picker">
|
||||
{renderTrigger(
|
||||
{},
|
||||
{ open: false },
|
||||
{
|
||||
inputElem: <span data-testid="update-time-value">{displayValue}</span>,
|
||||
onClick: vi.fn(),
|
||||
},
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
onChange({
|
||||
hour: () => 1,
|
||||
minute: () => 15,
|
||||
})
|
||||
}
|
||||
>
|
||||
set update time
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/service/client', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/service/client')>()
|
||||
const originalWorkspaces = actual.consoleQuery.workspaces
|
||||
@ -485,13 +433,12 @@ describe('ModelProviderPage', () => {
|
||||
renderModelProviderPage()
|
||||
|
||||
expect(screen.getAllByText('plugin.autoUpdate.strategy.latest.name')[0]).toBeInTheDocument()
|
||||
expect(screen.getAllByTestId('update-setting-dialog')[0]).toBeInTheDocument()
|
||||
openUpdateSettings()
|
||||
expect(
|
||||
screen.getByRole('radiogroup', { name: 'plugin.autoUpdate.autoUpdate' }),
|
||||
).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.autoUpdate.scope')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.autoUpdate.updateTime')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('update-time-picker')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.autoUpdate.changeTimezone')).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByRole('radio', { name: 'plugin.autoUpdate.strategy.fixOnly.name' }),
|
||||
@ -506,10 +453,10 @@ describe('ModelProviderPage', () => {
|
||||
const updateSettingButton = screen.getByText('plugin.autoUpdate.autoUpdate').closest('button')
|
||||
expect(updateSettingButton).not.toBeDisabled()
|
||||
expect(screen.queryByText('plugin.autoUpdate.strategy.latest.name')).not.toBeInTheDocument()
|
||||
openUpdateSettings()
|
||||
expect(screen.getByRole('status')).toHaveTextContent('common.loading')
|
||||
expect(screen.queryByRole('button', { name: 'common.operation.save' })).not.toBeInTheDocument()
|
||||
expect(mockSaveAutoUpgrade).not.toHaveBeenCalled()
|
||||
expect(screen.getByTestId('update-setting-dialog')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render a failure state when backend auto-upgrade data fails', () => {
|
||||
@ -518,6 +465,7 @@ describe('ModelProviderPage', () => {
|
||||
|
||||
renderModelProviderPage()
|
||||
|
||||
openUpdateSettings()
|
||||
expect(screen.getByText('common.api.actionFailed')).toBeInTheDocument()
|
||||
expect(
|
||||
screen.queryByRole('radiogroup', { name: 'plugin.autoUpdate.autoUpdate' }),
|
||||
@ -529,6 +477,7 @@ describe('ModelProviderPage', () => {
|
||||
it('should update scope from the dialog while keeping the backend returned strategy', () => {
|
||||
renderModelProviderPage()
|
||||
|
||||
openUpdateSettings()
|
||||
fireEvent.click(screen.getByRole('radio', { name: 'plugin.autoUpdate.upgradeMode.partial' }))
|
||||
saveUpdateSettings()
|
||||
|
||||
@ -544,11 +493,13 @@ describe('ModelProviderPage', () => {
|
||||
it('should update time from the popover while keeping the model provider default strategy as latest', () => {
|
||||
renderModelProviderPage()
|
||||
|
||||
expect(screen.getByTestId('update-time-value')).toHaveTextContent('00:00')
|
||||
openUpdateSettings()
|
||||
fireEvent.click(screen.getByDisplayValue('12:00 AM'))
|
||||
fireEvent.click(screen.getByRole('button', { name: '01' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: '15' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'time.operation.ok' }))
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'set update time' }))
|
||||
|
||||
expect(screen.getByTestId('update-time-value')).toHaveTextContent('01:15')
|
||||
expect(screen.getByDisplayValue('01:15 AM')).toBeInTheDocument()
|
||||
|
||||
saveUpdateSettings()
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { ModelProvider } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { fireEvent, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { ConfigurationMethodEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { render } from '@/test/console/render'
|
||||
import AddCustomModel from '../add-custom-model'
|
||||
@ -51,17 +52,13 @@ vi.mock('@remixicon/react', () => ({
|
||||
RiAddLine: () => <div data-testid="add-line-icon" />,
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/tooltip', () => ({
|
||||
Tooltip: ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="tooltip-mock">{children}</div>
|
||||
),
|
||||
TooltipTrigger: ({ render }: { render: React.ReactNode }) => <>{render}</>,
|
||||
TooltipContent: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/popover', async () => await import('@/__mocks__/base-ui-popover'))
|
||||
|
||||
describe('AddCustomModel', () => {
|
||||
const getAddModelTrigger = () =>
|
||||
screen
|
||||
.getAllByRole('button', { name: /modelProvider.addModel/i })
|
||||
.find((element) => element.getAttribute('aria-haspopup') === 'dialog') ??
|
||||
screen.getAllByRole('button', { name: /modelProvider.addModel/i })[0]!
|
||||
|
||||
const mockProvider = {
|
||||
provider: 'openai',
|
||||
allow_custom_token: true,
|
||||
@ -97,7 +94,8 @@ describe('AddCustomModel', () => {
|
||||
expect(mockHandleOpenModalForAddNewCustomModel).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should show models list when models are available', () => {
|
||||
it('should show models list when models are available', async () => {
|
||||
const user = userEvent.setup()
|
||||
mockCanAddedModels = [{ model: 'gpt-4', model_type: 'llm' }]
|
||||
render(
|
||||
<AddCustomModel
|
||||
@ -106,19 +104,14 @@ describe('AddCustomModel', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
const trigger = screen.getByTestId('popover-trigger')
|
||||
expect(trigger).not.toHaveAttribute('data-popup-open')
|
||||
await user.click(getAddModelTrigger())
|
||||
|
||||
fireEvent.click(trigger)
|
||||
|
||||
// The portal should be "open"
|
||||
expect(trigger).toHaveAttribute('data-popup-open', '')
|
||||
expect(screen.getByTestId('popover')).toHaveAttribute('data-open', 'true')
|
||||
expect(screen.getByText('gpt-4')).toBeInTheDocument()
|
||||
expect(await screen.findByText('gpt-4')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('model-icon')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call handleOpenModalForAddCustomModelToModelList when clicking a model', () => {
|
||||
it('should call handleOpenModalForAddCustomModelToModelList when clicking a model', async () => {
|
||||
const user = userEvent.setup()
|
||||
const model = { model: 'gpt-4', model_type: 'llm' }
|
||||
mockCanAddedModels = [model]
|
||||
render(
|
||||
@ -128,13 +121,14 @@ describe('AddCustomModel', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('popover-trigger'))
|
||||
fireEvent.click(screen.getByText('gpt-4'))
|
||||
await user.click(getAddModelTrigger())
|
||||
await user.click(await screen.findByText('gpt-4'))
|
||||
|
||||
expect(mockHandleOpenModalForAddCustomModelToModelList).toHaveBeenCalledWith(undefined, model)
|
||||
})
|
||||
|
||||
it('should show existing model rows as disabled for create-only users', () => {
|
||||
it('should show existing model rows as disabled for create-only users', async () => {
|
||||
const user = userEvent.setup()
|
||||
const model = { model: 'gpt-4', model_type: 'llm' }
|
||||
mockWorkspacePermissionKeys.value = ['credential.create']
|
||||
mockCanAddedModels = [model]
|
||||
@ -146,20 +140,20 @@ describe('AddCustomModel', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('popover-trigger'))
|
||||
await user.click(getAddModelTrigger())
|
||||
|
||||
const modelRow = screen.getByText('gpt-4').closest('[aria-disabled]')
|
||||
const modelRow = (await screen.findByText('gpt-4')).closest('[aria-disabled]')
|
||||
expect(modelRow).toHaveAttribute('aria-disabled', 'true')
|
||||
expect(modelRow).toHaveClass('cursor-not-allowed')
|
||||
|
||||
fireEvent.click(screen.getByText('gpt-4'))
|
||||
await user.click(modelRow!)
|
||||
expect(mockHandleOpenModalForAddCustomModelToModelList).not.toHaveBeenCalled()
|
||||
|
||||
fireEvent.click(screen.getByText(/modelProvider.auth.addNewModel/))
|
||||
await user.click(screen.getByText(/modelProvider.auth.addNewModel/))
|
||||
expect(mockHandleOpenModalForAddNewCustomModel).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call handleOpenModalForAddNewCustomModel when clicking "Add New Model" in list', () => {
|
||||
it('should call handleOpenModalForAddNewCustomModel when clicking "Add New Model" in list', async () => {
|
||||
const user = userEvent.setup()
|
||||
mockCanAddedModels = [{ model: 'gpt-4', model_type: 'llm' }]
|
||||
render(
|
||||
<AddCustomModel
|
||||
@ -168,13 +162,14 @@ describe('AddCustomModel', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('popover-trigger'))
|
||||
fireEvent.click(screen.getByText(/modelProvider.auth.addNewModel/))
|
||||
await user.click(getAddModelTrigger())
|
||||
await user.click(await screen.findByText(/modelProvider.auth.addNewModel/))
|
||||
|
||||
expect(mockHandleOpenModalForAddNewCustomModel).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should show tooltip when no models and custom tokens not allowed', () => {
|
||||
it('should show tooltip when no models and custom tokens not allowed', async () => {
|
||||
const user = userEvent.setup()
|
||||
const restrictedProvider = { ...mockProvider, allow_custom_token: false }
|
||||
mockCanAddedModels = []
|
||||
render(
|
||||
@ -184,10 +179,11 @@ describe('AddCustomModel', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('tooltip-mock')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.auth.credentialUnavailable')).toBeInTheDocument()
|
||||
const trigger = getAddModelTrigger()
|
||||
await user.hover(trigger)
|
||||
expect(await screen.findByText('plugin.auth.credentialUnavailable')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /modelProvider.addModel/i }))
|
||||
await user.click(trigger)
|
||||
expect(mockHandleOpenModalForAddNewCustomModel).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@ -72,8 +72,6 @@ vi.mock('../authorized-item', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/popover', async () => await import('@/__mocks__/base-ui-popover'))
|
||||
|
||||
describe('Authorized', () => {
|
||||
const mockProvider: ModelProvider = {
|
||||
provider: 'openai',
|
||||
@ -102,6 +100,8 @@ describe('Authorized', () => {
|
||||
</button>
|
||||
)
|
||||
|
||||
const getTrigger = () => screen.getAllByRole('button', { name: /trigger\s*(open|closed)/i })[0]!
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockDeleteCredentialId = null
|
||||
@ -119,13 +119,13 @@ describe('Authorized', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
const trigger = screen.getByTestId('popover-trigger')
|
||||
const trigger = getTrigger()
|
||||
expect(trigger).not.toHaveAttribute('data-popup-open')
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /trigger\s*closed/i }))
|
||||
fireEvent.click(trigger)
|
||||
|
||||
expect(trigger).toHaveAttribute('data-popup-open', '')
|
||||
expect(screen.getByRole('button', { name: /trigger\s*open/i })).toBeInTheDocument()
|
||||
expect(trigger).toHaveTextContent(/trigger\s*open/i)
|
||||
expect(screen.getByTestId('authorized-item'))!.toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: /addApiKey/i }))!.toBeInTheDocument()
|
||||
})
|
||||
@ -141,9 +141,8 @@ describe('Authorized', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /trigger\s*closed/i }))
|
||||
fireEvent.click(getTrigger())
|
||||
expect(mockHandleOpenModal).toHaveBeenCalled()
|
||||
expect(screen.queryByTestId('authorized-item')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call onItemClick when credential is selected', () => {
|
||||
@ -158,7 +157,7 @@ describe('Authorized', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /trigger\s*closed/i }))
|
||||
fireEvent.click(getTrigger())
|
||||
fireEvent.click(screen.getAllByRole('button', { name: 'Select' })[0]!)
|
||||
|
||||
expect(onItemClick).toHaveBeenCalledWith(mockCredentials[0], mockItems[0]!.model)
|
||||
@ -174,7 +173,7 @@ describe('Authorized', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /trigger\s*closed/i }))
|
||||
fireEvent.click(getTrigger())
|
||||
fireEvent.click(screen.getAllByRole('button', { name: 'Select' })[0]!)
|
||||
|
||||
expect(mockHandleActiveCredential).toHaveBeenCalledWith(mockCredentials[0], mockItems[0]!.model)
|
||||
@ -195,7 +194,7 @@ describe('Authorized', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /trigger\s*closed/i }))
|
||||
fireEvent.click(getTrigger())
|
||||
fireEvent.click(screen.getByText(/addModelCredential/))
|
||||
|
||||
expect(mockHandleOpenModal).toHaveBeenCalledWith(undefined, {
|
||||
@ -215,7 +214,7 @@ describe('Authorized', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /trigger\s*closed/i }))
|
||||
fireEvent.click(getTrigger())
|
||||
expect(screen.queryByRole('button', { name: /addApiKey/i })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
@ -231,7 +230,7 @@ describe('Authorized', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /trigger\s*closed/i }))
|
||||
fireEvent.click(getTrigger())
|
||||
fireEvent.click(screen.getAllByRole('button', { name: 'Edit' })[0]!)
|
||||
fireEvent.click(screen.getAllByRole('button', { name: 'Delete' })[0]!)
|
||||
fireEvent.click(screen.getAllByRole('button', { name: 'Select' })[0]!)
|
||||
@ -254,7 +253,7 @@ describe('Authorized', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /trigger\s*closed/i }))
|
||||
fireEvent.click(getTrigger())
|
||||
fireEvent.click(screen.getAllByRole('button', { name: 'Edit' })[0]!)
|
||||
fireEvent.click(screen.getAllByRole('button', { name: 'Delete' })[0]!)
|
||||
fireEvent.click(screen.getAllByRole('button', { name: 'Select' })[0]!)
|
||||
@ -278,11 +277,11 @@ describe('Authorized', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /trigger\s*closed/i }))
|
||||
fireEvent.click(getTrigger())
|
||||
fireEvent.click(screen.getAllByRole('button', { name: 'Edit' })[0]!)
|
||||
fireEvent.click(screen.getByRole('button', { name: /trigger\s*closed/i }))
|
||||
fireEvent.click(getTrigger())
|
||||
fireEvent.click(screen.getAllByRole('button', { name: 'Delete' })[0]!)
|
||||
fireEvent.click(screen.getByRole('button', { name: /trigger\s*closed/i }))
|
||||
fireEvent.click(getTrigger())
|
||||
fireEvent.click(screen.getAllByRole('button', { name: 'Select' })[0]!)
|
||||
|
||||
expect(mockHandleOpenModal).toHaveBeenCalledWith(mockCredentials[0], mockItems[0]!.model)
|
||||
|
||||
@ -1,23 +1,10 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { Credential, ModelProvider } from '../../declarations'
|
||||
import { act, screen } from '@testing-library/react'
|
||||
import { screen } from '@testing-library/react'
|
||||
import { render } from '@/test/console/render'
|
||||
import { ConfigurationMethodEnum, ModelModalModeEnum } from '../../declarations'
|
||||
import ModelModal from '../index'
|
||||
|
||||
type DialogProps = {
|
||||
children: ReactNode
|
||||
onOpenChange?: (open: boolean) => void
|
||||
}
|
||||
|
||||
type AlertDialogProps = {
|
||||
children: ReactNode
|
||||
onOpenChange?: (open: boolean) => void
|
||||
}
|
||||
|
||||
let mockLanguage = 'en_US'
|
||||
let latestDialogOnOpenChange: DialogProps['onOpenChange']
|
||||
let latestAlertDialogOnOpenChange: AlertDialogProps['onOpenChange']
|
||||
let mockAvailableCredentials: Credential[] | undefined = []
|
||||
let mockDeleteCredentialId: string | null = null
|
||||
|
||||
@ -41,39 +28,6 @@ vi.mock('../../model-auth', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/dialog', () => ({
|
||||
Dialog: ({ children, onOpenChange }: DialogProps) => {
|
||||
latestDialogOnOpenChange = onOpenChange
|
||||
return <div>{children}</div>
|
||||
},
|
||||
DialogContent: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
DialogCloseButton: () => <button type="button">close</button>,
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/alert-dialog', () => ({
|
||||
AlertDialog: ({ children, onOpenChange }: AlertDialogProps) => {
|
||||
latestAlertDialogOnOpenChange = onOpenChange
|
||||
return <div>{children}</div>
|
||||
},
|
||||
AlertDialogActions: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
AlertDialogCancelButton: ({ children }: { children: ReactNode }) => (
|
||||
<button type="button">{children}</button>
|
||||
),
|
||||
AlertDialogConfirmButton: ({
|
||||
children,
|
||||
onClick,
|
||||
}: {
|
||||
children: ReactNode
|
||||
onClick?: () => void
|
||||
}) => (
|
||||
<button type="button" onClick={onClick}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
AlertDialogContent: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
AlertDialogTitle: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
}))
|
||||
|
||||
vi.mock('../../model-auth/hooks', () => ({
|
||||
useCredentialData: () => ({
|
||||
isLoading: false,
|
||||
@ -153,53 +107,10 @@ describe('ModelModal dialog branches', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockLanguage = 'en_US'
|
||||
latestDialogOnOpenChange = undefined
|
||||
latestAlertDialogOnOpenChange = undefined
|
||||
mockAvailableCredentials = []
|
||||
mockDeleteCredentialId = null
|
||||
})
|
||||
|
||||
it('should only cancel when the dialog reports it has closed', () => {
|
||||
const onCancel = vi.fn()
|
||||
render(
|
||||
<ModelModal
|
||||
provider={createProvider()}
|
||||
configurateMethod={ConfigurationMethodEnum.predefinedModel}
|
||||
onCancel={onCancel}
|
||||
onSave={vi.fn()}
|
||||
onRemove={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
act(() => {
|
||||
latestDialogOnOpenChange?.(true)
|
||||
latestDialogOnOpenChange?.(false)
|
||||
})
|
||||
|
||||
expect(onCancel).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should only close the confirm dialog when the alert dialog closes', () => {
|
||||
mockDeleteCredentialId = 'cred-1'
|
||||
|
||||
render(
|
||||
<ModelModal
|
||||
provider={createProvider()}
|
||||
configurateMethod={ConfigurationMethodEnum.predefinedModel}
|
||||
onCancel={vi.fn()}
|
||||
onSave={vi.fn()}
|
||||
onRemove={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
act(() => {
|
||||
latestAlertDialogOnOpenChange?.(true)
|
||||
latestAlertDialogOnOpenChange?.(false)
|
||||
})
|
||||
|
||||
expect(mockCloseConfirmDelete).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should pass an empty credential list to the selector when no credentials are available', () => {
|
||||
mockAvailableCredentials = undefined
|
||||
|
||||
|
||||
@ -1,45 +1,14 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import ParameterItem from '../parameter-item'
|
||||
|
||||
vi.mock('../../hooks', () => ({
|
||||
useLanguage: () => 'en_US',
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/select', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@langgenius/dify-ui/select')>()
|
||||
|
||||
return {
|
||||
...actual,
|
||||
Select: ({
|
||||
children,
|
||||
onValueChange,
|
||||
}: {
|
||||
children: ReactNode
|
||||
onValueChange: (value: string | undefined) => void
|
||||
}) => (
|
||||
<div>
|
||||
<button type="button" onClick={() => onValueChange('updated')}>
|
||||
select-updated
|
||||
</button>
|
||||
<button type="button" onClick={() => onValueChange(undefined)}>
|
||||
select-empty
|
||||
</button>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
SelectContent: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
SelectItem: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
SelectLabel: () => null,
|
||||
SelectTrigger: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
SelectValue: () => <div>SelectValue</div>,
|
||||
SelectItemText: ({ children }: { children: ReactNode }) => <span>{children}</span>,
|
||||
SelectItemIndicator: () => <span data-testid="select-item-indicator" />,
|
||||
}
|
||||
})
|
||||
|
||||
describe('ParameterItem select mode', () => {
|
||||
it('should propagate both explicit and empty select values', () => {
|
||||
it('should propagate a selected value', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onChange = vi.fn()
|
||||
|
||||
render(
|
||||
@ -57,10 +26,9 @@ describe('ParameterItem select mode', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'select-updated' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'select-empty' }))
|
||||
await user.click(screen.getByRole('combobox'))
|
||||
await user.click(await screen.findByRole('option', { name: 'text' }))
|
||||
|
||||
expect(onChange).toHaveBeenNthCalledWith(1, 'updated')
|
||||
expect(onChange).toHaveBeenNthCalledWith(2, undefined)
|
||||
expect(onChange).toHaveBeenCalledWith('text')
|
||||
})
|
||||
})
|
||||
|
||||
@ -8,14 +8,6 @@ vi.mock('../../hooks', () => ({
|
||||
useLanguage: () => 'en_US',
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/slider', () => ({
|
||||
Slider: ({ onValueChange }: { onValueChange: (v: number) => void }) => (
|
||||
<button onClick={() => onValueChange(2)} data-testid="slider-btn">
|
||||
Slide 2
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/tag-input', () => ({
|
||||
default: ({ onChange }: { onChange: (v: string[]) => void }) => (
|
||||
<button onClick={() => onChange(['tag1', 'tag2'])} data-testid="tag-input">
|
||||
@ -83,7 +75,7 @@ describe('ParameterItem', () => {
|
||||
const input = screen.getByRole('spinbutton')
|
||||
fireEvent.change(input, { target: { value: '1.4' } })
|
||||
expect(onChange).toHaveBeenCalledWith(1)
|
||||
expect(screen.getByTestId('slider-btn'))!.toBeInTheDocument()
|
||||
expect(screen.getByRole('slider'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should clamp float numeric input to min', () => {
|
||||
@ -139,20 +131,6 @@ describe('ParameterItem', () => {
|
||||
expect(screen.getByRole('spinbutton'))!.toHaveAttribute('step', '0')
|
||||
})
|
||||
|
||||
it('should handle slide change and clamp values', () => {
|
||||
const onChange = vi.fn()
|
||||
render(
|
||||
<ParameterItem
|
||||
parameterRule={createRule({ type: 'float', min: 0, max: 10 })}
|
||||
value={0.7}
|
||||
onChange={onChange}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('slider-btn'))
|
||||
expect(onChange).toHaveBeenCalledWith(2)
|
||||
})
|
||||
|
||||
it('should render exact string input and propagate text changes', () => {
|
||||
const onChange = vi.fn()
|
||||
render(
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { ReactElement, ReactNode } from 'react'
|
||||
import type { ReactElement } from 'react'
|
||||
import type { PluginDetail } from '@/app/components/plugins/types'
|
||||
import { fireEvent, screen } from '@testing-library/react'
|
||||
import { PluginSource } from '@/app/components/plugins/types'
|
||||
@ -48,6 +48,10 @@ vi.mock('@/app/components/plugins/plugin-detail-panel/detail-header/hooks', () =
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-plugins', () => ({
|
||||
useVersionListOfPlugin: () => ({ data: { data: { versions: [] } } }),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/plugins/plugin-detail-panel/detail-header/components', () => ({
|
||||
HeaderModals: ({
|
||||
targetVersion,
|
||||
@ -77,29 +81,6 @@ vi.mock('@/app/components/plugins/plugin-page/use-reference-setting', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/plugins/update-plugin/plugin-version-picker', () => ({
|
||||
default: ({
|
||||
trigger,
|
||||
onSelect,
|
||||
disabled,
|
||||
}: {
|
||||
trigger: (open: boolean) => ReactNode
|
||||
onSelect: (state: { version: string; unique_identifier: string; isDowngrade?: boolean }) => void
|
||||
disabled?: boolean
|
||||
}) => (
|
||||
<div data-testid="plugin-version-picker" data-disabled={String(Boolean(disabled))}>
|
||||
{trigger(false)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
onSelect({ version: '2.0.0', unique_identifier: 'plugin@2.0.0', isDowngrade: true })
|
||||
}
|
||||
>
|
||||
select version
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
vi.mock('@/hooks/use-theme', () => ({
|
||||
default: () => ({ theme: 'light' }),
|
||||
}))
|
||||
@ -150,20 +131,10 @@ describe('ProviderCardActions', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('should render version controls for marketplace plugins and handle manual version selection', () => {
|
||||
it('should render version controls for marketplace plugins', () => {
|
||||
render(<ProviderCardActions detail={createDetail()} />)
|
||||
|
||||
expect(screen.getByText('1.0.0')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('plugin-version-picker')).toHaveAttribute('data-disabled', 'false')
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'select version' }))
|
||||
|
||||
expect(mockSetTargetVersion).toHaveBeenCalledWith({
|
||||
version: '2.0.0',
|
||||
unique_identifier: 'plugin@2.0.0',
|
||||
isDowngrade: true,
|
||||
})
|
||||
expect(mockHandleUpdate).toHaveBeenCalledWith(true)
|
||||
expect(screen.getByRole('button', { name: '1.0.0' })).toBeEnabled()
|
||||
})
|
||||
|
||||
it('should show a compact debug badge after the version for debugging plugins', () => {
|
||||
@ -232,7 +203,7 @@ describe('ProviderCardActions', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('plugin-version-picker')).toHaveAttribute('data-disabled', 'true')
|
||||
expect(screen.getByRole('button', { name: '1.0.0' })).toBeDisabled()
|
||||
openActionsMenu()
|
||||
expect(
|
||||
screen.getByRole('menuitem', { name: 'plugin.detailPanel.operation.viewDetail' }),
|
||||
|
||||
@ -1,16 +1,9 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { ModelProvider } from '../../../declarations'
|
||||
import type { CredentialPanelState } from '../../use-credential-panel-state'
|
||||
import { act, fireEvent, screen } from '@testing-library/react'
|
||||
import { screen } from '@testing-library/react'
|
||||
import { render } from '@/test/console/render'
|
||||
import DropdownContent from '../dropdown-content'
|
||||
|
||||
type AlertDialogProps = {
|
||||
children: ReactNode
|
||||
onOpenChange?: (open: boolean) => void
|
||||
}
|
||||
|
||||
let latestOnOpenChange: AlertDialogProps['onOpenChange']
|
||||
const mockOpenConfirmDelete = vi.fn()
|
||||
const mockCloseConfirmDelete = vi.fn()
|
||||
const mockHandleConfirmDelete = vi.fn()
|
||||
@ -42,44 +35,10 @@ vi.mock('../use-activate-credential', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/alert-dialog', () => ({
|
||||
AlertDialog: ({ children, onOpenChange }: AlertDialogProps) => {
|
||||
latestOnOpenChange = onOpenChange
|
||||
return <div>{children}</div>
|
||||
},
|
||||
AlertDialogActions: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
AlertDialogCancelButton: ({ children }: { children: ReactNode }) => (
|
||||
<button type="button">{children}</button>
|
||||
),
|
||||
AlertDialogConfirmButton: ({
|
||||
children,
|
||||
onClick,
|
||||
}: {
|
||||
children: ReactNode
|
||||
onClick?: () => void
|
||||
}) => (
|
||||
<button type="button" onClick={onClick}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
AlertDialogContent: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
AlertDialogDescription: () => <div />,
|
||||
AlertDialogTitle: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
}))
|
||||
|
||||
vi.mock('../api-key-section', () => ({
|
||||
default: ({
|
||||
credentials,
|
||||
onDelete,
|
||||
}: {
|
||||
credentials: unknown[]
|
||||
onDelete: (credential?: unknown) => void
|
||||
}) => (
|
||||
default: ({ credentials }: { credentials: unknown[] }) => (
|
||||
<div>
|
||||
<span>{`credentials:${credentials.length}`}</span>
|
||||
<button type="button" onClick={() => onDelete(undefined)}>
|
||||
delete-undefined
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
@ -135,7 +94,6 @@ vi.mock('@/context/permission-state', async () => {
|
||||
describe('DropdownContent dialog branches', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
latestOnOpenChange = undefined
|
||||
})
|
||||
|
||||
it('should fall back to an empty credential list when the provider has no credentials', () => {
|
||||
@ -151,39 +109,4 @@ describe('DropdownContent dialog branches', () => {
|
||||
|
||||
expect(screen.getByText('credentials:0')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should ignore delete requests without a credential payload', () => {
|
||||
render(
|
||||
<DropdownContent
|
||||
provider={createProvider()}
|
||||
state={createState()}
|
||||
isChangingPriority={false}
|
||||
onChangePriority={vi.fn()}
|
||||
onClose={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'delete-undefined' }))
|
||||
|
||||
expect(mockOpenConfirmDelete).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should only close the confirm dialog when the alert dialog reports closed', () => {
|
||||
render(
|
||||
<DropdownContent
|
||||
provider={createProvider()}
|
||||
state={createState()}
|
||||
isChangingPriority={false}
|
||||
onChangePriority={vi.fn()}
|
||||
onClose={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
act(() => {
|
||||
latestOnOpenChange?.(true)
|
||||
latestOnOpenChange?.(false)
|
||||
})
|
||||
|
||||
expect(mockCloseConfirmDelete).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import type { GetAccountProfileResponse } from '@dify/contracts/api/console/account/types.gen'
|
||||
import { ToastHost } from '@langgenius/dify-ui/toast'
|
||||
import { act, fireEvent, screen, waitFor, within } from '@testing-library/react'
|
||||
import { screen, waitFor, within } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { languages } from '@/i18n-config/language'
|
||||
import { updateUserProfile } from '@/service/common'
|
||||
import { render } from '@/test/console/render'
|
||||
@ -12,72 +13,6 @@ const mockMutateUserProfile = vi.fn()
|
||||
let mockLocale: string | undefined = 'en-US'
|
||||
let mockUserProfile: GetAccountProfileResponse
|
||||
|
||||
vi.mock('@langgenius/dify-ui/select', async () => {
|
||||
const React = await import('react')
|
||||
const SelectContext = React.createContext<{
|
||||
disabled?: boolean
|
||||
onValueChange?: (value: string) => void
|
||||
}>({})
|
||||
|
||||
return {
|
||||
Select: ({
|
||||
children,
|
||||
disabled,
|
||||
onValueChange,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
disabled?: boolean
|
||||
onValueChange?: (value: string) => void
|
||||
}) => {
|
||||
return (
|
||||
<SelectContext.Provider value={{ disabled, onValueChange }}>
|
||||
<div>{children}</div>
|
||||
</SelectContext.Provider>
|
||||
)
|
||||
},
|
||||
SelectTrigger: ({ children }: { children: React.ReactNode }) => {
|
||||
const context = React.useContext(SelectContext)
|
||||
return (
|
||||
<div>
|
||||
<button type="button" disabled={context.disabled}>
|
||||
{children}
|
||||
</button>
|
||||
<button
|
||||
data-testid="select-empty"
|
||||
type="button"
|
||||
onClick={() => context.onValueChange?.('')}
|
||||
>
|
||||
empty value
|
||||
</button>
|
||||
<button
|
||||
data-testid="select-invalid"
|
||||
type="button"
|
||||
onClick={() => context.onValueChange?.('__missing__')}
|
||||
>
|
||||
invalid value
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
SelectContent: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
SelectItem: ({ children, value }: { children: React.ReactNode; value: string }) => {
|
||||
const context = React.useContext(SelectContext)
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
role="option"
|
||||
aria-selected={false}
|
||||
onClick={() => context.onValueChange?.(value)}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
},
|
||||
SelectItemText: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
||||
SelectItemIndicator: () => null,
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/next/navigation', () => ({
|
||||
useRouter: () => ({ refresh: mockRefresh }),
|
||||
}))
|
||||
@ -135,13 +70,10 @@ const getSectionByLabel = (sectionLabel: string) => {
|
||||
}
|
||||
|
||||
const selectOption = async (sectionLabel: string, optionName: string) => {
|
||||
const user = userEvent.setup()
|
||||
const section = getSectionByLabel(sectionLabel)
|
||||
await act(async () => {
|
||||
fireEvent.click(within(section).getAllByRole('button')[0]!)
|
||||
})
|
||||
await act(async () => {
|
||||
fireEvent.click(await within(section).findByRole('option', { name: optionName }))
|
||||
})
|
||||
await user.click(within(section).getByRole('combobox'))
|
||||
await user.click(await screen.findByRole('option', { name: optionName }))
|
||||
}
|
||||
|
||||
const getLanguageOption = (value: string) => {
|
||||
@ -178,8 +110,12 @@ describe('PreferencePage - Rendering', () => {
|
||||
|
||||
expect(screen.getByText('common.language.displayLanguage')).toBeInTheDocument()
|
||||
expect(screen.getByText('common.language.timezone')).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: english.name })).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: niueTimezone.name })).toBeInTheDocument()
|
||||
expect(
|
||||
within(getSectionByLabel('common.language.displayLanguage')).getByRole('combobox'),
|
||||
).toHaveTextContent(english.name)
|
||||
expect(
|
||||
within(getSectionByLabel('common.language.timezone')).getByRole('combobox'),
|
||||
).toHaveTextContent(niueTimezone.name)
|
||||
})
|
||||
|
||||
it('should render placeholders when the current locale or timezone is unsupported', () => {
|
||||
@ -191,7 +127,12 @@ describe('PreferencePage - Rendering', () => {
|
||||
|
||||
renderPage()
|
||||
|
||||
expect(screen.getAllByRole('button', { name: 'common.placeholder.select' })).toHaveLength(2)
|
||||
expect(
|
||||
within(getSectionByLabel('common.language.displayLanguage')).getByRole('combobox'),
|
||||
).toHaveTextContent('common.placeholder.select')
|
||||
expect(
|
||||
within(getSectionByLabel('common.language.timezone')).getByRole('combobox'),
|
||||
).toHaveTextContent('common.placeholder.select')
|
||||
})
|
||||
})
|
||||
|
||||
@ -253,30 +194,4 @@ describe('PreferencePage - Interactions', () => {
|
||||
|
||||
expect(await screen.findByText('Timezone failed')).toBeInTheDocument()
|
||||
}, 15000)
|
||||
|
||||
it('should ignore empty and unknown language selections', async () => {
|
||||
renderPage()
|
||||
|
||||
const section = getSectionByLabel('common.language.displayLanguage')
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(within(section).getByTestId('select-empty'))
|
||||
fireEvent.click(within(section).getByTestId('select-invalid'))
|
||||
})
|
||||
|
||||
expect(updateUserProfileMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should ignore empty and unknown timezone selections', async () => {
|
||||
renderPage()
|
||||
|
||||
const section = getSectionByLabel('common.language.timezone')
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(within(section).getByTestId('select-empty'))
|
||||
fireEvent.click(within(section).getByTestId('select-invalid'))
|
||||
})
|
||||
|
||||
expect(updateUserProfileMock).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { PluginDeclaration, UpdateFromGitHubPayload } from '../../../../types'
|
||||
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { PluginCategoryEnum } from '../../../../types'
|
||||
import SelectPackage from '../selectPackage'
|
||||
@ -21,69 +22,6 @@ vi.mock('../../../hooks', async (importOriginal) => {
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@langgenius/dify-ui/select', async () => {
|
||||
const React = await import('react')
|
||||
const SelectContext = React.createContext<{
|
||||
readOnly?: boolean
|
||||
onValueChange?: (value: string) => void
|
||||
}>({})
|
||||
|
||||
return {
|
||||
Select: ({
|
||||
children,
|
||||
readOnly,
|
||||
onValueChange,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
readOnly?: boolean
|
||||
onValueChange?: (value: string) => void
|
||||
}) => (
|
||||
<SelectContext.Provider value={{ readOnly, onValueChange }}>
|
||||
<div>{children}</div>
|
||||
</SelectContext.Provider>
|
||||
),
|
||||
SelectLabel: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
SelectTrigger: ({ children }: { children: React.ReactNode }) => {
|
||||
const context = React.useContext(SelectContext)
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
data-testid="select-trigger"
|
||||
className={context.readOnly ? 'cursor-not-allowed' : 'cursor-pointer'}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
<button
|
||||
data-testid="select-empty"
|
||||
type="button"
|
||||
onClick={() => context.onValueChange?.('')}
|
||||
>
|
||||
empty select value
|
||||
</button>
|
||||
<button
|
||||
data-testid="select-invalid"
|
||||
type="button"
|
||||
onClick={() => context.onValueChange?.('__missing__')}
|
||||
>
|
||||
invalid select value
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
SelectContent: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
SelectItem: ({ children, value }: { children: React.ReactNode; value: string }) => {
|
||||
const context = React.useContext(SelectContext)
|
||||
return (
|
||||
<button type="button" onClick={() => context.onValueChange?.(value)}>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
},
|
||||
SelectItemText: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
||||
SelectItemIndicator: () => null,
|
||||
}
|
||||
})
|
||||
|
||||
// Factory functions
|
||||
const createMockManifest = (): PluginDeclaration => ({
|
||||
plugin_unique_identifier: 'test-uid',
|
||||
@ -312,28 +250,20 @@ describe('SelectPackage', () => {
|
||||
expect(mockHandleUpload).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should ignore empty and unknown version selections', () => {
|
||||
it('should select a valid version option', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onSelectVersion = vi.fn()
|
||||
renderSelectPackage({ onSelectVersion })
|
||||
|
||||
const section = getSection('plugin.installFromGitHub.selectVersion')
|
||||
fireEvent.click(within(section).getByTestId('select-empty'))
|
||||
fireEvent.click(within(section).getByTestId('select-invalid'))
|
||||
|
||||
expect(onSelectVersion).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should select a valid version option', () => {
|
||||
const onSelectVersion = vi.fn()
|
||||
renderSelectPackage({ onSelectVersion })
|
||||
|
||||
const section = getSection('plugin.installFromGitHub.selectVersion')
|
||||
fireEvent.click(within(section).getByRole('button', { name: 'v0.9.0' }))
|
||||
await user.click(within(section).getByRole('combobox'))
|
||||
await user.click(await screen.findByRole('option', { name: 'v0.9.0' }))
|
||||
|
||||
expect(onSelectVersion).toHaveBeenCalledWith({ value: 'v0.9.0', name: 'v0.9.0' })
|
||||
})
|
||||
|
||||
it('should ignore empty and unknown package selections', () => {
|
||||
it('should select a valid package option', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onSelectPackage = vi.fn()
|
||||
renderSelectPackage({
|
||||
selectedVersion: 'v1.0.0',
|
||||
@ -341,21 +271,8 @@ describe('SelectPackage', () => {
|
||||
})
|
||||
|
||||
const section = getSection('plugin.installFromGitHub.selectPackage')
|
||||
fireEvent.click(within(section).getByTestId('select-empty'))
|
||||
fireEvent.click(within(section).getByTestId('select-invalid'))
|
||||
|
||||
expect(onSelectPackage).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should select a valid package option', () => {
|
||||
const onSelectPackage = vi.fn()
|
||||
renderSelectPackage({
|
||||
selectedVersion: 'v1.0.0',
|
||||
onSelectPackage,
|
||||
})
|
||||
|
||||
const section = getSection('plugin.installFromGitHub.selectPackage')
|
||||
fireEvent.click(within(section).getByRole('button', { name: 'plugin.tar.gz' }))
|
||||
await user.click(within(section).getByRole('combobox'))
|
||||
await user.click(await screen.findByRole('option', { name: 'plugin.tar.gz' }))
|
||||
|
||||
expect(onSelectPackage).toHaveBeenCalledWith({
|
||||
value: 'plugin.tar.gz',
|
||||
@ -563,16 +480,16 @@ describe('SelectPackage', () => {
|
||||
renderSelectPackage({ selectedVersion: '' })
|
||||
|
||||
// When no version is selected, package select should be readonly
|
||||
const trigger = screen.getAllByTestId('select-trigger')[1]
|
||||
expect(trigger).toHaveClass('cursor-not-allowed')
|
||||
const trigger = screen.getAllByRole('combobox')[1]
|
||||
expect(trigger).toHaveAttribute('aria-readonly', 'true')
|
||||
})
|
||||
|
||||
it('should make package select active when version is selected', () => {
|
||||
renderSelectPackage({ selectedVersion: 'v1.0.0' })
|
||||
|
||||
// When version is selected, package select should be active
|
||||
const trigger = screen.getAllByTestId('select-trigger')[1]
|
||||
expect(trigger).toHaveClass('cursor-pointer')
|
||||
const trigger = screen.getAllByRole('combobox')[1]
|
||||
expect(trigger).not.toHaveAttribute('aria-readonly', 'true')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -33,8 +33,6 @@ vi.mock('@/app/components/plugins/hooks', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/popover', () => import('@/__mocks__/base-ui-popover'))
|
||||
|
||||
describe('TagsFilter', () => {
|
||||
const ensurePopoverOpen = async (user: ReturnType<typeof userEvent.setup>) => {
|
||||
if (!screen.queryByRole('searchbox', { name: 'pluginTags.searchTags' }))
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import type { MouseEventHandler, ReactNode } from 'react'
|
||||
import { render, screen, within } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import SortDropdown from '../index'
|
||||
@ -33,78 +32,6 @@ vi.mock('../../atoms', () => ({
|
||||
useMarketplaceSort: () => [mockSort, mockHandleSortChange],
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/dropdown-menu', async () => {
|
||||
const React = await import('react')
|
||||
const DropdownMenuContext = React.createContext<{
|
||||
open: boolean
|
||||
setOpen: (open: boolean) => void
|
||||
} | null>(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,
|
||||
onOpenChange,
|
||||
}: {
|
||||
children: ReactNode
|
||||
open: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
}) => (
|
||||
<DropdownMenuContext value={{ open, setOpen: onOpenChange ?? vi.fn() }}>
|
||||
<div data-testid="dropdown-wrapper" data-open={open}>
|
||||
{children}
|
||||
</div>
|
||||
</DropdownMenuContext>
|
||||
),
|
||||
DropdownMenuTrigger: ({ children, className }: { children: ReactNode; className?: string }) => {
|
||||
const { open, setOpen } = useDropdownMenuContext()
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={className}
|
||||
data-testid="dropdown-trigger"
|
||||
onClick={() => setOpen(!open)}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
},
|
||||
DropdownMenuContent: ({ children }: { children: ReactNode }) => {
|
||||
const { open } = useDropdownMenuContext()
|
||||
return open ? <div data-testid="dropdown-content">{children}</div> : null
|
||||
},
|
||||
DropdownMenuItem: ({
|
||||
children,
|
||||
onClick,
|
||||
className,
|
||||
}: {
|
||||
children: ReactNode
|
||||
onClick?: MouseEventHandler<HTMLButtonElement>
|
||||
className?: string
|
||||
}) => {
|
||||
const { setOpen } = useDropdownMenuContext()
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={className}
|
||||
onClick={(event) => {
|
||||
onClick?.(event)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
describe('SortDropdown', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@ -114,7 +41,7 @@ describe('SortDropdown', () => {
|
||||
it('renders the selected sort option in the trigger', () => {
|
||||
render(<SortDropdown />)
|
||||
|
||||
const trigger = screen.getByTestId('dropdown-trigger')
|
||||
const trigger = screen.getByRole('button', { name: 'Sort by Most Popular' })
|
||||
expect(within(trigger).getByText('Sort by')).toBeInTheDocument()
|
||||
expect(within(trigger).getByText('Most Popular')).toBeInTheDocument()
|
||||
})
|
||||
@ -131,9 +58,9 @@ describe('SortDropdown', () => {
|
||||
const user = userEvent.setup()
|
||||
render(<SortDropdown />)
|
||||
|
||||
await user.click(screen.getByTestId('dropdown-trigger'))
|
||||
await user.click(screen.getByRole('button', { name: 'Sort by Most Popular' }))
|
||||
|
||||
const content = screen.getByTestId('dropdown-content')
|
||||
const content = await screen.findByRole('menu')
|
||||
expect(within(content).getByText('Most Popular')).toBeInTheDocument()
|
||||
expect(within(content).getByText('Recently Updated')).toBeInTheDocument()
|
||||
expect(within(content).getByText('Newly Released')).toBeInTheDocument()
|
||||
@ -142,24 +69,24 @@ describe('SortDropdown', () => {
|
||||
|
||||
it('shows a check icon for the currently selected option', async () => {
|
||||
const user = userEvent.setup()
|
||||
const { container } = render(<SortDropdown />)
|
||||
render(<SortDropdown />)
|
||||
|
||||
await user.click(screen.getByTestId('dropdown-trigger'))
|
||||
await user.click(screen.getByRole('button', { name: 'Sort by Most Popular' }))
|
||||
|
||||
expect(container.querySelector('.i-ri-check-line')).toBeInTheDocument()
|
||||
expect(document.querySelector('.i-ri-check-line')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('updates the sort and closes the menu when an option is selected', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<SortDropdown />)
|
||||
|
||||
await user.click(screen.getByTestId('dropdown-trigger'))
|
||||
await user.click(screen.getByText('Recently Updated'))
|
||||
await user.click(screen.getByRole('button', { name: 'Sort by Most Popular' }))
|
||||
await user.click(await screen.findByRole('menuitem', { name: 'Recently Updated' }))
|
||||
|
||||
expect(mockHandleSortChange).toHaveBeenCalledWith({
|
||||
sortBy: 'version_updated_at',
|
||||
sortOrder: 'DESC',
|
||||
})
|
||||
expect(screen.queryByTestId('dropdown-content')).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('menu')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -82,8 +82,6 @@ vi.mock('@/hooks/use-oauth', () => ({
|
||||
openOAuthPopup: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/popover', async () => await import('@/__mocks__/base-ui-popover'))
|
||||
|
||||
const mockConsoleState = vi.hoisted(() => ({
|
||||
userProfile: { id: 'test-user', name: 'Test User', email: 'test@example.com', avatar_url: '' },
|
||||
workspacePermissionKeys: ['credential.use', 'credential.create', 'credential.manage'] as string[],
|
||||
@ -203,13 +201,10 @@ describe('Authorized Component', () => {
|
||||
{ wrapper: createWrapper() },
|
||||
)
|
||||
|
||||
const trigger = screen.getByTestId('popover-trigger')
|
||||
expect(trigger).not.toHaveAttribute('data-popup-open')
|
||||
expect(screen.getByText('Closed'))!.toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByTestId('custom-trigger'))
|
||||
|
||||
expect(trigger).toHaveAttribute('data-popup-open', '')
|
||||
expect(screen.getByText('Open')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { InputVarType } from '@/app/components/workflow/types'
|
||||
import AppInputsForm from '../app-inputs-form'
|
||||
@ -26,59 +27,6 @@ vi.mock('@/app/components/base/file-uploader', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/select', async () => {
|
||||
const React = await import('react')
|
||||
const SelectContext = React.createContext<{
|
||||
onValueChange?: (value: string) => void
|
||||
}>({})
|
||||
|
||||
return {
|
||||
Select: ({
|
||||
children,
|
||||
onValueChange,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
onValueChange?: (value: string) => void
|
||||
}) => (
|
||||
<SelectContext.Provider value={{ onValueChange }}>
|
||||
<div>{children}</div>
|
||||
</SelectContext.Provider>
|
||||
),
|
||||
SelectTrigger: ({ children }: { children: React.ReactNode }) => {
|
||||
const context = React.useContext(SelectContext)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button type="button">{children}</button>
|
||||
<button
|
||||
data-testid="select-empty"
|
||||
type="button"
|
||||
onClick={() => context.onValueChange?.('')}
|
||||
>
|
||||
Empty Select
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
SelectContent: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
SelectItem: ({ children, value }: { children: React.ReactNode; value: string }) => {
|
||||
const context = React.useContext(SelectContext)
|
||||
return (
|
||||
<button
|
||||
key={value}
|
||||
data-testid={`select-${value}`}
|
||||
type="button"
|
||||
onClick={() => context.onValueChange?.(value)}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
},
|
||||
SelectItemText: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
||||
SelectItemIndicator: () => null,
|
||||
}
|
||||
})
|
||||
|
||||
describe('AppInputsForm', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@ -146,7 +94,8 @@ describe('AppInputsForm', () => {
|
||||
expect(onFormChange).toHaveBeenCalledWith({ count: '42' })
|
||||
})
|
||||
|
||||
it('should update select values', () => {
|
||||
it('should update select values', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onFormChange = vi.fn()
|
||||
const inputsRef = { current: { tone: '' } }
|
||||
|
||||
@ -167,38 +116,12 @@ describe('AppInputsForm', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('select-formal'))
|
||||
await user.click(screen.getByRole('combobox'))
|
||||
await user.click(await screen.findByRole('option', { name: 'formal' }))
|
||||
|
||||
expect(onFormChange).toHaveBeenCalledWith({ tone: 'formal' })
|
||||
})
|
||||
|
||||
it('should ignore empty select values and render the placeholder when there is no current selection', () => {
|
||||
const onFormChange = vi.fn()
|
||||
const inputsRef = { current: { tone: '' } }
|
||||
|
||||
render(
|
||||
<AppInputsForm
|
||||
inputsForms={[
|
||||
{
|
||||
variable: 'tone',
|
||||
label: 'Tone',
|
||||
type: InputVarType.select,
|
||||
options: ['friendly', 'formal'],
|
||||
required: false,
|
||||
},
|
||||
]}
|
||||
inputs={{ tone: '' }}
|
||||
inputsRef={inputsRef}
|
||||
onFormChange={onFormChange}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getAllByText('Tone').length).toBeGreaterThan(0)
|
||||
fireEvent.click(screen.getByTestId('select-empty'))
|
||||
|
||||
expect(onFormChange).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should update uploaded single file values', () => {
|
||||
const onFormChange = vi.fn()
|
||||
const inputsRef = { current: { attachment: null } }
|
||||
|
||||
@ -36,6 +36,10 @@ vi.mock('@/service/use-tools', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-plugins', () => ({
|
||||
useVersionListOfPlugin: () => ({ data: { data: { versions: [] } } }),
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/var', () => ({
|
||||
getMarketplaceUrl: (path: string) => `https://marketplace.example.com${path}`,
|
||||
}))
|
||||
@ -48,24 +52,12 @@ vi.mock('@/app/components/base/action-button', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/button', () => ({
|
||||
Button: ({ children, onClick }: { children: React.ReactNode; onClick?: () => void }) => (
|
||||
<button onClick={onClick}>{children}</button>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/badge', () => ({
|
||||
default: ({ text, children }: { text?: React.ReactNode; children?: React.ReactNode }) => (
|
||||
<div data-testid="badge">{text ?? children}</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/tooltip', () => ({
|
||||
Tooltip: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
TooltipTrigger: ({ render }: { render: React.ReactNode }) => <>{render}</>,
|
||||
TooltipContent: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/plugins/plugin-auth', () => ({
|
||||
AuthCategory: {
|
||||
tool: 'tool',
|
||||
@ -75,28 +67,6 @@ vi.mock('@/app/components/plugins/plugin-auth', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/plugins/update-plugin/plugin-version-picker', () => ({
|
||||
default: ({
|
||||
onSelect,
|
||||
trigger,
|
||||
}: {
|
||||
onSelect: (value: { version: string; unique_identifier: string; isDowngrade?: boolean }) => void
|
||||
trigger: React.ReactNode
|
||||
}) => (
|
||||
<div>
|
||||
{trigger}
|
||||
<button
|
||||
data-testid="version-select"
|
||||
onClick={() =>
|
||||
onSelect({ version: '2.0.0', unique_identifier: 'uid-2', isDowngrade: true })
|
||||
}
|
||||
>
|
||||
select version
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/badges/verified', () => ({
|
||||
default: () => <div data-testid="verified" />,
|
||||
}))
|
||||
@ -244,21 +214,14 @@ describe('DetailHeader', () => {
|
||||
expect(screen.getByTestId('header-modals')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('wires version selection, latest update, and hide actions', () => {
|
||||
it('wires latest update and hide actions', () => {
|
||||
const onHide = vi.fn()
|
||||
render(<DetailHeader detail={createDetail()} onHide={onHide} onUpdate={vi.fn()} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('version-select'))
|
||||
fireEvent.click(screen.getByText('plugin.detailPanel.operation.update'))
|
||||
fireEvent.click(screen.getByTestId('close-button'))
|
||||
|
||||
expect(mockSetTargetVersion).toHaveBeenCalledWith({
|
||||
version: '2.0.0',
|
||||
unique_identifier: 'uid-2',
|
||||
isDowngrade: true,
|
||||
})
|
||||
expect(mockHandleUpdate).toHaveBeenCalledTimes(2)
|
||||
expect(mockHandleUpdate).toHaveBeenNthCalledWith(1, true)
|
||||
expect(mockHandleUpdate).toHaveBeenCalledTimes(1)
|
||||
expect(onHide).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,689 +1,73 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
// Import component after mocks
|
||||
import { fireEvent, render, screen, within } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import TTSParamsPanel from '../tts-params-panel'
|
||||
|
||||
// ==================== Mock Setup ====================
|
||||
// All vi.mock() calls are hoisted, so inline all mock data
|
||||
|
||||
// Mock languages data with inline definition
|
||||
vi.mock('@/i18n-config/language', () => ({
|
||||
languages: [
|
||||
{ value: 'en-US', name: 'English (United States)', supported: true },
|
||||
{ value: 'zh-Hans', name: '简体中文', supported: true },
|
||||
{ value: 'ja-JP', name: '日本語', supported: true },
|
||||
{ value: 'unsupported-lang', name: 'Unsupported Language', supported: false },
|
||||
{ value: 'en-US', name: 'English', supported: true },
|
||||
{ value: 'zh-Hans', name: 'Chinese', supported: true },
|
||||
{ value: 'unsupported', name: 'Unsupported', supported: false },
|
||||
],
|
||||
}))
|
||||
|
||||
const MockSelectContext = React.createContext<{
|
||||
value: string
|
||||
onValueChange: (value: string) => void
|
||||
}>({
|
||||
value: '',
|
||||
onValueChange: () => {},
|
||||
})
|
||||
|
||||
vi.mock('@langgenius/dify-ui/select', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@langgenius/dify-ui/select')>()
|
||||
|
||||
return {
|
||||
...actual,
|
||||
Select: ({
|
||||
value,
|
||||
onValueChange,
|
||||
children,
|
||||
}: {
|
||||
value: string
|
||||
onValueChange: (value: string) => void
|
||||
children: React.ReactNode
|
||||
}) => (
|
||||
<MockSelectContext.Provider value={{ value, onValueChange }}>
|
||||
<div data-testid="select-root">{children}</div>
|
||||
</MockSelectContext.Provider>
|
||||
),
|
||||
SelectTrigger: ({
|
||||
children,
|
||||
className,
|
||||
'data-testid': testId,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
'data-testid'?: string
|
||||
}) => (
|
||||
<button data-testid={testId ?? 'select-trigger'} data-class={className}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
SelectValue: () => {
|
||||
const { value } = React.useContext(MockSelectContext)
|
||||
return <span data-testid="selected-value">{value}</span>
|
||||
},
|
||||
SelectContent: ({
|
||||
children,
|
||||
popupClassName,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
popupClassName?: string
|
||||
}) => (
|
||||
<div data-testid="select-content" data-popup-class={popupClassName}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
SelectItem: ({ children, value }: { children: React.ReactNode; value: string }) => {
|
||||
const { onValueChange } = React.useContext(MockSelectContext)
|
||||
return (
|
||||
<button data-testid={`select-item-${value}`} onClick={() => onValueChange(value)}>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
},
|
||||
SelectItemText: ({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}) => <span data-class={className}>{children}</span>,
|
||||
SelectItemIndicator: ({ className }: { className?: string }) => (
|
||||
<span data-testid="select-item-indicator" data-class={className} />
|
||||
),
|
||||
}
|
||||
})
|
||||
|
||||
// ==================== Test Utilities ====================
|
||||
|
||||
/**
|
||||
* Factory function to create a voice item
|
||||
*/
|
||||
const createVoiceItem = (overrides: Partial<{ mode: string; name: string }> = {}) => ({
|
||||
mode: 'alloy',
|
||||
name: 'Alloy',
|
||||
...overrides,
|
||||
})
|
||||
|
||||
/**
|
||||
* Factory function to create a currentModel with voices
|
||||
*/
|
||||
const createCurrentModel = (voices: Array<{ mode: string; name: string }> = []) => ({
|
||||
const model = {
|
||||
model_properties: {
|
||||
voices,
|
||||
voices: [
|
||||
{ mode: 'alloy', name: 'Alloy' },
|
||||
{ mode: 'echo', name: 'Echo' },
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* Factory function to create default props
|
||||
*/
|
||||
const createDefaultProps = (
|
||||
overrides: Partial<{
|
||||
currentModel: { model_properties: { voices: Array<{ mode: string; name: string }> } } | null
|
||||
language: string
|
||||
voice: string
|
||||
onChange: (language: string, voice: string) => void
|
||||
}> = {},
|
||||
) => ({
|
||||
currentModel: createCurrentModel([
|
||||
createVoiceItem({ mode: 'alloy', name: 'Alloy' }),
|
||||
createVoiceItem({ mode: 'echo', name: 'Echo' }),
|
||||
createVoiceItem({ mode: 'fable', name: 'Fable' }),
|
||||
]),
|
||||
language: 'en-US',
|
||||
voice: 'alloy',
|
||||
onChange: vi.fn(),
|
||||
...overrides,
|
||||
})
|
||||
|
||||
// ==================== Tests ====================
|
||||
}
|
||||
|
||||
describe('TTSParamsPanel', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
it('renders the selected language and voice', () => {
|
||||
render(
|
||||
<TTSParamsPanel currentModel={model} language="en-US" voice="alloy" onChange={vi.fn()} />,
|
||||
)
|
||||
|
||||
expect(
|
||||
screen.getByRole('combobox', { name: 'appDebug.voice.voiceSettings.language' }),
|
||||
).toHaveTextContent('en-US')
|
||||
expect(
|
||||
screen.getByRole('combobox', { name: 'appDebug.voice.voiceSettings.voice' }),
|
||||
).toHaveTextContent('alloy')
|
||||
})
|
||||
|
||||
// ==================== Rendering Tests ====================
|
||||
describe('Rendering', () => {
|
||||
it('should render language label', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
it('only exposes supported languages and preserves the selected voice', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onChange = vi.fn()
|
||||
render(
|
||||
<TTSParamsPanel currentModel={model} language="en-US" voice="alloy" onChange={onChange} />,
|
||||
)
|
||||
|
||||
// Act
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
await user.click(
|
||||
screen.getByRole('combobox', { name: 'appDebug.voice.voiceSettings.language' }),
|
||||
)
|
||||
const listbox = await screen.findByRole('listbox')
|
||||
expect(within(listbox).queryByRole('option', { name: 'Unsupported' })).not.toBeInTheDocument()
|
||||
await user.click(within(listbox).getByRole('option', { name: 'Chinese' }))
|
||||
|
||||
// Assert
|
||||
// Assert
|
||||
expect(screen.getByText('appDebug.voice.voiceSettings.language'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render voice label', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
// Assert
|
||||
expect(screen.getByText('appDebug.voice.voiceSettings.voice'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render two Select components', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
const selects = screen.getAllByTestId('select-root')
|
||||
expect(selects).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('should render language select with correct value', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ language: 'zh-Hans' })
|
||||
|
||||
// Act
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
const values = screen.getAllByTestId('selected-value')
|
||||
expect(values[0])!.toHaveTextContent('zh-Hans')
|
||||
})
|
||||
|
||||
it('should render voice select with correct value', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ voice: 'echo' })
|
||||
|
||||
// Act
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
const values = screen.getAllByTestId('selected-value')
|
||||
expect(values[1])!.toHaveTextContent('echo')
|
||||
})
|
||||
|
||||
it('should only show supported languages in language select', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
// Assert
|
||||
expect(screen.getByTestId('select-item-en-US'))!.toBeInTheDocument()
|
||||
expect(screen.getByTestId('select-item-zh-Hans'))!.toBeInTheDocument()
|
||||
expect(screen.getByTestId('select-item-ja-JP'))!.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('select-item-unsupported-lang')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render voice items from currentModel', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
// Assert
|
||||
expect(screen.getByTestId('select-item-alloy'))!.toBeInTheDocument()
|
||||
expect(screen.getByTestId('select-item-echo'))!.toBeInTheDocument()
|
||||
expect(screen.getByTestId('select-item-fable'))!.toBeInTheDocument()
|
||||
})
|
||||
expect(onChange).toHaveBeenCalledWith('zh-Hans', 'alloy')
|
||||
})
|
||||
|
||||
// ==================== Props Testing ====================
|
||||
it('changes the voice while preserving the selected language', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onChange = vi.fn()
|
||||
render(
|
||||
<TTSParamsPanel currentModel={model} language="en-US" voice="alloy" onChange={onChange} />,
|
||||
)
|
||||
|
||||
// ==================== Event Handlers ====================
|
||||
describe('Event Handlers', () => {
|
||||
describe('setLanguage', () => {
|
||||
it('should call onChange with new language and current voice', () => {
|
||||
// Arrange
|
||||
const onChange = vi.fn()
|
||||
const props = createDefaultProps({
|
||||
onChange,
|
||||
language: 'en-US',
|
||||
voice: 'alloy',
|
||||
})
|
||||
await user.click(screen.getByRole('combobox', { name: 'appDebug.voice.voiceSettings.voice' }))
|
||||
await user.click(await screen.findByRole('option', { name: 'Echo' }))
|
||||
|
||||
// Act
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
fireEvent.click(screen.getByTestId('select-item-zh-Hans'))
|
||||
|
||||
// Assert
|
||||
expect(onChange).toHaveBeenCalledWith('zh-Hans', 'alloy')
|
||||
})
|
||||
|
||||
it('should call onChange with different languages', () => {
|
||||
// Arrange
|
||||
const onChange = vi.fn()
|
||||
const props = createDefaultProps({
|
||||
onChange,
|
||||
language: 'en-US',
|
||||
voice: 'echo',
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
fireEvent.click(screen.getByTestId('select-item-ja-JP'))
|
||||
|
||||
// Assert
|
||||
expect(onChange).toHaveBeenCalledWith('ja-JP', 'echo')
|
||||
})
|
||||
|
||||
it('should preserve voice when changing language', () => {
|
||||
// Arrange
|
||||
const onChange = vi.fn()
|
||||
const props = createDefaultProps({
|
||||
onChange,
|
||||
language: 'en-US',
|
||||
voice: 'fable',
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
fireEvent.click(screen.getByTestId('select-item-zh-Hans'))
|
||||
|
||||
// Assert
|
||||
expect(onChange).toHaveBeenCalledWith('zh-Hans', 'fable')
|
||||
})
|
||||
})
|
||||
|
||||
describe('setVoice', () => {
|
||||
it('should call onChange with current language and new voice', () => {
|
||||
// Arrange
|
||||
const onChange = vi.fn()
|
||||
const props = createDefaultProps({
|
||||
onChange,
|
||||
language: 'en-US',
|
||||
voice: 'alloy',
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
fireEvent.click(screen.getByTestId('select-item-echo'))
|
||||
|
||||
// Assert
|
||||
expect(onChange).toHaveBeenCalledWith('en-US', 'echo')
|
||||
})
|
||||
|
||||
it('should call onChange with different voices', () => {
|
||||
// Arrange
|
||||
const onChange = vi.fn()
|
||||
const props = createDefaultProps({
|
||||
onChange,
|
||||
language: 'zh-Hans',
|
||||
voice: 'alloy',
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
fireEvent.click(screen.getByTestId('select-item-fable'))
|
||||
|
||||
// Assert
|
||||
expect(onChange).toHaveBeenCalledWith('zh-Hans', 'fable')
|
||||
})
|
||||
|
||||
it('should preserve language when changing voice', () => {
|
||||
// Arrange
|
||||
const onChange = vi.fn()
|
||||
const props = createDefaultProps({
|
||||
onChange,
|
||||
language: 'ja-JP',
|
||||
voice: 'alloy',
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
fireEvent.click(screen.getByTestId('select-item-echo'))
|
||||
|
||||
// Assert
|
||||
expect(onChange).toHaveBeenCalledWith('ja-JP', 'echo')
|
||||
})
|
||||
})
|
||||
expect(onChange).toHaveBeenCalledWith('en-US', 'echo')
|
||||
})
|
||||
|
||||
// ==================== Memoization ====================
|
||||
describe('Memoization - voiceList', () => {
|
||||
it('should return empty array when currentModel is null', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ currentModel: null })
|
||||
it('renders an empty voice list without a model', async () => {
|
||||
render(<TTSParamsPanel currentModel={null} language="en-US" voice="" onChange={vi.fn()} />)
|
||||
|
||||
// Act
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
fireEvent.click(screen.getByRole('combobox', { name: 'appDebug.voice.voiceSettings.voice' }))
|
||||
|
||||
// Assert - no voice items should be rendered
|
||||
// Assert - no voice items should be rendered
|
||||
// Assert - no voice items should be rendered
|
||||
// Assert - no voice items should be rendered
|
||||
// Assert - no voice items should be rendered
|
||||
// Assert - no voice items should be rendered
|
||||
// Assert - no voice items should be rendered
|
||||
// Assert - no voice items should be rendered
|
||||
// Assert - no voice items should be rendered
|
||||
// Assert - no voice items should be rendered
|
||||
// Assert - no voice items should be rendered
|
||||
// Assert - no voice items should be rendered
|
||||
// Assert - no voice items should be rendered
|
||||
// Assert - no voice items should be rendered
|
||||
// Assert - no voice items should be rendered
|
||||
// Assert - no voice items should be rendered
|
||||
// Assert - no voice items should be rendered
|
||||
// Assert - no voice items should be rendered
|
||||
// Assert - no voice items should be rendered
|
||||
// Assert - no voice items should be rendered
|
||||
// Assert - no voice items should be rendered
|
||||
// Assert - no voice items should be rendered
|
||||
// Assert - no voice items should be rendered
|
||||
// Assert - no voice items should be rendered
|
||||
// Assert - no voice items should be rendered
|
||||
// Assert - no voice items should be rendered
|
||||
// Assert - no voice items should be rendered
|
||||
// Assert - no voice items should be rendered
|
||||
// Assert - no voice items should be rendered
|
||||
// Assert - no voice items should be rendered
|
||||
// Assert - no voice items should be rendered
|
||||
// Assert - no voice items should be rendered
|
||||
expect(screen.queryByTestId('select-item-alloy')).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('select-item-echo')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should return empty array when currentModel is undefined', () => {
|
||||
// Arrange
|
||||
const props = {
|
||||
currentModel: undefined,
|
||||
language: 'en-US',
|
||||
voice: 'alloy',
|
||||
onChange: vi.fn(),
|
||||
}
|
||||
|
||||
// Act
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
expect(screen.queryByTestId('select-item-alloy')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should map voices with mode as value', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({
|
||||
currentModel: createCurrentModel([
|
||||
{ mode: 'voice-1', name: 'Voice One' },
|
||||
{ mode: 'voice-2', name: 'Voice Two' },
|
||||
]),
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
// Assert
|
||||
expect(screen.getByTestId('select-item-voice-1'))!.toBeInTheDocument()
|
||||
expect(screen.getByTestId('select-item-voice-2'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle currentModel with empty voices array', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({
|
||||
currentModel: createCurrentModel([]),
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
|
||||
// Assert - no voice items (except language items)
|
||||
expect(screen.getAllByTestId('select-content')[1]!.children).toHaveLength(0)
|
||||
expect(screen.queryByTestId('select-item-alloy')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle currentModel with single voice', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({
|
||||
currentModel: createCurrentModel([{ mode: 'single-voice', name: 'Single Voice' }]),
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
// Assert
|
||||
expect(screen.getByTestId('select-item-single-voice'))!.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ==================== Edge Cases ====================
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty language value', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ language: '' })
|
||||
|
||||
// Act
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
const values = screen.getAllByTestId('selected-value')
|
||||
expect(values[0])!.toHaveTextContent('')
|
||||
})
|
||||
|
||||
it('should handle empty voice value', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ voice: '' })
|
||||
|
||||
// Act
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
const values = screen.getAllByTestId('selected-value')
|
||||
expect(values[1])!.toHaveTextContent('')
|
||||
})
|
||||
|
||||
it('should handle many voices', () => {
|
||||
// Arrange
|
||||
const manyVoices = Array.from({ length: 20 }, (_, i) => ({
|
||||
mode: `voice-${i}`,
|
||||
name: `Voice ${i}`,
|
||||
}))
|
||||
const props = createDefaultProps({
|
||||
currentModel: createCurrentModel(manyVoices),
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
// Assert
|
||||
expect(screen.getByTestId('select-item-voice-0'))!.toBeInTheDocument()
|
||||
expect(screen.getByTestId('select-item-voice-19'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle voice with special characters in mode', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({
|
||||
currentModel: createCurrentModel([
|
||||
{ mode: 'voice-with_special.chars', name: 'Special Voice' },
|
||||
]),
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
// Assert
|
||||
expect(screen.getByTestId('select-item-voice-with_special.chars'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle onChange not being called multiple times', () => {
|
||||
// Arrange
|
||||
const onChange = vi.fn()
|
||||
const props = createDefaultProps({ onChange })
|
||||
|
||||
// Act
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
fireEvent.click(screen.getByTestId('select-item-echo'))
|
||||
|
||||
// Assert
|
||||
expect(onChange).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
// ==================== Re-render Behavior ====================
|
||||
describe('Re-render Behavior', () => {
|
||||
it('should update when language prop changes', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ language: 'en-US' })
|
||||
|
||||
// Act
|
||||
const { rerender } = render(<TTSParamsPanel {...props} />)
|
||||
const values = screen.getAllByTestId('selected-value')
|
||||
expect(values[0])!.toHaveTextContent('en-US')
|
||||
|
||||
rerender(<TTSParamsPanel {...props} language="zh-Hans" />)
|
||||
|
||||
// Assert
|
||||
const updatedValues = screen.getAllByTestId('selected-value')
|
||||
expect(updatedValues[0])!.toHaveTextContent('zh-Hans')
|
||||
})
|
||||
|
||||
it('should update when voice prop changes', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ voice: 'alloy' })
|
||||
|
||||
// Act
|
||||
const { rerender } = render(<TTSParamsPanel {...props} />)
|
||||
const values = screen.getAllByTestId('selected-value')
|
||||
expect(values[1])!.toHaveTextContent('alloy')
|
||||
|
||||
rerender(<TTSParamsPanel {...props} voice="echo" />)
|
||||
|
||||
// Assert
|
||||
const updatedValues = screen.getAllByTestId('selected-value')
|
||||
expect(updatedValues[1])!.toHaveTextContent('echo')
|
||||
})
|
||||
|
||||
it('should update voice list when currentModel changes', () => {
|
||||
// Arrange
|
||||
const initialModel = createCurrentModel([{ mode: 'alloy', name: 'Alloy' }])
|
||||
const props = createDefaultProps({ currentModel: initialModel })
|
||||
|
||||
// Act
|
||||
const { rerender } = render(<TTSParamsPanel {...props} />)
|
||||
expect(screen.getByTestId('select-item-alloy'))!.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('select-item-nova')).not.toBeInTheDocument()
|
||||
|
||||
const newModel = createCurrentModel([
|
||||
{ mode: 'alloy', name: 'Alloy' },
|
||||
{ mode: 'nova', name: 'Nova' },
|
||||
])
|
||||
rerender(<TTSParamsPanel {...props} currentModel={newModel} />)
|
||||
|
||||
// Assert
|
||||
// Assert
|
||||
expect(screen.getByTestId('select-item-alloy'))!.toBeInTheDocument()
|
||||
expect(screen.getByTestId('select-item-nova'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle currentModel becoming null', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
const { rerender } = render(<TTSParamsPanel {...props} />)
|
||||
expect(screen.getByTestId('select-item-alloy'))!.toBeInTheDocument()
|
||||
|
||||
rerender(<TTSParamsPanel {...props} currentModel={null} />)
|
||||
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
// Assert
|
||||
expect(screen.queryByTestId('select-item-alloy')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ==================== Accessibility ====================
|
||||
describe('Accessibility', () => {
|
||||
it('should have proper label structure for language select', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
const languageLabel = screen.getByText('appDebug.voice.voiceSettings.language')
|
||||
expect(languageLabel)!.toHaveClass('system-sm-semibold')
|
||||
})
|
||||
|
||||
it('should have proper label structure for voice select', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
render(<TTSParamsPanel {...props} />)
|
||||
|
||||
// Assert
|
||||
const voiceLabel = screen.getByText('appDebug.voice.voiceSettings.voice')
|
||||
expect(voiceLabel)!.toHaveClass('system-sm-semibold')
|
||||
})
|
||||
expect(await screen.findByRole('listbox')).toBeEmptyDOMElement()
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,72 +1,10 @@
|
||||
import type { TriggerSubscription } from '@/app/components/workflow/block-selector/types'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { TriggerCredentialType } from '@/app/components/workflow/block-selector/types'
|
||||
import { SubscriptionSelectorEntry } from '../selector-entry'
|
||||
|
||||
vi.mock('@langgenius/dify-ui/popover', async () => {
|
||||
const React = await import('react')
|
||||
const PopoverContext = React.createContext({
|
||||
open: false,
|
||||
setOpen: (_open: boolean) => {},
|
||||
})
|
||||
|
||||
const Popover = ({
|
||||
children,
|
||||
open: controlledOpen,
|
||||
onOpenChange,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
}) => {
|
||||
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(false)
|
||||
const isControlled = controlledOpen !== undefined
|
||||
const open = isControlled ? !!controlledOpen : uncontrolledOpen
|
||||
const setOpen = (nextOpen: boolean) => {
|
||||
if (!isControlled) setUncontrolledOpen(nextOpen)
|
||||
onOpenChange?.(nextOpen)
|
||||
}
|
||||
|
||||
return <PopoverContext.Provider value={{ open, setOpen }}>{children}</PopoverContext.Provider>
|
||||
}
|
||||
|
||||
type TriggerProps = React.HTMLAttributes<HTMLElement> & {
|
||||
'data-popup-open'?: string
|
||||
'data-testid'?: string
|
||||
}
|
||||
|
||||
const PopoverTrigger = ({
|
||||
render,
|
||||
}: {
|
||||
render:
|
||||
| React.ReactNode
|
||||
| ((props: TriggerProps, state: { open: boolean }) => React.ReactElement)
|
||||
}) => {
|
||||
const { open, setOpen } = React.useContext(PopoverContext)
|
||||
const props: TriggerProps = {
|
||||
'data-testid': 'popover-trigger',
|
||||
'data-popup-open': open ? '' : undefined,
|
||||
onClick: () => setOpen(!open),
|
||||
}
|
||||
|
||||
if (typeof render === 'function') return render(props, { open })
|
||||
|
||||
return <div {...props}>{render}</div>
|
||||
}
|
||||
|
||||
const PopoverContent = ({ children }: { children: React.ReactNode }) => {
|
||||
const { open } = React.useContext(PopoverContext)
|
||||
return open ? <div data-testid="popover-content">{children}</div> : null
|
||||
}
|
||||
|
||||
return {
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
PopoverContent,
|
||||
}
|
||||
})
|
||||
|
||||
let mockSubscriptions: TriggerSubscription[] = []
|
||||
const mockRefetch = vi.fn()
|
||||
|
||||
@ -128,16 +66,15 @@ describe('SubscriptionSelectorEntry', () => {
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render placeholder when open without selection', () => {
|
||||
it('should render placeholder when open without selection', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<SubscriptionSelectorEntry selectedId={undefined} onSelect={vi.fn()} />)
|
||||
|
||||
const trigger = screen.getByTestId('popover-trigger')
|
||||
expect(trigger).not.toHaveAttribute('data-popup-open')
|
||||
await user.click(screen.getByRole('button'))
|
||||
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
|
||||
expect(trigger).toHaveAttribute('data-popup-open', '')
|
||||
expect(screen.getByText('pluginTrigger.subscription.selectPlaceholder')).toBeInTheDocument()
|
||||
expect(
|
||||
await screen.findByText('pluginTrigger.subscription.selectPlaceholder'),
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show selected subscription name when id matches', () => {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -2,66 +2,13 @@ import type { ReactNode } from 'react'
|
||||
import type { AppSelectorValue } from '@/app/components/plugins/plugin-detail-panel/app-selector'
|
||||
import type { ToolFormSchema } from '@/app/components/tools/utils/to-form-schema'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { Type } from '@/app/components/workflow/nodes/llm/types'
|
||||
import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
|
||||
import ReasoningConfigForm from '../reasoning-config-form'
|
||||
|
||||
vi.mock('@langgenius/dify-ui/select', async () => {
|
||||
const React = await import('react')
|
||||
const SelectContext = React.createContext<{
|
||||
onValueChange?: (value: string) => void
|
||||
}>({})
|
||||
|
||||
return {
|
||||
Select: ({
|
||||
children,
|
||||
onValueChange,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
onValueChange?: (value: string) => void
|
||||
}) => (
|
||||
<SelectContext value={{ onValueChange }}>
|
||||
<div>{children}</div>
|
||||
</SelectContext>
|
||||
),
|
||||
SelectTrigger: ({ children }: { children: React.ReactNode }) => (
|
||||
<button type="button">{children}</button>
|
||||
),
|
||||
SelectContent: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
SelectItem: ({ children, value }: { children: React.ReactNode; value: string }) => {
|
||||
const context = React.use(SelectContext)
|
||||
return (
|
||||
<button
|
||||
key={value}
|
||||
data-testid={`select-${value}`}
|
||||
type="button"
|
||||
onClick={() => context.onValueChange?.(value)}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
},
|
||||
SelectItemText: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
||||
SelectItemIndicator: () => null,
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@langgenius/dify-ui/switch', () => ({
|
||||
Switch: ({
|
||||
checked,
|
||||
onCheckedChange,
|
||||
}: {
|
||||
checked: boolean
|
||||
onCheckedChange: (checked: boolean) => void
|
||||
}) => (
|
||||
<button data-testid="auto-switch" onClick={() => onCheckedChange(!checked)}>
|
||||
{checked ? 'on' : 'off'}
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
|
||||
useLanguage: () => 'en_US',
|
||||
}))
|
||||
@ -211,7 +158,7 @@ describe('ReasoningConfigForm', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('auto-switch'))
|
||||
fireEvent.click(screen.getByRole('switch'))
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith({
|
||||
field: {
|
||||
@ -407,7 +354,8 @@ describe('ReasoningConfigForm', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should update number, boolean, and select fields', () => {
|
||||
it('should update number, boolean, and select fields', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onChange = vi.fn()
|
||||
|
||||
render(
|
||||
@ -469,7 +417,8 @@ describe('ReasoningConfigForm', () => {
|
||||
|
||||
fireEvent.change(screen.getByRole('spinbutton'), { target: { value: '7' } })
|
||||
fireEvent.click(screen.getByTestId('boolean-input'))
|
||||
fireEvent.click(screen.getByTestId('select-beta'))
|
||||
await user.click(screen.getByRole('combobox'))
|
||||
await user.click(await screen.findByRole('option', { name: 'Beta' }))
|
||||
|
||||
expect(onChange).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
|
||||
@ -2,15 +2,6 @@ import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { SchemaModal } from '../schema-modal'
|
||||
|
||||
vi.mock('@langgenius/dify-ui/dialog', () => ({
|
||||
Dialog: ({ children, open }: { children: React.ReactNode; open?: boolean }) =>
|
||||
open === false ? null : <>{children}</>,
|
||||
DialogContent: ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="modal">{children}</div>
|
||||
),
|
||||
DialogTitle: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
}))
|
||||
|
||||
vi.mock(
|
||||
'@/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor',
|
||||
() => ({
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { MetaData, PluginCategoryEnum } from '../../types'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { expectLoadingButton } from '@/test/button'
|
||||
// ==================== Imports (after mocks) ====================
|
||||
@ -94,8 +95,6 @@ vi.mock('../../plugin-page/plugin-info', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/tooltip', () => import('@/__mocks__/base-ui-tooltip'))
|
||||
|
||||
// ==================== Test Utilities ====================
|
||||
|
||||
type ActionProps = {
|
||||
@ -229,7 +228,8 @@ describe('Action Component', () => {
|
||||
expect(queryActionButtons()).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should render tooltips for each button', () => {
|
||||
it('should render tooltips for each button', async () => {
|
||||
const user = userEvent.setup()
|
||||
// Arrange
|
||||
const props = createActionProps({
|
||||
isShowFetchNewVersion: true,
|
||||
@ -242,16 +242,16 @@ describe('Action Component', () => {
|
||||
|
||||
// Assert
|
||||
const buttons = getActionButtons()
|
||||
fireEvent.mouseEnter(buttons[0]!)
|
||||
expect(screen.getByText('plugin.action.checkForUpdates'))!.toBeInTheDocument()
|
||||
fireEvent.mouseLeave(buttons[0]!)
|
||||
await user.hover(buttons[0]!)
|
||||
expect(await screen.findByText('plugin.action.checkForUpdates'))!.toBeInTheDocument()
|
||||
await user.unhover(buttons[0]!)
|
||||
|
||||
fireEvent.mouseEnter(buttons[1]!)
|
||||
expect(screen.getByText('plugin.action.pluginInfo'))!.toBeInTheDocument()
|
||||
fireEvent.mouseLeave(buttons[1]!)
|
||||
await user.hover(buttons[1]!)
|
||||
expect(await screen.findByText('plugin.action.pluginInfo'))!.toBeInTheDocument()
|
||||
await user.unhover(buttons[1]!)
|
||||
|
||||
fireEvent.mouseEnter(buttons[2]!)
|
||||
expect(screen.getByText('plugin.action.delete'))!.toBeInTheDocument()
|
||||
await user.hover(buttons[2]!)
|
||||
expect(await screen.findByText('plugin.action.delete'))!.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -45,149 +45,6 @@ vi.mock('@remixicon/react', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
type MockButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
|
||||
variant?: string
|
||||
}
|
||||
|
||||
vi.mock('@langgenius/dify-ui/button', () => ({
|
||||
Button: ({ children, onClick, className, variant, ...props }: MockButtonProps) => (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="button-content"
|
||||
data-variant={variant}
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/dropdown-menu', async () => {
|
||||
const React = await import('react')
|
||||
const DropdownMenuContext = React.createContext<{
|
||||
isOpen: boolean
|
||||
setOpen: (open: boolean) => void
|
||||
} | null>(null)
|
||||
|
||||
const useDropdownMenuContext = () => {
|
||||
const context = React.use(DropdownMenuContext)
|
||||
if (!context) throw new Error('DropdownMenu components must be wrapped in DropdownMenu')
|
||||
return context
|
||||
}
|
||||
|
||||
return {
|
||||
DropdownMenu: ({
|
||||
open,
|
||||
onOpenChange,
|
||||
modal,
|
||||
children,
|
||||
}: {
|
||||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
modal?: boolean
|
||||
children: React.ReactNode
|
||||
}) => {
|
||||
const [internalOpen, setInternalOpen] = React.useState(open ?? false)
|
||||
const isOpen = open ?? internalOpen
|
||||
const setOpen = (nextOpen: boolean) => {
|
||||
if (open === undefined) setInternalOpen(nextOpen)
|
||||
onOpenChange?.(nextOpen)
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenuContext value={{ isOpen, setOpen }}>
|
||||
<div data-testid="dropdown-menu" data-open={isOpen} data-modal={modal}>
|
||||
{children}
|
||||
</div>
|
||||
</DropdownMenuContext>
|
||||
)
|
||||
},
|
||||
DropdownMenuTrigger: ({
|
||||
children,
|
||||
onClick,
|
||||
render,
|
||||
}: {
|
||||
children?: React.ReactNode
|
||||
onClick?: React.MouseEventHandler<HTMLElement>
|
||||
render?:
|
||||
| React.ReactElement
|
||||
| ((
|
||||
props: React.HTMLAttributes<HTMLElement> & {
|
||||
'data-testid'?: string
|
||||
'data-popup-open'?: string
|
||||
},
|
||||
state: { open: boolean },
|
||||
) => React.ReactElement)
|
||||
}) => {
|
||||
const { isOpen, setOpen } = useDropdownMenuContext()
|
||||
const handleClick = (e: React.MouseEvent<HTMLElement>) => {
|
||||
onClick?.(e)
|
||||
setOpen(!isOpen)
|
||||
}
|
||||
|
||||
if (typeof render === 'function')
|
||||
return render(
|
||||
{
|
||||
'data-testid': 'dropdown-trigger',
|
||||
'data-popup-open': isOpen ? '' : undefined,
|
||||
onClick: handleClick,
|
||||
},
|
||||
{ open: isOpen },
|
||||
)
|
||||
|
||||
if (render)
|
||||
return React.cloneElement(
|
||||
render,
|
||||
{ 'data-testid': 'dropdown-trigger', onClick: handleClick } as Record<string, unknown>,
|
||||
children,
|
||||
)
|
||||
|
||||
return (
|
||||
<button data-testid="dropdown-trigger" onClick={handleClick}>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
},
|
||||
DropdownMenuContent: ({
|
||||
children,
|
||||
popupClassName,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
popupClassName?: string
|
||||
}) => {
|
||||
const { isOpen } = useDropdownMenuContext()
|
||||
return isOpen ? (
|
||||
<div data-testid="dropdown-content" className={popupClassName}>
|
||||
{children}
|
||||
</div>
|
||||
) : null
|
||||
},
|
||||
DropdownMenuItem: ({
|
||||
children,
|
||||
onClick,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
onClick?: React.MouseEventHandler<HTMLButtonElement>
|
||||
}) => {
|
||||
const { setOpen } = useDropdownMenuContext()
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="dropdown-item"
|
||||
onClick={(e) => {
|
||||
onClick?.(e)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/app/components/plugins/install-plugin/install-from-github', () => ({
|
||||
default: ({ onClose }: { onClose: () => void }) => (
|
||||
<div data-testid="github-modal">
|
||||
@ -209,6 +66,8 @@ vi.mock('@/app/components/plugins/install-plugin/install-from-local-package', ()
|
||||
),
|
||||
}))
|
||||
|
||||
const getTrigger = (name = 'plugin.installPlugin') => screen.getByRole('button', { name })
|
||||
|
||||
describe('InstallPluginDropdown', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@ -217,24 +76,23 @@ describe('InstallPluginDropdown', () => {
|
||||
})
|
||||
|
||||
it('shows all install methods when marketplace and custom installs are enabled', () => {
|
||||
const { container } = render(<InstallPluginDropdown onSwitchToMarketplaceTab={vi.fn()} />)
|
||||
render(<InstallPluginDropdown onSwitchToMarketplaceTab={vi.fn()} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('dropdown-trigger'))
|
||||
fireEvent.click(getTrigger())
|
||||
|
||||
expect(screen.getByTestId('dropdown-menu')).toHaveAttribute('data-modal', 'false')
|
||||
expect(screen.getByText('plugin.installFrom')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.source.marketplace')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.source.github')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.source.local')).toBeInTheDocument()
|
||||
expect(container.querySelector('.i-custom-vender-plugin-box-sparkle-fill')).toHaveClass(
|
||||
expect(document.querySelector('.i-custom-vender-plugin-box-sparkle-fill')).toHaveClass(
|
||||
'size-4',
|
||||
'shrink-0',
|
||||
)
|
||||
expect(container.querySelector('.i-custom-vender-solid-general-github')).toHaveClass(
|
||||
expect(document.querySelector('.i-custom-vender-solid-general-github')).toHaveClass(
|
||||
'size-4',
|
||||
'shrink-0',
|
||||
)
|
||||
expect(container.querySelector('.i-custom-vender-solid-files-file-zip')).toHaveClass(
|
||||
expect(document.querySelector('.i-custom-vender-solid-files-file-zip')).toHaveClass(
|
||||
'size-4',
|
||||
'shrink-0',
|
||||
)
|
||||
@ -253,21 +111,20 @@ describe('InstallPluginDropdown', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
const trigger = screen.getByTestId('dropdown-trigger')
|
||||
const trigger = getTrigger('Install')
|
||||
|
||||
expect(container.querySelector('.custom-root')).toBeInTheDocument()
|
||||
expect(trigger).toHaveTextContent('Install')
|
||||
expect(screen.getByTestId('add-circle-fill-icon')).toHaveClass('size-4', 'shrink-0')
|
||||
expect(screen.getByTestId('arrow-down-icon')).toHaveClass('ml-1', 'size-4')
|
||||
expect(trigger).toHaveClass('custom-trigger')
|
||||
expect(trigger).toHaveAttribute('data-variant', 'primary')
|
||||
expect(trigger).not.toHaveAttribute('data-popup-open')
|
||||
|
||||
fireEvent.click(trigger)
|
||||
|
||||
expect(trigger).toHaveClass('custom-open')
|
||||
expect(trigger).toHaveAttribute('data-popup-open', '')
|
||||
expect(screen.getByTestId('dropdown-content')).toHaveClass('custom-popup')
|
||||
expect(screen.getByRole('menu')).toHaveClass('custom-popup')
|
||||
})
|
||||
|
||||
it('can hide the trigger arrow for compact integrations placement', () => {
|
||||
@ -279,7 +136,7 @@ describe('InstallPluginDropdown', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
const trigger = screen.getByTestId('dropdown-trigger')
|
||||
const trigger = getTrigger('Install')
|
||||
|
||||
expect(trigger).toHaveTextContent('Install')
|
||||
expect(screen.getByTestId('add-circle-fill-icon')).toHaveClass('size-4', 'shrink-0')
|
||||
@ -293,7 +150,7 @@ describe('InstallPluginDropdown', () => {
|
||||
<InstallPluginDropdown disabled onSwitchToMarketplaceTab={onSwitchToMarketplaceTab} />,
|
||||
)
|
||||
|
||||
const trigger = screen.getByTestId('dropdown-trigger')
|
||||
const trigger = getTrigger()
|
||||
|
||||
expect(trigger).toBeDisabled()
|
||||
|
||||
@ -304,7 +161,7 @@ describe('InstallPluginDropdown', () => {
|
||||
},
|
||||
})
|
||||
|
||||
expect(screen.queryByTestId('dropdown-content')).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('menu')).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('local-modal')).not.toBeInTheDocument()
|
||||
expect(onSwitchToMarketplaceTab).not.toHaveBeenCalled()
|
||||
})
|
||||
@ -314,7 +171,7 @@ describe('InstallPluginDropdown', () => {
|
||||
|
||||
render(<InstallPluginDropdown onSwitchToMarketplaceTab={vi.fn()} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('dropdown-trigger'))
|
||||
fireEvent.click(getTrigger())
|
||||
|
||||
expect(screen.getByText('plugin.source.marketplace')).toBeInTheDocument()
|
||||
expect(screen.queryByText('plugin.source.github')).not.toBeInTheDocument()
|
||||
@ -325,7 +182,7 @@ describe('InstallPluginDropdown', () => {
|
||||
const onSwitchToMarketplaceTab = vi.fn()
|
||||
render(<InstallPluginDropdown onSwitchToMarketplaceTab={onSwitchToMarketplaceTab} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('dropdown-trigger'))
|
||||
fireEvent.click(getTrigger())
|
||||
fireEvent.click(screen.getByText('plugin.source.marketplace'))
|
||||
|
||||
expect(onSwitchToMarketplaceTab).toHaveBeenCalledTimes(1)
|
||||
@ -334,7 +191,7 @@ describe('InstallPluginDropdown', () => {
|
||||
it('opens the github installer when github is selected', async () => {
|
||||
render(<InstallPluginDropdown onSwitchToMarketplaceTab={vi.fn()} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('dropdown-trigger'))
|
||||
fireEvent.click(getTrigger())
|
||||
fireEvent.click(screen.getByText('plugin.source.github'))
|
||||
|
||||
expect(await screen.findByTestId('github-modal')).toBeInTheDocument()
|
||||
@ -343,7 +200,7 @@ describe('InstallPluginDropdown', () => {
|
||||
it('opens the local package installer when a file is selected', () => {
|
||||
const { container } = render(<InstallPluginDropdown onSwitchToMarketplaceTab={vi.fn()} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('dropdown-trigger'))
|
||||
fireEvent.click(getTrigger())
|
||||
fireEvent.click(screen.getByText('plugin.source.local'))
|
||||
fireEvent.change(container.querySelector('input[type="file"]')!, {
|
||||
target: {
|
||||
@ -360,7 +217,7 @@ describe('InstallPluginDropdown', () => {
|
||||
|
||||
render(<InstallPluginDropdown onSwitchToMarketplaceTab={vi.fn()} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('dropdown-trigger'))
|
||||
fireEvent.click(getTrigger())
|
||||
fireEvent.click(screen.getByText('plugin.source.local'))
|
||||
|
||||
expect(clickSpy).toHaveBeenCalledTimes(1)
|
||||
@ -370,7 +227,7 @@ describe('InstallPluginDropdown', () => {
|
||||
it('closes the github installer when the modal requests close', async () => {
|
||||
render(<InstallPluginDropdown onSwitchToMarketplaceTab={vi.fn()} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('dropdown-trigger'))
|
||||
fireEvent.click(getTrigger())
|
||||
fireEvent.click(screen.getByText('plugin.source.github'))
|
||||
fireEvent.click(await screen.findByTestId('close-github-modal'))
|
||||
|
||||
@ -380,7 +237,7 @@ describe('InstallPluginDropdown', () => {
|
||||
it('closes the local package installer when the modal requests close', () => {
|
||||
const { container } = render(<InstallPluginDropdown onSwitchToMarketplaceTab={vi.fn()} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('dropdown-trigger'))
|
||||
fireEvent.click(getTrigger())
|
||||
fireEvent.click(screen.getByText('plugin.source.local'))
|
||||
fireEvent.change(container.querySelector('input[type="file"]')!, {
|
||||
target: {
|
||||
|
||||
@ -2,8 +2,6 @@ import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('@langgenius/dify-ui/popover', () => import('@/__mocks__/base-ui-popover'))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/cn', () => ({
|
||||
cn: (...args: unknown[]) => args.filter(Boolean).join(' '),
|
||||
}))
|
||||
@ -57,7 +55,7 @@ describe('CategoriesFilter', () => {
|
||||
const mockOnChange = vi.fn()
|
||||
render(<CategoriesFilter value={['tool']} onChange={mockOnChange} />)
|
||||
|
||||
const trigger = screen.getByTestId('popover-trigger')
|
||||
const trigger = screen.getByRole('button', { name: /Tool/ })
|
||||
const clearSvg = trigger.querySelector('svg')
|
||||
fireEvent.click(clearSvg!)
|
||||
expect(mockOnChange).toHaveBeenCalledWith([])
|
||||
@ -65,7 +63,7 @@ describe('CategoriesFilter', () => {
|
||||
|
||||
it('should render category options in dropdown', () => {
|
||||
render(<CategoriesFilter value={[]} onChange={vi.fn()} />)
|
||||
fireEvent.click(screen.getByTestId('popover-trigger'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'plugin.allCategories' }))
|
||||
|
||||
expect(screen.getByText('Tool'))!.toBeInTheDocument()
|
||||
expect(screen.getByText('Model'))!.toBeInTheDocument()
|
||||
@ -76,7 +74,7 @@ describe('CategoriesFilter', () => {
|
||||
const mockOnChange = vi.fn()
|
||||
render(<CategoriesFilter value={[]} onChange={mockOnChange} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('popover-trigger'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'plugin.allCategories' }))
|
||||
fireEvent.click(screen.getByText('Tool'))
|
||||
expect(mockOnChange).toHaveBeenCalledWith(['tool'])
|
||||
})
|
||||
@ -85,7 +83,7 @@ describe('CategoriesFilter', () => {
|
||||
const mockOnChange = vi.fn()
|
||||
render(<CategoriesFilter value={['tool']} onChange={mockOnChange} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('popover-trigger'))
|
||||
fireEvent.click(screen.getByRole('button', { name: /Tool/ }))
|
||||
const toolElements = screen.getAllByText('Tool')
|
||||
fireEvent.click(toolElements[toolElements.length - 1]!)
|
||||
expect(mockOnChange).toHaveBeenCalledWith([])
|
||||
@ -94,7 +92,7 @@ describe('CategoriesFilter', () => {
|
||||
it('should filter categories by search text', () => {
|
||||
render(<CategoriesFilter value={[]} onChange={vi.fn()} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('popover-trigger'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'plugin.allCategories' }))
|
||||
fireEvent.change(screen.getByPlaceholderText('plugin.searchCategories'), {
|
||||
target: { value: 'mod' },
|
||||
})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -18,8 +18,6 @@ vi.mock('../../../hooks', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/popover', () => import('@/__mocks__/base-ui-popover'))
|
||||
|
||||
describe('TagFilter', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@ -38,12 +36,12 @@ describe('TagFilter', () => {
|
||||
expect(screen.getByText('+1')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('filters options by search text and toggles tag selection', () => {
|
||||
it('filters options by search text and toggles tag selection', async () => {
|
||||
const onChange = vi.fn()
|
||||
render(<TagFilter value={['agent']} onChange={onChange} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('popover-trigger'))
|
||||
const portal = screen.getByTestId('popover-content')
|
||||
fireEvent.click(screen.getByRole('button', { name: /Agent/ }))
|
||||
const portal = await screen.findByRole('dialog')
|
||||
|
||||
fireEvent.change(screen.getByPlaceholderText('pluginTags.searchTags'), {
|
||||
target: { value: 'ra' },
|
||||
@ -61,18 +59,18 @@ describe('TagFilter', () => {
|
||||
const onChange = vi.fn()
|
||||
render(<TagFilter value={['agent']} onChange={onChange} />)
|
||||
|
||||
const trigger = screen.getByTestId('popover-trigger')
|
||||
const trigger = screen.getByRole('button', { name: /Agent/ })
|
||||
fireEvent.click(trigger.querySelector('.i-ri-close-circle-fill')!)
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith([])
|
||||
})
|
||||
|
||||
it('removes a selected tag when clicking the same option again', () => {
|
||||
it('removes a selected tag when clicking the same option again', async () => {
|
||||
const onChange = vi.fn()
|
||||
render(<TagFilter value={['agent']} onChange={onChange} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('popover-trigger'))
|
||||
fireEvent.click(within(screen.getByTestId('popover-content')).getByText('Agent'))
|
||||
fireEvent.click(screen.getByRole('button', { name: /Agent/ }))
|
||||
fireEvent.click(within(await screen.findByRole('dialog')).getByText('Agent'))
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith([])
|
||||
})
|
||||
|
||||
@ -17,33 +17,6 @@ const mockSystemFeatures = {
|
||||
const render = (ui: ReactElement) =>
|
||||
renderWithConsoleQuery(ui, { systemFeatures: mockSystemFeatures })
|
||||
|
||||
let mockDialogOnOpenChange: ((open: boolean) => void) | undefined
|
||||
|
||||
vi.mock('@langgenius/dify-ui/dialog', () => ({
|
||||
Dialog: ({
|
||||
children,
|
||||
open,
|
||||
onOpenChange,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
}) => {
|
||||
mockDialogOnOpenChange = onOpenChange
|
||||
return open === false ? null : <>{children}</>
|
||||
},
|
||||
DialogContent: ({ children, className }: { children: React.ReactNode; className?: string }) => (
|
||||
<div data-testid="modal" className={className}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
DialogCloseButton: () => (
|
||||
<button data-testid="modal-close" onClick={() => mockDialogOnOpenChange?.(false)}>
|
||||
Close
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock OptionCard component
|
||||
vi.mock('@/app/components/workflow/nodes/_base/components/option-card', () => ({
|
||||
default: ({
|
||||
@ -280,7 +253,7 @@ describe('reference-setting-modal', () => {
|
||||
|
||||
// Assert
|
||||
// Assert
|
||||
expect(screen.getByTestId('modal-close'))!.toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'Close' }))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should disable permission controls with settings permissions tooltip beside titles when RBAC is enabled', () => {
|
||||
@ -381,7 +354,7 @@ describe('reference-setting-modal', () => {
|
||||
|
||||
// Act
|
||||
render(<ReferenceSettingModal {...defaultProps} onHide={onHide} />)
|
||||
fireEvent.click(screen.getByTestId('modal-close'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Close' }))
|
||||
|
||||
// Assert
|
||||
expect(onHide).toHaveBeenCalledTimes(1)
|
||||
@ -843,7 +816,7 @@ describe('reference-setting-modal', () => {
|
||||
|
||||
// Assert - modal should be visible
|
||||
// Assert - modal should be visible
|
||||
expect(screen.getByTestId('modal'))!.toBeInTheDocument()
|
||||
expect(screen.getByRole('dialog'))!.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -88,126 +88,11 @@ vi.mock('@/service/use-plugins', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock popover component for ToolPicker and StrategyPicker
|
||||
let mockPopoverOpen = false
|
||||
let forcePopoverContentVisible = false // Allow tests to force content visibility
|
||||
let mockPopoverOnOpenChange: ((open: boolean) => void) | undefined
|
||||
vi.mock('@langgenius/dify-ui/popover', () => ({
|
||||
Popover: ({
|
||||
children,
|
||||
open = false,
|
||||
onOpenChange,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
}) => {
|
||||
mockPopoverOpen = open
|
||||
mockPopoverOnOpenChange = onOpenChange
|
||||
return (
|
||||
<div data-testid="popover" data-open={open}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
PopoverTrigger: ({
|
||||
children,
|
||||
render,
|
||||
onClick,
|
||||
className,
|
||||
}: {
|
||||
children?: React.ReactNode
|
||||
render?: React.ReactNode
|
||||
onClick?: (e: React.MouseEvent) => void
|
||||
className?: string
|
||||
}) => (
|
||||
<div
|
||||
data-testid="popover-trigger"
|
||||
role="button"
|
||||
aria-label="popover trigger"
|
||||
tabIndex={0}
|
||||
onClick={(e) => {
|
||||
onClick?.(e)
|
||||
if (!onClick) mockPopoverOnOpenChange?.(!mockPopoverOpen)
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if ((e.key === 'Enter' || e.key === ' ') && !onClick)
|
||||
mockPopoverOnOpenChange?.(!mockPopoverOpen)
|
||||
}}
|
||||
className={className}
|
||||
>
|
||||
{render ?? children}
|
||||
</div>
|
||||
),
|
||||
PopoverContent: ({
|
||||
children,
|
||||
className,
|
||||
popupClassName,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
popupClassName?: string
|
||||
}) => {
|
||||
if (!mockPopoverOpen && !forcePopoverContentVisible) return null
|
||||
return (
|
||||
<div
|
||||
data-testid="popover-content"
|
||||
className={[className, popupClassName].filter(Boolean).join(' ')}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock TimePicker component - simplified stateless mock
|
||||
vi.mock('@/app/components/base/date-and-time-picker/time-picker', () => ({
|
||||
default: ({
|
||||
value,
|
||||
onChange,
|
||||
onClear,
|
||||
renderTrigger,
|
||||
}: {
|
||||
value: { format: (f: string) => string }
|
||||
onChange: (v: unknown) => void
|
||||
onClear: () => void
|
||||
title?: string
|
||||
renderTrigger: (
|
||||
props: React.HTMLAttributes<HTMLElement>,
|
||||
state: { open: boolean },
|
||||
params: { inputElem: React.ReactNode; onClick: () => void },
|
||||
) => React.ReactNode
|
||||
}) => {
|
||||
const inputElem = <span data-testid="time-input">{value.format('HH:mm')}</span>
|
||||
|
||||
return (
|
||||
<div data-testid="time-picker">
|
||||
{renderTrigger({}, { open: false }, { inputElem, onClick: () => {} })}
|
||||
<div data-testid="time-picker-dropdown">
|
||||
<button
|
||||
data-testid="time-picker-set"
|
||||
onClick={() => {
|
||||
onChange(dayjs().hour(10).minute(30))
|
||||
}}
|
||||
>
|
||||
Set 10:30
|
||||
</button>
|
||||
<button
|
||||
data-testid="time-picker-clear"
|
||||
onClick={() => {
|
||||
onClear()
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock utils from date-and-time-picker
|
||||
vi.mock('@/app/components/base/date-and-time-picker/utils/dayjs', () => ({
|
||||
vi.mock('@/app/components/base/date-and-time-picker/utils/dayjs', async (importOriginal) => ({
|
||||
...(await importOriginal<
|
||||
typeof import('@/app/components/base/date-and-time-picker/utils/dayjs')
|
||||
>()),
|
||||
convertTimezoneToOffsetStr: (tz: string) => {
|
||||
if (tz === 'America/New_York') return 'GMT-5'
|
||||
if (tz === 'Asia/Shanghai') return 'GMT+8'
|
||||
@ -390,9 +275,6 @@ const renderWithQueryClient = (ui: React.ReactElement) => {
|
||||
describe('auto-update-setting', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockPopoverOpen = false
|
||||
mockPopoverOnOpenChange = undefined
|
||||
forcePopoverContentVisible = false
|
||||
mockPluginsData.plugins = []
|
||||
})
|
||||
|
||||
@ -862,7 +744,6 @@ describe('auto-update-setting', () => {
|
||||
|
||||
it('should render search box and tabs when isShow is true', () => {
|
||||
// Arrange
|
||||
mockPopoverOpen = true
|
||||
|
||||
// Act
|
||||
render(<ToolPicker {...defaultProps} isShow={true} />)
|
||||
@ -873,7 +754,6 @@ describe('auto-update-setting', () => {
|
||||
|
||||
it('should show NoDataPlaceholder when no plugins and no search query', () => {
|
||||
// Arrange
|
||||
mockPopoverOpen = true
|
||||
mockPluginsData.plugins = []
|
||||
|
||||
// Act
|
||||
@ -915,7 +795,6 @@ describe('auto-update-setting', () => {
|
||||
|
||||
it('should filter out non-marketplace plugins', () => {
|
||||
// Arrange
|
||||
mockPopoverOpen = true
|
||||
|
||||
// Act
|
||||
renderWithQueryClient(<ToolPicker {...defaultProps} isShow={true} />)
|
||||
@ -926,7 +805,6 @@ describe('auto-update-setting', () => {
|
||||
|
||||
it('should filter by search query', () => {
|
||||
// Arrange
|
||||
mockPopoverOpen = true
|
||||
|
||||
// Act
|
||||
renderWithQueryClient(<ToolPicker {...defaultProps} isShow={true} />)
|
||||
@ -947,15 +825,14 @@ describe('auto-update-setting', () => {
|
||||
|
||||
// Act
|
||||
render(<ToolPicker {...defaultProps} onShowChange={onShowChange} />)
|
||||
fireEvent.click(screen.getByTestId('popover-trigger'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Select Plugins' }))
|
||||
|
||||
// Assert
|
||||
expect(onShowChange).toHaveBeenCalledWith(true)
|
||||
expect(onShowChange).toHaveBeenCalledWith(true, expect.any(Object))
|
||||
})
|
||||
|
||||
it('should call onChange when plugin is selected', () => {
|
||||
// Arrange
|
||||
mockPopoverOpen = true
|
||||
mockPluginsData.plugins = [
|
||||
createMockPluginDetail({
|
||||
plugin_id: 'test-plugin',
|
||||
@ -977,7 +854,6 @@ describe('auto-update-setting', () => {
|
||||
|
||||
it('should unselect plugin when already selected', () => {
|
||||
// Arrange
|
||||
mockPopoverOpen = true
|
||||
mockPluginsData.plugins = [
|
||||
createMockPluginDetail({
|
||||
plugin_id: 'test-plugin',
|
||||
@ -1128,7 +1004,6 @@ describe('auto-update-setting', () => {
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('plugin.autoUpdate.updateTime')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('time-picker')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should hide time picker and plugins selection when strategy is disabled', () => {
|
||||
@ -1142,7 +1017,6 @@ describe('auto-update-setting', () => {
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByText('plugin.autoUpdate.updateTime')).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('time-picker')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show plugins picker when mode is not update_all', () => {
|
||||
@ -1284,46 +1158,6 @@ describe('auto-update-setting', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('should call onChange with updated time when time changes', () => {
|
||||
// Arrange
|
||||
const onChange = vi.fn()
|
||||
const payload = createMockAutoUpdateConfig({
|
||||
strategy_setting: AUTO_UPDATE_STRATEGY.fixOnly,
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<AutoUpdateSetting payload={payload} onChange={onChange} />)
|
||||
|
||||
// Click time picker trigger
|
||||
fireEvent.click(screen.getByRole('button', { name: /GMT-5/ }))
|
||||
|
||||
// Set time
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Set 10:30' }))
|
||||
|
||||
// Assert
|
||||
expect(onChange).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call onChange with 0 when time is cleared', () => {
|
||||
// Arrange
|
||||
const onChange = vi.fn()
|
||||
const payload = createMockAutoUpdateConfig({
|
||||
strategy_setting: AUTO_UPDATE_STRATEGY.fixOnly,
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<AutoUpdateSetting payload={payload} onChange={onChange} />)
|
||||
|
||||
// Click time picker trigger
|
||||
fireEvent.click(screen.getByRole('button', { name: /GMT-5/ }))
|
||||
|
||||
// Clear time
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Clear' }))
|
||||
|
||||
// Assert
|
||||
expect(onChange).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call onChange with include_plugins when in partial mode', () => {
|
||||
// Arrange
|
||||
const onChange = vi.fn()
|
||||
@ -1390,20 +1224,6 @@ describe('auto-update-setting', () => {
|
||||
})
|
||||
|
||||
describe('Callback Memoization', () => {
|
||||
it('minuteFilter should filter to 15 minute intervals', () => {
|
||||
// Arrange
|
||||
const payload = createMockAutoUpdateConfig({
|
||||
strategy_setting: AUTO_UPDATE_STRATEGY.fixOnly,
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<AutoUpdateSetting {...defaultProps} payload={payload} />)
|
||||
|
||||
// The minuteFilter is passed to TimePicker internally
|
||||
// We verify the component renders correctly
|
||||
expect(screen.getByTestId('time-picker')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('handleChange should preserve other config values', () => {
|
||||
// Arrange
|
||||
const onChange = vi.fn()
|
||||
@ -1523,20 +1343,6 @@ describe('auto-update-setting', () => {
|
||||
expect(screen.getByText('plugin.autoUpdate.updateSettings')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle null timezone gracefully', () => {
|
||||
// This tests the timezone! non-null assertion in the component
|
||||
// The mock provides a valid timezone, so the component should work
|
||||
const payload = createMockAutoUpdateConfig({
|
||||
strategy_setting: AUTO_UPDATE_STRATEGY.fixOnly,
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<AutoUpdateSetting {...defaultProps} payload={payload} />)
|
||||
|
||||
// Assert - should render without errors
|
||||
expect(screen.getByTestId('time-picker')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render timezone offset correctly', () => {
|
||||
// Arrange
|
||||
const payload = createMockAutoUpdateConfig({
|
||||
@ -1630,7 +1436,7 @@ describe('auto-update-setting', () => {
|
||||
)
|
||||
|
||||
// Assert - initially disabled
|
||||
expect(screen.queryByTestId('time-picker')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('plugin.autoUpdate.updateTime')).not.toBeInTheDocument()
|
||||
|
||||
// Simulate enabling updates
|
||||
currentPayload = createMockAutoUpdateConfig({
|
||||
@ -1641,7 +1447,7 @@ describe('auto-update-setting', () => {
|
||||
rerender(<AutoUpdateSetting payload={currentPayload} onChange={onChange} />)
|
||||
|
||||
// Assert - time picker and plugins visible
|
||||
expect(screen.getByTestId('time-picker')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.autoUpdate.updateTime')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.autoUpdate.operation.select')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
|
||||
@ -28,10 +28,6 @@ vi.mock('@/service/use-plugins', () => ({
|
||||
useInstalledPluginList: () => mockInstalledPluginList,
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/button', () => ({
|
||||
Button: ({ children }: { children: React.ReactNode }) => <button>{children}</button>,
|
||||
}))
|
||||
|
||||
vi.mock('../no-plugin-selected', () => ({
|
||||
default: ({ updateMode }: { updateMode: AUTO_UPDATE_MODE }) => (
|
||||
<div data-testid="no-plugin-selected">{updateMode}</div>
|
||||
|
||||
@ -19,56 +19,6 @@ vi.mock('@/app/components/base/loading', () => ({
|
||||
default: () => <div data-testid="loading">loading</div>,
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/popover', async () => {
|
||||
const React = await import('react')
|
||||
const PopoverContext = React.createContext({
|
||||
open: false,
|
||||
setOpen: (_open: boolean) => {},
|
||||
})
|
||||
|
||||
const Popover = ({
|
||||
children,
|
||||
open,
|
||||
onOpenChange,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
}) => (
|
||||
<PopoverContext.Provider
|
||||
value={{ open: !!open, setOpen: (nextOpen: boolean) => onOpenChange?.(nextOpen) }}
|
||||
>
|
||||
{children}
|
||||
</PopoverContext.Provider>
|
||||
)
|
||||
|
||||
const PopoverTrigger = ({ render }: { render: React.ReactNode }) => {
|
||||
const { open, setOpen } = React.useContext(PopoverContext)
|
||||
return <div onClick={() => setOpen(!open)}>{render}</div>
|
||||
}
|
||||
|
||||
const PopoverContent = ({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}) => {
|
||||
const { open } = React.useContext(PopoverContext)
|
||||
return open ? (
|
||||
<div data-testid="popover-content" className={className}>
|
||||
{children}
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
|
||||
return {
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
PopoverContent,
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/app/components/plugins/marketplace/search-box', () => ({
|
||||
default: ({
|
||||
search,
|
||||
@ -151,7 +101,7 @@ describe('ToolPicker', () => {
|
||||
const onShowChange = vi.fn()
|
||||
render(
|
||||
<ToolPicker
|
||||
trigger={<span>trigger</span>}
|
||||
trigger={<button type="button">trigger</button>}
|
||||
value={[]}
|
||||
onChange={vi.fn()}
|
||||
isShow={false}
|
||||
@ -161,7 +111,7 @@ describe('ToolPicker', () => {
|
||||
|
||||
fireEvent.click(screen.getByText('trigger'))
|
||||
|
||||
expect(onShowChange).toHaveBeenCalledWith(true)
|
||||
expect(onShowChange).toHaveBeenCalledWith(true, expect.any(Object))
|
||||
})
|
||||
|
||||
it('renders loading content while installed plugins are loading', () => {
|
||||
|
||||
@ -28,13 +28,6 @@ vi.mock('@/context/workspace-state', async () => {
|
||||
}))
|
||||
})
|
||||
|
||||
vi.mock('@langgenius/dify-ui/dialog', () => ({
|
||||
Dialog: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
DialogContent: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
DialogTitle: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
DialogCloseButton: () => <button>close dialog</button>,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/badge/index', () => ({
|
||||
__esModule: true,
|
||||
BadgeState: {
|
||||
@ -43,22 +36,6 @@ vi.mock('@/app/components/base/badge/index', () => ({
|
||||
default: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/button', () => ({
|
||||
Button: ({
|
||||
children,
|
||||
onClick,
|
||||
disabled,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
onClick?: () => void
|
||||
disabled?: boolean
|
||||
}) => (
|
||||
<button disabled={disabled} onClick={onClick}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/toast', () => ({
|
||||
toast: {
|
||||
error: mockToastError,
|
||||
|
||||
@ -60,14 +60,6 @@ vi.mock('@langgenius/dify-ui/toast', () => ({
|
||||
toast: mockToast,
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/button', () => ({
|
||||
Button: ({ children, onClick, ...props }: Record<string, unknown>) => (
|
||||
<button onClick={onClick as () => void} {...props}>
|
||||
{children as string}
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../screenshot', () => ({
|
||||
default: () => <div data-testid="screenshot" />,
|
||||
}))
|
||||
|
||||
@ -16,27 +16,6 @@ vi.mock('@/app/components/workflow/store', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/dialog', () => ({
|
||||
Dialog: ({ children, open }: { children: React.ReactNode; open?: boolean }) =>
|
||||
open === false ? null : <>{children}</>,
|
||||
DialogContent: ({ children, className }: { children: React.ReactNode; className?: string }) => (
|
||||
<div data-testid="modal" className={className}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
DialogTitle: ({ children, className }: { children: React.ReactNode; className?: string }) => (
|
||||
<h2 className={className}>{children}</h2>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/button', () => ({
|
||||
Button: ({ children, onClick, disabled, ...props }: Record<string, unknown>) => (
|
||||
<button onClick={onClick as () => void} disabled={disabled as boolean} {...props}>
|
||||
{children as string}
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/input', () => ({
|
||||
default: ({ value, onChange, ...props }: Record<string, unknown>) => (
|
||||
<input
|
||||
@ -78,7 +57,7 @@ describe('PublishAsKnowledgePipelineModal', () => {
|
||||
it('should render modal with title', () => {
|
||||
render(<PublishAsKnowledgePipelineModal {...defaultProps} />)
|
||||
|
||||
expect(screen.getByTestId('modal')).toBeInTheDocument()
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument()
|
||||
expect(screen.getByText('pipeline.common.publishAs')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import type { PropsWithChildren } from 'react'
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { DSLImportStatus } from '@/models/app'
|
||||
@ -107,44 +106,6 @@ vi.mock('@/app/components/app/create-from-dsl-modal/uploader', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/button', () => ({
|
||||
Button: ({
|
||||
children,
|
||||
onClick,
|
||||
disabled,
|
||||
className,
|
||||
variant,
|
||||
loading,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
onClick?: () => void
|
||||
disabled?: boolean
|
||||
className?: string
|
||||
variant?: string
|
||||
loading?: boolean
|
||||
}) => (
|
||||
<button
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
className={className}
|
||||
data-variant={variant}
|
||||
data-loading={loading}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/dialog', () => ({
|
||||
Dialog: ({ children, open }: PropsWithChildren<{ open?: boolean }>) =>
|
||||
open === false ? null : <>{children}</>,
|
||||
DialogContent: ({ children, className }: PropsWithChildren<{ className?: string }>) => (
|
||||
<div data-testid="modal" className={className}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/constants', () => ({
|
||||
WORKFLOW_DATA_UPDATE: 'WORKFLOW_DATA_UPDATE',
|
||||
}))
|
||||
@ -310,34 +271,6 @@ describe('UpdateDSLModal', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle missing onImport callback', () => {
|
||||
const props = {
|
||||
onCancel: mockOnCancel,
|
||||
onBackup: mockOnBackup,
|
||||
}
|
||||
|
||||
render(<UpdateDSLModal {...props} />)
|
||||
|
||||
expect(screen.getByTestId('modal')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render import button with primary destructive variant', () => {
|
||||
render(<UpdateDSLModal {...defaultProps} />)
|
||||
|
||||
const importButton = screen.getByText('workflow.common.overwriteAndImport')
|
||||
expect(importButton).toHaveAttribute('data-variant', 'primary')
|
||||
})
|
||||
|
||||
it('should render backup button with secondary variant', () => {
|
||||
render(<UpdateDSLModal {...defaultProps} />)
|
||||
|
||||
const backupButtonText = screen.getByText('workflow.common.backupCurrentDraft')
|
||||
const backupButton = backupButtonText.closest('button')
|
||||
expect(backupButton).toHaveAttribute('data-variant', 'secondary')
|
||||
})
|
||||
})
|
||||
|
||||
describe('import flow', () => {
|
||||
it('should call importDSL when import button is clicked with file content', async () => {
|
||||
render(<UpdateDSLModal {...defaultProps} />)
|
||||
@ -718,13 +651,8 @@ describe('UpdateDSLModal', () => {
|
||||
{ timeout: 1000 },
|
||||
)
|
||||
|
||||
const cancelButtons = screen.getAllByText('app.newApp.Cancel')
|
||||
const errorModalCancelButton = cancelButtons.find(
|
||||
(btn) => btn.getAttribute('data-variant') === 'secondary',
|
||||
)
|
||||
if (errorModalCancelButton) {
|
||||
fireEvent.click(errorModalCancelButton)
|
||||
}
|
||||
const cancelButtons = screen.getAllByRole('button', { name: 'app.newApp.Cancel' })
|
||||
fireEvent.click(cancelButtons.at(-1)!)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('app.newApp.appCreateDSLErrorTitle')).not.toBeInTheDocument()
|
||||
|
||||
@ -36,54 +36,6 @@ const triggerHotkey = (hotkey: string) => {
|
||||
})
|
||||
}
|
||||
|
||||
vi.mock('@langgenius/dify-ui/popover', async () => await import('@/__mocks__/base-ui-popover'))
|
||||
vi.mock('@langgenius/dify-ui/button', () => ({
|
||||
Button: ({ children, onClick, disabled, variant, className }: Record<string, unknown>) => (
|
||||
<button
|
||||
onClick={onClick as (() => void) | undefined}
|
||||
disabled={disabled as boolean | undefined}
|
||||
data-variant={variant as string | undefined}
|
||||
className={className as string | undefined}
|
||||
>
|
||||
{children as React.ReactNode}
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
vi.mock('@langgenius/dify-ui/alert-dialog', () => ({
|
||||
AlertDialog: ({
|
||||
children,
|
||||
open,
|
||||
onOpenChange,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
}) =>
|
||||
open ? (
|
||||
<div role="alertdialog">
|
||||
{children}
|
||||
<button data-testid="alert-dialog-close" onClick={() => onOpenChange?.(false)}>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
) : null,
|
||||
AlertDialogActions: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
AlertDialogCancelButton: ({ children }: { children: React.ReactNode }) => (
|
||||
<button>{children}</button>
|
||||
),
|
||||
AlertDialogConfirmButton: ({ children, onClick, disabled }: Record<string, unknown>) => (
|
||||
<button
|
||||
onClick={onClick as (() => void) | undefined}
|
||||
disabled={disabled as boolean | undefined}
|
||||
>
|
||||
{children as React.ReactNode}
|
||||
</button>
|
||||
),
|
||||
AlertDialogContent: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
AlertDialogDescription: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
AlertDialogTitle: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
}))
|
||||
|
||||
const mockPush = vi.fn()
|
||||
vi.mock('@/next/navigation', () => ({
|
||||
useParams: () => ({ datasetId: 'test-dataset-id' }),
|
||||
@ -348,7 +300,7 @@ describe('publisher', () => {
|
||||
it('should render portal element in closed state by default', () => {
|
||||
renderWithQueryClient(<Publisher />)
|
||||
|
||||
expect(screen.getByTestId('popover')).toHaveAttribute('data-open', 'false')
|
||||
expect(screen.getByRole('button')).toHaveAttribute('aria-expanded', 'false')
|
||||
expect(screen.queryByText('workflow.common.publishUpdate')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
@ -900,7 +852,7 @@ describe('publisher', () => {
|
||||
expect(screen.getByText('pipeline.common.confirmPublish')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByTestId('alert-dialog-close'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.cancel' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()
|
||||
|
||||
@ -11,46 +11,6 @@ const render = (ui: React.ReactElement) => {
|
||||
return renderWithConsoleState(ui, { wrapper })
|
||||
}
|
||||
|
||||
vi.mock('@langgenius/dify-ui/alert-dialog', () => ({
|
||||
AlertDialog: ({
|
||||
children,
|
||||
open,
|
||||
onOpenChange,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
}) =>
|
||||
open ? (
|
||||
<div role="alertdialog">
|
||||
{children}
|
||||
<button data-testid="alert-dialog-close" onClick={() => onOpenChange?.(false)}>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
) : null,
|
||||
AlertDialogActions: ({ children }: { children?: React.ReactNode }) => <div>{children}</div>,
|
||||
AlertDialogCancelButton: ({ children }: { children?: React.ReactNode }) => (
|
||||
<button>{children}</button>
|
||||
),
|
||||
AlertDialogConfirmButton: ({
|
||||
children,
|
||||
onClick,
|
||||
disabled,
|
||||
}: {
|
||||
children?: React.ReactNode
|
||||
onClick?: () => void
|
||||
disabled?: boolean
|
||||
}) => (
|
||||
<button onClick={onClick} disabled={disabled}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
AlertDialogContent: ({ children }: { children?: React.ReactNode }) => <div>{children}</div>,
|
||||
AlertDialogDescription: ({ children }: { children?: React.ReactNode }) => <div>{children}</div>,
|
||||
AlertDialogTitle: ({ children }: { children?: React.ReactNode }) => <div>{children}</div>,
|
||||
}))
|
||||
|
||||
const mockPublishWorkflow = vi.fn().mockResolvedValue({ created_at: '2024-01-01T00:00:00Z' })
|
||||
const mockPublishAsCustomizedPipeline = vi.fn().mockResolvedValue({})
|
||||
const toastMocks = vi.hoisted(() => ({
|
||||
@ -137,19 +97,6 @@ vi.mock('@/app/components/workflow/store', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/button', () => ({
|
||||
Button: ({ children, onClick, disabled, variant, className }: Record<string, unknown>) => (
|
||||
<button
|
||||
onClick={onClick as () => void}
|
||||
disabled={disabled as boolean}
|
||||
data-variant={variant as string}
|
||||
className={className as string}
|
||||
>
|
||||
{children as React.ReactNode}
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/divider', () => ({
|
||||
default: () => <hr />,
|
||||
}))
|
||||
@ -459,7 +406,7 @@ describe('Popup', () => {
|
||||
|
||||
render(<Popup />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('alert-dialog-close'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.cancel' }))
|
||||
|
||||
expect(hideConfirm).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
@ -2,71 +2,6 @@ import { act, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import LabelSelector from '../selector'
|
||||
|
||||
vi.mock('@langgenius/dify-ui/popover', async () => {
|
||||
const React = await import('react')
|
||||
const PopoverContext = React.createContext({
|
||||
open: false,
|
||||
setOpen: (_open: boolean) => {},
|
||||
})
|
||||
|
||||
const Popover = ({
|
||||
children,
|
||||
open: controlledOpen,
|
||||
onOpenChange,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
}) => {
|
||||
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(false)
|
||||
const isControlled = controlledOpen !== undefined
|
||||
const open = isControlled ? !!controlledOpen : uncontrolledOpen
|
||||
const setOpen = (nextOpen: boolean) => {
|
||||
if (!isControlled) setUncontrolledOpen(nextOpen)
|
||||
onOpenChange?.(nextOpen)
|
||||
}
|
||||
|
||||
return <PopoverContext.Provider value={{ open, setOpen }}>{children}</PopoverContext.Provider>
|
||||
}
|
||||
|
||||
const PopoverTrigger = ({
|
||||
children,
|
||||
className,
|
||||
render,
|
||||
}: {
|
||||
children?: React.ReactNode
|
||||
className?: string
|
||||
render?: React.ReactNode
|
||||
}) => {
|
||||
const { open, setOpen } = React.useContext(PopoverContext)
|
||||
if (render) {
|
||||
return <div onClick={() => setOpen(!open)}>{render}</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<button type="button" className={className} onClick={() => setOpen(!open)}>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
const PopoverContent = ({
|
||||
children,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement> & { children?: React.ReactNode }) => {
|
||||
const { open } = React.useContext(PopoverContext)
|
||||
if (!open) return null
|
||||
|
||||
return <div {...props}>{children}</div>
|
||||
}
|
||||
|
||||
return {
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
PopoverContent,
|
||||
}
|
||||
})
|
||||
|
||||
// Mock useTags hook with controlled test data
|
||||
const mockTags = [
|
||||
{ name: 'agent', label: 'Agent' },
|
||||
|
||||
@ -2,118 +2,6 @@ import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import OperationDropdown from '../operation-dropdown'
|
||||
|
||||
vi.mock('@langgenius/dify-ui/dropdown-menu', async () => {
|
||||
const React = await import('react')
|
||||
const DropdownMenuContext = React.createContext<{
|
||||
isOpen: boolean
|
||||
setOpen: (open: boolean) => void
|
||||
} | null>(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,
|
||||
onOpenChange,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
}) => {
|
||||
const [internalOpen, setInternalOpen] = React.useState(open ?? false)
|
||||
const isOpen = open ?? internalOpen
|
||||
const setOpen = (nextOpen: boolean) => {
|
||||
if (open === undefined) setInternalOpen(nextOpen)
|
||||
onOpenChange?.(nextOpen)
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenuContext value={{ isOpen, setOpen }}>
|
||||
<div data-testid="dropdown-menu" data-open={isOpen}>
|
||||
{children}
|
||||
</div>
|
||||
</DropdownMenuContext>
|
||||
)
|
||||
},
|
||||
DropdownMenuTrigger: ({
|
||||
children,
|
||||
render,
|
||||
onClick,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
render?: React.ReactElement
|
||||
onClick?: React.MouseEventHandler<HTMLElement>
|
||||
}) => {
|
||||
const { isOpen, setOpen } = useDropdownMenuContext()
|
||||
const handleClick = (e: React.MouseEvent<HTMLElement>) => {
|
||||
onClick?.(e)
|
||||
setOpen(!isOpen)
|
||||
}
|
||||
|
||||
if (render)
|
||||
return React.cloneElement(
|
||||
render,
|
||||
{ 'data-testid': 'dropdown-trigger', onClick: handleClick } as Record<string, unknown>,
|
||||
children,
|
||||
)
|
||||
|
||||
return (
|
||||
<button data-testid="dropdown-trigger" onClick={handleClick}>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
},
|
||||
DropdownMenuContent: ({
|
||||
children,
|
||||
className,
|
||||
popupClassName,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
popupClassName?: string
|
||||
}) => {
|
||||
const { isOpen } = useDropdownMenuContext()
|
||||
return isOpen ? (
|
||||
<div
|
||||
data-testid="dropdown-content"
|
||||
className={[className, popupClassName].filter(Boolean).join(' ')}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
) : null
|
||||
},
|
||||
DropdownMenuItem: ({
|
||||
children,
|
||||
onClick,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
onClick?: React.MouseEventHandler<HTMLButtonElement>
|
||||
className?: string
|
||||
}) => {
|
||||
const { setOpen } = useDropdownMenuContext()
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="dropdown-item"
|
||||
className={className}
|
||||
onClick={(e) => {
|
||||
onClick?.(e)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
describe('OperationDropdown', () => {
|
||||
const defaultProps = {
|
||||
onEdit: vi.fn(),
|
||||
@ -145,7 +33,7 @@ describe('OperationDropdown', () => {
|
||||
it('should open dropdown when trigger is clicked', async () => {
|
||||
render(<OperationDropdown {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('dropdown-trigger'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.more' }))
|
||||
|
||||
expect(screen.getByText('tools.mcp.operation.edit')).toBeInTheDocument()
|
||||
expect(screen.getByText('tools.mcp.operation.remove')).toBeInTheDocument()
|
||||
@ -155,17 +43,17 @@ describe('OperationDropdown', () => {
|
||||
const onOpenChange = vi.fn()
|
||||
render(<OperationDropdown {...defaultProps} onOpenChange={onOpenChange} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('dropdown-trigger'))
|
||||
expect(onOpenChange).toHaveBeenCalledWith(true)
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.more' }))
|
||||
expect(onOpenChange).toHaveBeenCalledWith(true, expect.any(Object))
|
||||
})
|
||||
|
||||
it('should close dropdown when trigger is clicked again', async () => {
|
||||
const onOpenChange = vi.fn()
|
||||
render(<OperationDropdown {...defaultProps} onOpenChange={onOpenChange} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('dropdown-trigger'))
|
||||
fireEvent.click(screen.getByTestId('dropdown-trigger'))
|
||||
expect(onOpenChange).toHaveBeenLastCalledWith(false)
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.more' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.more' }))
|
||||
expect(onOpenChange).toHaveBeenLastCalledWith(false, expect.any(Object))
|
||||
})
|
||||
})
|
||||
|
||||
@ -174,7 +62,7 @@ describe('OperationDropdown', () => {
|
||||
const onEdit = vi.fn()
|
||||
render(<OperationDropdown {...defaultProps} onEdit={onEdit} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('dropdown-trigger'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.more' }))
|
||||
fireEvent.click(screen.getByText('tools.mcp.operation.edit'))
|
||||
expect(onEdit).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
@ -183,7 +71,7 @@ describe('OperationDropdown', () => {
|
||||
const onRemove = vi.fn()
|
||||
render(<OperationDropdown {...defaultProps} onRemove={onRemove} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('dropdown-trigger'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.more' }))
|
||||
fireEvent.click(screen.getByText('tools.mcp.operation.remove'))
|
||||
expect(onRemove).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
@ -192,20 +80,20 @@ describe('OperationDropdown', () => {
|
||||
const onOpenChange = vi.fn()
|
||||
render(<OperationDropdown {...defaultProps} onOpenChange={onOpenChange} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('dropdown-trigger'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.more' }))
|
||||
onOpenChange.mockClear()
|
||||
fireEvent.click(screen.getByText('tools.mcp.operation.edit'))
|
||||
expect(onOpenChange).toHaveBeenCalledWith(false)
|
||||
expect(onOpenChange).toHaveBeenCalledWith(false, expect.any(Object))
|
||||
})
|
||||
|
||||
it('should close dropdown after remove is clicked', () => {
|
||||
const onOpenChange = vi.fn()
|
||||
render(<OperationDropdown {...defaultProps} onOpenChange={onOpenChange} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('dropdown-trigger'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.more' }))
|
||||
onOpenChange.mockClear()
|
||||
fireEvent.click(screen.getByText('tools.mcp.operation.remove'))
|
||||
expect(onOpenChange).toHaveBeenCalledWith(false)
|
||||
expect(onOpenChange).toHaveBeenCalledWith(false, expect.any(Object))
|
||||
})
|
||||
})
|
||||
|
||||
@ -213,7 +101,7 @@ describe('OperationDropdown', () => {
|
||||
it('should have correct dropdown width', () => {
|
||||
render(<OperationDropdown {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('dropdown-trigger'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.more' }))
|
||||
const dropdown = document.querySelector('.w-\\[160px\\]')
|
||||
expect(dropdown).toBeInTheDocument()
|
||||
})
|
||||
@ -221,8 +109,8 @@ describe('OperationDropdown', () => {
|
||||
it('should render dropdown content through the shared popup shell', () => {
|
||||
render(<OperationDropdown {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('dropdown-trigger'))
|
||||
expect(screen.getByTestId('dropdown-content')).toBeInTheDocument()
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.more' }))
|
||||
expect(screen.getByRole('menu')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -7,8 +7,6 @@ import WorkflowToolConfigureButton from '../configure-button'
|
||||
import { WorkflowToolDrawer } from '../index'
|
||||
import MethodSelector from '../method-selector'
|
||||
|
||||
vi.mock('@langgenius/dify-ui/popover', () => import('@/__mocks__/base-ui-popover'))
|
||||
|
||||
// Mock Next.js navigation
|
||||
const mockPush = vi.fn()
|
||||
vi.mock('@/next/navigation', () => ({
|
||||
@ -1316,11 +1314,11 @@ describe('MethodSelector', () => {
|
||||
|
||||
// Act
|
||||
render(<MethodSelector {...props} />)
|
||||
await user.click(screen.getByTestId('popover-trigger'))
|
||||
await user.click(screen.getByRole('button'))
|
||||
|
||||
// Assert
|
||||
// Assert
|
||||
expect(screen.getByTestId('popover-content'))!.toBeInTheDocument()
|
||||
expect(screen.getByText('tools.createTool.toolInput.methodParameterTip')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call onChange with llm when parameter option clicked', async () => {
|
||||
@ -1334,7 +1332,7 @@ describe('MethodSelector', () => {
|
||||
|
||||
// Act
|
||||
render(<MethodSelector {...props} />)
|
||||
await user.click(screen.getByTestId('popover-trigger'))
|
||||
await user.click(screen.getByRole('button'))
|
||||
|
||||
const paramOption = screen.getAllByText('tools.createTool.toolInput.methodParameter')[0]
|
||||
await user.click(paramOption!)
|
||||
@ -1354,7 +1352,7 @@ describe('MethodSelector', () => {
|
||||
|
||||
// Act
|
||||
render(<MethodSelector {...props} />)
|
||||
await user.click(screen.getByTestId('popover-trigger'))
|
||||
await user.click(screen.getByRole('button'))
|
||||
|
||||
const settingOption = screen.getByText('tools.createTool.toolInput.methodSetting')
|
||||
await user.click(settingOption)
|
||||
@ -1373,51 +1371,17 @@ describe('MethodSelector', () => {
|
||||
|
||||
// Act
|
||||
render(<MethodSelector {...props} />)
|
||||
const trigger = screen.getByRole('button')
|
||||
|
||||
// First click - open
|
||||
await user.click(screen.getByTestId('popover-trigger'))
|
||||
expect(screen.getByTestId('popover-content'))!.toBeInTheDocument()
|
||||
await user.click(trigger)
|
||||
expect(screen.getByText('tools.createTool.toolInput.methodParameterTip')).toBeInTheDocument()
|
||||
|
||||
// Second click - close
|
||||
await user.click(screen.getByTestId('popover-trigger'))
|
||||
expect(screen.queryByTestId('popover-content')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Props Tests (REQUIRED)
|
||||
describe('Props', () => {
|
||||
it('should show check icon for selected llm value', async () => {
|
||||
// Arrange
|
||||
const user = userEvent.setup()
|
||||
const props = {
|
||||
value: 'llm',
|
||||
onChange: vi.fn(),
|
||||
}
|
||||
|
||||
// Act
|
||||
render(<MethodSelector {...props} />)
|
||||
await user.click(screen.getByTestId('popover-trigger'))
|
||||
|
||||
// Assert - the first option (llm) should have a check icon container
|
||||
const content = screen.getByTestId('popover-content')
|
||||
expect(content)!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show check icon for selected form value', async () => {
|
||||
// Arrange
|
||||
const user = userEvent.setup()
|
||||
const props = {
|
||||
value: 'form',
|
||||
onChange: vi.fn(),
|
||||
}
|
||||
|
||||
// Act
|
||||
render(<MethodSelector {...props} />)
|
||||
await user.click(screen.getByTestId('popover-trigger'))
|
||||
|
||||
// Assert
|
||||
const content = screen.getByTestId('popover-content')
|
||||
expect(content)!.toBeInTheDocument()
|
||||
await user.click(trigger)
|
||||
expect(
|
||||
screen.queryByText('tools.createTool.toolInput.methodParameterTip'),
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -1,33 +1,8 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import WorkflowOnboardingModal from '../index'
|
||||
|
||||
vi.mock('@/app/components/workflow/block-selector', () => ({
|
||||
default: ({
|
||||
open,
|
||||
onSelect,
|
||||
trigger,
|
||||
}: {
|
||||
open?: boolean
|
||||
onSelect: (type: BlockEnum, config?: Record<string, unknown>) => void
|
||||
trigger?: ((open: boolean) => ReactNode) | ReactNode
|
||||
}) => (
|
||||
<div>
|
||||
{typeof trigger === 'function' ? trigger(Boolean(open)) : trigger}
|
||||
{open && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onSelect(BlockEnum.TriggerWebhook, { config: 'test' })}
|
||||
>
|
||||
Select Trigger Webhook
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
describe('WorkflowOnboardingModal', () => {
|
||||
it('only renders while onboarding is open', () => {
|
||||
const props = { onClose: vi.fn(), onSelectStartNode: vi.fn() }
|
||||
@ -50,20 +25,6 @@ describe('WorkflowOnboardingModal', () => {
|
||||
|
||||
expect(onSelectStartNode).toHaveBeenCalledWith(BlockEnum.Start)
|
||||
})
|
||||
|
||||
it('forwards trigger configuration', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onSelectStartNode = vi.fn()
|
||||
render(
|
||||
<WorkflowOnboardingModal isShow onClose={vi.fn()} onSelectStartNode={onSelectStartNode} />,
|
||||
)
|
||||
|
||||
await user.click(screen.getByText('workflow.onboarding.trigger'))
|
||||
await user.click(screen.getByRole('button', { name: 'Select Trigger Webhook' }))
|
||||
|
||||
expect(onSelectStartNode).toHaveBeenCalledWith(BlockEnum.TriggerWebhook, { config: 'test' })
|
||||
})
|
||||
|
||||
it('closes from the dialog control', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onClose = vi.fn()
|
||||
|
||||
@ -1,37 +1,7 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import StartNodeSelectionPanel from '../start-node-selection-panel'
|
||||
|
||||
vi.mock('@/app/components/workflow/block-selector', () => ({
|
||||
default: ({
|
||||
open,
|
||||
onOpenChange,
|
||||
onSelect,
|
||||
trigger,
|
||||
}: {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
onSelect: (type: BlockEnum) => void
|
||||
trigger: (() => ReactNode) | ReactNode
|
||||
}) => (
|
||||
<div>
|
||||
{typeof trigger === 'function' ? trigger() : trigger}
|
||||
{open && (
|
||||
<div>
|
||||
<button type="button" onClick={() => onSelect(BlockEnum.TriggerSchedule)}>
|
||||
Select Schedule
|
||||
</button>
|
||||
<button type="button" onClick={() => onOpenChange(false)}>
|
||||
Close selector
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
describe('StartNodeSelectionPanel', () => {
|
||||
it('selects a user input start node', async () => {
|
||||
const user = userEvent.setup()
|
||||
@ -44,32 +14,4 @@ describe('StartNodeSelectionPanel', () => {
|
||||
|
||||
expect(onSelectUserInput).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('selects a trigger start node and closes the selector', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onSelectTrigger = vi.fn()
|
||||
render(
|
||||
<StartNodeSelectionPanel onSelectUserInput={vi.fn()} onSelectTrigger={onSelectTrigger} />,
|
||||
)
|
||||
|
||||
await user.click(screen.getByText('workflow.onboarding.trigger'))
|
||||
await user.click(screen.getByRole('button', { name: 'Select Schedule' }))
|
||||
|
||||
expect(onSelectTrigger).toHaveBeenCalledWith(BlockEnum.TriggerSchedule, undefined)
|
||||
expect(screen.queryByRole('button', { name: 'Select Schedule' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('closes the trigger selector without selecting a node', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onSelectTrigger = vi.fn()
|
||||
render(
|
||||
<StartNodeSelectionPanel onSelectUserInput={vi.fn()} onSelectTrigger={onSelectTrigger} />,
|
||||
)
|
||||
|
||||
await user.click(screen.getByText('workflow.onboarding.trigger'))
|
||||
await user.click(screen.getByRole('button', { name: 'Close selector' }))
|
||||
|
||||
expect(onSelectTrigger).not.toHaveBeenCalled()
|
||||
expect(screen.queryByRole('button', { name: 'Select Schedule' })).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { Position } from 'reactflow'
|
||||
import { ErrorHandleTypeEnum } from '@/app/components/workflow/nodes/_base/components/error-handle/types'
|
||||
import CustomEdge from '../custom-edge'
|
||||
@ -7,7 +7,6 @@ import { BlockEnum, NodeRunningStatus } from '../types'
|
||||
|
||||
const mockUseAvailableBlocks = vi.hoisted(() => vi.fn())
|
||||
const mockUseNodesInteractions = vi.hoisted(() => vi.fn())
|
||||
const mockBlockSelector = vi.hoisted(() => vi.fn())
|
||||
const mockGradientRender = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('reactflow', () => ({
|
||||
@ -59,30 +58,6 @@ vi.mock('../hooks/use-nodes-interactions', async (importOriginal) => {
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/app/components/workflow/block-selector', () => ({
|
||||
__esModule: true,
|
||||
default: (props: {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
onSelect: (nodeType: string, pluginDefaultValue?: Record<string, unknown>) => void
|
||||
availableBlocksTypes: string[]
|
||||
}) => {
|
||||
mockBlockSelector(props)
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="block-selector"
|
||||
onClick={() => {
|
||||
props.onOpenChange(true)
|
||||
props.onSelect('llm', { provider: 'openai' })
|
||||
}}
|
||||
>
|
||||
{props.availableBlocksTypes.join(',')}
|
||||
</button>
|
||||
)
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/custom-edge-linear-gradient-render', () => ({
|
||||
__esModule: true,
|
||||
default: (props: { id: string; startColor: string; stopColor: string }) => {
|
||||
@ -106,7 +81,7 @@ describe('CustomEdge', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should render a gradient edge and insert a node between the source and target', () => {
|
||||
it('should render a gradient edge and its real insert-node trigger', () => {
|
||||
render(
|
||||
<CustomEdge
|
||||
id="edge-1"
|
||||
@ -149,27 +124,12 @@ describe('CustomEdge', () => {
|
||||
expect(screen.getByTestId('base-edge')).toHaveAttribute('data-stroke', 'url(#edge-1)')
|
||||
expect(screen.getByTestId('base-edge')).toHaveAttribute('data-opacity', '0.3')
|
||||
expect(screen.getByTestId('base-edge')).toHaveAttribute('data-dasharray', '8 8')
|
||||
expect(screen.getByTestId('block-selector')).toHaveTextContent('llm')
|
||||
expect(screen.getByTestId('block-selector').parentElement).toHaveStyle({
|
||||
const addBlockTrigger = screen.getByRole('button', { name: 'workflow.common.addBlock' })
|
||||
expect(addBlockTrigger.parentElement).toHaveStyle({
|
||||
transform: 'translate(-50%, -50%) translate(24px, 48px)',
|
||||
opacity: '0.7',
|
||||
zIndex: '1001',
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByTestId('block-selector'))
|
||||
|
||||
expect(mockHandleNodeAdd).toHaveBeenCalledWith(
|
||||
{
|
||||
nodeType: 'llm',
|
||||
pluginDefaultValue: { provider: 'openai' },
|
||||
},
|
||||
{
|
||||
prevNodeId: 'source-node',
|
||||
prevNodeSourceHandle: 'source',
|
||||
nextNodeId: 'target-node',
|
||||
nextNodeTargetHandle: 'target',
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
it('should prefer the running stroke color when the edge is selected', () => {
|
||||
@ -258,7 +218,9 @@ describe('CustomEdge', () => {
|
||||
'data-stroke',
|
||||
'var(--color-workflow-link-line-normal)',
|
||||
)
|
||||
expect(screen.getByTestId('block-selector').parentElement).toHaveStyle({
|
||||
expect(
|
||||
screen.getByRole('button', { name: 'workflow.common.addBlock' }).parentElement,
|
||||
).toHaveStyle({
|
||||
opacity: '0',
|
||||
pointerEvents: 'none',
|
||||
})
|
||||
|
||||
@ -40,11 +40,6 @@ vi.mock('@/context/account-state', async () => {
|
||||
return createAccountStateModuleMock(() => mockConsoleState)
|
||||
})
|
||||
|
||||
vi.mock('@langgenius/dify-ui/avatar', () => ({
|
||||
Avatar: ({ name }: { name: string }) => <div data-testid="avatar">{name}</div>,
|
||||
default: ({ name }: { name: string }) => <div data-testid="avatar">{name}</div>,
|
||||
}))
|
||||
|
||||
vi.mock('./mention-input', () => ({
|
||||
MentionInput: ((props: MentionInputProps) => {
|
||||
mentionInputProps = props
|
||||
|
||||
@ -46,10 +46,6 @@ vi.mock('../store', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/avatar', () => ({
|
||||
Avatar: ({ name }: { name: string }) => <div data-testid="mention-avatar">{name}</div>,
|
||||
}))
|
||||
|
||||
const mentionUsers: UserProfile[] = [
|
||||
{
|
||||
id: 'user-2',
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { WorkflowCommentDetail } from '@/app/components/workflow/comment/types'
|
||||
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { render } from '@/test/console/render'
|
||||
import { CommentThread } from './thread'
|
||||
|
||||
@ -66,45 +67,6 @@ vi.mock('@/app/components/base/inline-delete-confirm', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/avatar', () => ({
|
||||
Avatar: ({ name }: { name: string }) => <div data-testid="avatar">{name}</div>,
|
||||
AvatarRoot: ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="avatar-root">{children}</div>
|
||||
),
|
||||
AvatarImage: ({ alt }: { alt: string }) => <div data-testid="avatar-image">{alt}</div>,
|
||||
AvatarFallback: ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="avatar-fallback">{children}</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/dropdown-menu', () => ({
|
||||
DropdownMenu: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
DropdownMenuTrigger: ({ children, ...props }: React.ComponentProps<'button'>) => (
|
||||
<button type="button" {...props}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
DropdownMenuContent: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/tooltip', () => ({
|
||||
Tooltip: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
||||
TooltipTrigger: ({
|
||||
children,
|
||||
render,
|
||||
...props
|
||||
}: React.ComponentProps<'button'> & { children?: React.ReactNode; render?: React.ReactNode }) => {
|
||||
if (render) return <>{render}</>
|
||||
|
||||
return (
|
||||
<button type="button" {...props}>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
},
|
||||
TooltipContent: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
||||
}))
|
||||
|
||||
vi.mock('./mention-input', () => ({
|
||||
MentionInput: ({
|
||||
placeholder,
|
||||
@ -264,6 +226,7 @@ describe('CommentThread', () => {
|
||||
})
|
||||
|
||||
it('supports editing and direct deleting an existing reply', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onReplyEdit = vi.fn()
|
||||
const onReplyDeleteDirect = vi.fn()
|
||||
|
||||
@ -276,18 +239,20 @@ describe('CommentThread', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByText('workflow.comments.actions.editReply'))
|
||||
fireEvent.click(screen.getByText('submit-workflow.comments.placeholder.editReply'))
|
||||
await user.click(screen.getByLabelText('workflow.comments.aria.replyActions'))
|
||||
await user.click(await screen.findByText('workflow.comments.actions.editReply'))
|
||||
await user.click(screen.getByText('submit-workflow.comments.placeholder.editReply'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onReplyEdit).toHaveBeenCalledWith('reply-1', 'first reply', ['user-2'])
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('workflow.comments.actions.deleteReply')).toBeInTheDocument()
|
||||
expect(screen.getByLabelText('workflow.comments.aria.replyActions')).toBeInTheDocument()
|
||||
})
|
||||
fireEvent.click(screen.getByText('workflow.comments.actions.deleteReply'))
|
||||
fireEvent.click(screen.getByTestId('confirm-delete-reply'))
|
||||
await user.click(screen.getByLabelText('workflow.comments.aria.replyActions'))
|
||||
await user.click(await screen.findByText('workflow.comments.actions.deleteReply'))
|
||||
await user.click(screen.getByTestId('confirm-delete-reply'))
|
||||
|
||||
expect(onReplyDeleteDirect).toHaveBeenCalledWith('reply-1')
|
||||
})
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type * as React from 'react'
|
||||
import type { TriggerOption } from '../test-run-menu'
|
||||
import { DropdownMenu, DropdownMenuContent } from '@langgenius/dify-ui/dropdown-menu'
|
||||
import { fireEvent, render, renderHook, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { TriggerType } from '../test-run-menu'
|
||||
@ -10,53 +10,6 @@ import {
|
||||
useShortcutMenu,
|
||||
} from '../test-run-menu-helpers'
|
||||
|
||||
vi.mock('@langgenius/dify-ui/dropdown-menu', async () => {
|
||||
const React = await import('react')
|
||||
const DropdownMenuContext = React.createContext<{
|
||||
open: boolean
|
||||
setOpen: (open: boolean) => void
|
||||
} | null>(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,
|
||||
onOpenChange,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
open: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
}) => (
|
||||
<DropdownMenuContext value={{ open, setOpen: onOpenChange ?? vi.fn() }}>
|
||||
<div>{children}</div>
|
||||
</DropdownMenuContext>
|
||||
),
|
||||
DropdownMenuContent: ({ children }: { children: React.ReactNode }) => {
|
||||
const { open } = useDropdownMenuContext()
|
||||
return open ? <div>{children}</div> : null
|
||||
},
|
||||
DropdownMenuItem: ({
|
||||
children,
|
||||
onClick,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
onClick?: React.MouseEventHandler<HTMLButtonElement>
|
||||
className?: string
|
||||
}) => (
|
||||
<button type="button" className={className} onClick={onClick}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}
|
||||
})
|
||||
|
||||
const createOption = (overrides: Partial<TriggerOption> = {}): TriggerOption => ({
|
||||
id: 'user-input',
|
||||
type: TriggerType.UserInput,
|
||||
@ -79,7 +32,13 @@ describe('test-run-menu helpers', () => {
|
||||
expect(getNormalizedShortcutKey(new KeyboardEvent('keydown', { key: '`' }))).toBe('~')
|
||||
expect(getNormalizedShortcutKey(new KeyboardEvent('keydown', { key: '1' }))).toBe('1')
|
||||
|
||||
render(<OptionRow option={option} shortcutKey="1" onSelect={onSelect} />)
|
||||
render(
|
||||
<DropdownMenu open>
|
||||
<DropdownMenuContent>
|
||||
<OptionRow option={option} shortcutKey="1" onSelect={onSelect} />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('1')).toBeInTheDocument()
|
||||
|
||||
|
||||
@ -5,104 +5,6 @@ import { act } from 'react'
|
||||
import * as React from 'react'
|
||||
import TestRunMenu, { TriggerType } from '../test-run-menu'
|
||||
|
||||
vi.mock('@langgenius/dify-ui/dropdown-menu', async () => {
|
||||
const React = await import('react')
|
||||
const DropdownMenuContext = React.createContext<{
|
||||
open: boolean
|
||||
setOpen: (open: boolean) => void
|
||||
} | null>(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,
|
||||
onOpenChange,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
open: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
}) => (
|
||||
<DropdownMenuContext value={{ open, setOpen: onOpenChange ?? vi.fn() }}>
|
||||
<div>{children}</div>
|
||||
</DropdownMenuContext>
|
||||
),
|
||||
DropdownMenuTrigger: ({
|
||||
children,
|
||||
render,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
render?: React.ReactElement<{ children?: React.ReactNode }>
|
||||
}) => {
|
||||
const { open, setOpen } = useDropdownMenuContext()
|
||||
|
||||
if (render) {
|
||||
return React.cloneElement(
|
||||
render,
|
||||
{ onClick: () => setOpen(!open) } as Record<string, unknown>,
|
||||
children ?? render.props.children,
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<button type="button" onClick={() => setOpen(!open)}>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
},
|
||||
DropdownMenuContent: ({ children }: { children: React.ReactNode }) => {
|
||||
const { open } = useDropdownMenuContext()
|
||||
return open ? <div>{children}</div> : null
|
||||
},
|
||||
DropdownMenuGroup: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
DropdownMenuLabel: ({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}) => <div className={className}>{children}</div>,
|
||||
DropdownMenuGroupLabel: ({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}) => <div className={className}>{children}</div>,
|
||||
DropdownMenuSeparator: ({ className }: { className?: string }) => (
|
||||
<div className={className} data-testid="dropdown-separator" />
|
||||
),
|
||||
DropdownMenuItem: ({
|
||||
children,
|
||||
onClick,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
onClick?: React.MouseEventHandler<HTMLButtonElement>
|
||||
className?: string
|
||||
}) => {
|
||||
const { setOpen } = useDropdownMenuContext()
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={className}
|
||||
onClick={(event) => {
|
||||
onClick?.(event)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const createOption = (overrides: Partial<TriggerOption> = {}): TriggerOption => ({
|
||||
id: 'user-input',
|
||||
type: TriggerType.UserInput,
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { VersionHistoryButton } from '../version-history-button'
|
||||
|
||||
let mockTheme: 'light' | 'dark' = 'light'
|
||||
@ -29,12 +30,6 @@ vi.mock('@tanstack/react-hotkeys', async (importOriginal) => {
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@langgenius/dify-ui/tooltip', () => ({
|
||||
Tooltip: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
||||
TooltipTrigger: ({ render }: { render: React.ReactNode }) => <>{render}</>,
|
||||
TooltipContent: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
}))
|
||||
|
||||
describe('VersionHistoryButton', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@ -66,9 +61,10 @@ describe('VersionHistoryButton', () => {
|
||||
})
|
||||
|
||||
it('should render the tooltip popup content on hover', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<VersionHistoryButton onClick={vi.fn()} />)
|
||||
|
||||
fireEvent.mouseEnter(screen.getByRole('button'))
|
||||
await user.hover(screen.getByRole('button'))
|
||||
|
||||
expect(await screen.findByText('workflow.common.versionHistory')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user