mirror of
https://github.com/langgenius/dify.git
synced 2026-04-28 11:56:55 +08:00
add unittests
This commit is contained in:
parent
c17f564718
commit
aaa3d2d74f
148
web/app/components/workflow/comment/comment-icon.spec.tsx
Normal file
148
web/app/components/workflow/comment/comment-icon.spec.tsx
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
import type { WorkflowCommentList } from '@/service/workflow-comment'
|
||||||
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { CommentIcon } from './comment-icon'
|
||||||
|
|
||||||
|
type Position = { x: number, y: number }
|
||||||
|
|
||||||
|
let mockUserId = 'user-1'
|
||||||
|
|
||||||
|
const mockFlowToScreenPosition = vi.fn((position: Position) => position)
|
||||||
|
const mockScreenToFlowPosition = vi.fn((position: Position) => position)
|
||||||
|
|
||||||
|
vi.mock('reactflow', () => ({
|
||||||
|
useReactFlow: () => ({
|
||||||
|
flowToScreenPosition: mockFlowToScreenPosition,
|
||||||
|
screenToFlowPosition: mockScreenToFlowPosition,
|
||||||
|
}),
|
||||||
|
useViewport: () => ({
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
zoom: 1,
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/context/app-context', () => ({
|
||||||
|
useAppContext: () => ({
|
||||||
|
userProfile: {
|
||||||
|
id: mockUserId,
|
||||||
|
name: 'User',
|
||||||
|
avatar_url: 'avatar',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/app/components/base/user-avatar-list', () => ({
|
||||||
|
UserAvatarList: ({ users }: { users: Array<{ id: string }> }) => (
|
||||||
|
<div data-testid="avatar-list">{users.map(user => user.id).join(',')}</div>
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('./comment-preview', () => ({
|
||||||
|
default: ({ onClick }: { onClick?: () => void }) => (
|
||||||
|
<button type="button" data-testid="comment-preview" onClick={onClick}>
|
||||||
|
Preview
|
||||||
|
</button>
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const createComment = (overrides: Partial<WorkflowCommentList> = {}): WorkflowCommentList => ({
|
||||||
|
id: 'comment-1',
|
||||||
|
position_x: 0,
|
||||||
|
position_y: 0,
|
||||||
|
content: 'Hello',
|
||||||
|
created_by: 'user-1',
|
||||||
|
created_by_account: {
|
||||||
|
id: 'user-1',
|
||||||
|
name: 'Alice',
|
||||||
|
email: 'alice@example.com',
|
||||||
|
},
|
||||||
|
created_at: 1,
|
||||||
|
updated_at: 2,
|
||||||
|
resolved: false,
|
||||||
|
mention_count: 0,
|
||||||
|
reply_count: 0,
|
||||||
|
participants: [],
|
||||||
|
...overrides,
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('CommentIcon', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
mockUserId = 'user-1'
|
||||||
|
})
|
||||||
|
|
||||||
|
it('toggles preview on hover when inactive', () => {
|
||||||
|
const comment = createComment()
|
||||||
|
const { container } = render(
|
||||||
|
<CommentIcon comment={comment} onClick={vi.fn()} isActive={false} />,
|
||||||
|
)
|
||||||
|
const marker = container.querySelector('[data-role="comment-marker"]') as HTMLElement
|
||||||
|
const hoverTarget = marker.firstElementChild as HTMLElement
|
||||||
|
|
||||||
|
fireEvent.mouseEnter(hoverTarget)
|
||||||
|
expect(screen.getByTestId('comment-preview')).toBeInTheDocument()
|
||||||
|
|
||||||
|
fireEvent.mouseLeave(hoverTarget)
|
||||||
|
expect(screen.queryByTestId('comment-preview')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls onPositionUpdate after dragging by author', () => {
|
||||||
|
const comment = createComment({ position_x: 0, position_y: 0 })
|
||||||
|
const onClick = vi.fn()
|
||||||
|
const onPositionUpdate = vi.fn()
|
||||||
|
const { container } = render(
|
||||||
|
<CommentIcon
|
||||||
|
comment={comment}
|
||||||
|
onClick={onClick}
|
||||||
|
onPositionUpdate={onPositionUpdate}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
const marker = container.querySelector('[data-role="comment-marker"]') as HTMLElement
|
||||||
|
|
||||||
|
fireEvent.pointerDown(marker, {
|
||||||
|
pointerId: 1,
|
||||||
|
button: 0,
|
||||||
|
clientX: 100,
|
||||||
|
clientY: 100,
|
||||||
|
})
|
||||||
|
fireEvent.pointerMove(marker, {
|
||||||
|
pointerId: 1,
|
||||||
|
clientX: 110,
|
||||||
|
clientY: 110,
|
||||||
|
})
|
||||||
|
fireEvent.pointerUp(marker, {
|
||||||
|
pointerId: 1,
|
||||||
|
clientX: 110,
|
||||||
|
clientY: 110,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(mockScreenToFlowPosition).toHaveBeenCalledWith({ x: 10, y: 10 })
|
||||||
|
expect(onPositionUpdate).toHaveBeenCalledWith({ x: 10, y: 10 })
|
||||||
|
expect(onClick).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls onClick for non-author clicks', () => {
|
||||||
|
mockUserId = 'user-2'
|
||||||
|
const comment = createComment()
|
||||||
|
const onClick = vi.fn()
|
||||||
|
const { container } = render(
|
||||||
|
<CommentIcon comment={comment} onClick={onClick} isActive={false} />,
|
||||||
|
)
|
||||||
|
const marker = container.querySelector('[data-role="comment-marker"]') as HTMLElement
|
||||||
|
|
||||||
|
fireEvent.pointerDown(marker, {
|
||||||
|
pointerId: 1,
|
||||||
|
button: 0,
|
||||||
|
clientX: 50,
|
||||||
|
clientY: 60,
|
||||||
|
})
|
||||||
|
fireEvent.pointerUp(marker, {
|
||||||
|
pointerId: 1,
|
||||||
|
clientX: 50,
|
||||||
|
clientY: 60,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(onClick).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
106
web/app/components/workflow/comment/comment-input.spec.tsx
Normal file
106
web/app/components/workflow/comment/comment-input.spec.tsx
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import type { FC } from 'react'
|
||||||
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { CommentInput } from './comment-input'
|
||||||
|
|
||||||
|
type MentionInputProps = {
|
||||||
|
value: string
|
||||||
|
onChange: (value: string) => void
|
||||||
|
onSubmit: (content: string, mentionedUserIds: string[]) => void
|
||||||
|
placeholder?: string
|
||||||
|
autoFocus?: boolean
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const stableT = (key: string, options?: { ns?: string }) => (
|
||||||
|
options?.ns ? `${options.ns}.${key}` : key
|
||||||
|
)
|
||||||
|
|
||||||
|
let mentionInputProps: MentionInputProps | null = null
|
||||||
|
|
||||||
|
vi.mock('react-i18next', () => ({
|
||||||
|
useTranslation: () => ({
|
||||||
|
t: stableT,
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/context/app-context', () => ({
|
||||||
|
useAppContext: () => ({
|
||||||
|
userProfile: {
|
||||||
|
id: 'user-1',
|
||||||
|
name: 'Alice',
|
||||||
|
avatar_url: 'avatar',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/app/components/base/avatar', () => ({
|
||||||
|
default: ({ name }: { name: string }) => <div data-testid="avatar">{name}</div>,
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('./mention-input', () => ({
|
||||||
|
MentionInput: ((props: MentionInputProps) => {
|
||||||
|
mentionInputProps = props
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-testid="mention-input"
|
||||||
|
onClick={() => props.onSubmit('Hello', ['user-2'])}
|
||||||
|
>
|
||||||
|
MentionInput
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}) as FC<MentionInputProps>,
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('CommentInput', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
mentionInputProps = null
|
||||||
|
})
|
||||||
|
|
||||||
|
it('passes translated placeholder to mention input', () => {
|
||||||
|
render(
|
||||||
|
<CommentInput
|
||||||
|
position={{ x: 0, y: 0 }}
|
||||||
|
onSubmit={vi.fn()}
|
||||||
|
onCancel={vi.fn()}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(mentionInputProps?.placeholder).toBe('workflow.comments.placeholder.add')
|
||||||
|
expect(mentionInputProps?.autoFocus).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls onCancel when Escape is pressed', () => {
|
||||||
|
const onCancel = vi.fn()
|
||||||
|
|
||||||
|
render(
|
||||||
|
<CommentInput
|
||||||
|
position={{ x: 0, y: 0 }}
|
||||||
|
onSubmit={vi.fn()}
|
||||||
|
onCancel={onCancel}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
fireEvent.keyDown(document, { key: 'Escape' })
|
||||||
|
|
||||||
|
expect(onCancel).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('forwards mention submit to onSubmit', () => {
|
||||||
|
const onSubmit = vi.fn()
|
||||||
|
|
||||||
|
render(
|
||||||
|
<CommentInput
|
||||||
|
position={{ x: 0, y: 0 }}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
onCancel={vi.fn()}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByTestId('mention-input'))
|
||||||
|
|
||||||
|
expect(onSubmit).toHaveBeenCalledWith('Hello', ['user-2'])
|
||||||
|
})
|
||||||
|
})
|
||||||
86
web/app/components/workflow/comment/comment-preview.spec.tsx
Normal file
86
web/app/components/workflow/comment/comment-preview.spec.tsx
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import type { WorkflowCommentList } from '@/service/workflow-comment'
|
||||||
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import CommentPreview from './comment-preview'
|
||||||
|
|
||||||
|
type UserProfile = WorkflowCommentList['created_by_account']
|
||||||
|
|
||||||
|
const mockSetHovering = vi.fn()
|
||||||
|
let capturedUsers: UserProfile[] = []
|
||||||
|
|
||||||
|
vi.mock('@/app/components/base/user-avatar-list', () => ({
|
||||||
|
UserAvatarList: ({ users }: { users: UserProfile[] }) => {
|
||||||
|
capturedUsers = users
|
||||||
|
return <div data-testid="avatar-list">{users.map(user => user.id).join(',')}</div>
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/hooks/use-format-time-from-now', () => ({
|
||||||
|
useFormatTimeFromNow: () => ({
|
||||||
|
formatTimeFromNow: (value: number) => `time:${value}`,
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('../store', () => ({
|
||||||
|
useStore: (selector: (state: { setCommentPreviewHovering: (value: boolean) => void }) => unknown) =>
|
||||||
|
selector({ setCommentPreviewHovering: mockSetHovering }),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const createComment = (overrides: Partial<WorkflowCommentList> = {}): WorkflowCommentList => {
|
||||||
|
const author = { id: 'user-1', name: 'Alice', email: 'alice@example.com' }
|
||||||
|
const participant = { id: 'user-2', name: 'Bob', email: 'bob@example.com' }
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: 'comment-1',
|
||||||
|
position_x: 0,
|
||||||
|
position_y: 0,
|
||||||
|
content: 'Hello',
|
||||||
|
created_by: author.id,
|
||||||
|
created_by_account: author,
|
||||||
|
created_at: 1,
|
||||||
|
updated_at: 10,
|
||||||
|
resolved: false,
|
||||||
|
mention_count: 0,
|
||||||
|
reply_count: 0,
|
||||||
|
participants: [author, participant],
|
||||||
|
...overrides,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('CommentPreview', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
capturedUsers = []
|
||||||
|
})
|
||||||
|
|
||||||
|
it('orders participants with author first and formats time', () => {
|
||||||
|
const comment = createComment()
|
||||||
|
|
||||||
|
render(<CommentPreview comment={comment} />)
|
||||||
|
|
||||||
|
expect(capturedUsers.map(user => user.id)).toEqual(['user-1', 'user-2'])
|
||||||
|
expect(screen.getByText('Hello')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('time:10000')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('updates hover state on enter and leave', () => {
|
||||||
|
const comment = createComment()
|
||||||
|
const { container } = render(<CommentPreview comment={comment} />)
|
||||||
|
const root = container.firstElementChild as HTMLElement
|
||||||
|
|
||||||
|
fireEvent.mouseEnter(root)
|
||||||
|
fireEvent.mouseLeave(root)
|
||||||
|
|
||||||
|
expect(mockSetHovering).toHaveBeenCalledWith(true)
|
||||||
|
expect(mockSetHovering).toHaveBeenCalledWith(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('clears hover state on unmount', () => {
|
||||||
|
const comment = createComment()
|
||||||
|
const { unmount } = render(<CommentPreview comment={comment} />)
|
||||||
|
|
||||||
|
unmount()
|
||||||
|
|
||||||
|
expect(mockSetHovering).toHaveBeenCalledWith(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
45
web/app/components/workflow/comment/cursor.spec.tsx
Normal file
45
web/app/components/workflow/comment/cursor.spec.tsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { ControlMode } from '../types'
|
||||||
|
import { CommentCursor } from './cursor'
|
||||||
|
|
||||||
|
const mockState = {
|
||||||
|
controlMode: ControlMode.Pointer,
|
||||||
|
mousePosition: {
|
||||||
|
elementX: 10,
|
||||||
|
elementY: 20,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
vi.mock('@/app/components/base/icons/src/public/other', () => ({
|
||||||
|
Comment: (props: { className?: string }) => <svg data-testid="comment-icon" {...props} />,
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('../store', () => ({
|
||||||
|
useStore: (selector: (state: typeof mockState) => unknown) => selector(mockState),
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('CommentCursor', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders nothing when not in comment mode', () => {
|
||||||
|
mockState.controlMode = ControlMode.Pointer
|
||||||
|
|
||||||
|
render(<CommentCursor />)
|
||||||
|
|
||||||
|
expect(screen.queryByTestId('comment-icon')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders at current mouse position when in comment mode', () => {
|
||||||
|
mockState.controlMode = ControlMode.Comment
|
||||||
|
|
||||||
|
render(<CommentCursor />)
|
||||||
|
|
||||||
|
const icon = screen.getByTestId('comment-icon')
|
||||||
|
const container = icon.parentElement as HTMLElement
|
||||||
|
|
||||||
|
expect(container).toHaveStyle({ left: '10px', top: '20px' })
|
||||||
|
})
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue
Block a user