mirror of https://github.com/langgenius/dify.git
test: add tests for some base components (#32265)
This commit is contained in:
parent
a4e03d6284
commit
98466e2d29
|
|
@ -0,0 +1,260 @@
|
|||
import type { ComponentProps } from 'react'
|
||||
import type { IChatItem } from '@/app/components/base/chat/chat/type'
|
||||
import type { AgentLogDetailResponse } from '@/models/log'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { fetchAgentLogDetail } from '@/service/log'
|
||||
import AgentLogDetail from './detail'
|
||||
|
||||
vi.mock('@/service/log', () => ({
|
||||
fetchAgentLogDetail: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/app/store', () => ({
|
||||
useStore: vi.fn(selector => selector({ appDetail: { id: 'app-id' } })),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/run/status', () => ({
|
||||
default: ({ status, time, tokens, error }: { status: string, time?: number, tokens?: number, error?: string }) => (
|
||||
<div data-testid="status-panel" data-status={String(status)} data-time={String(time)} data-tokens={String(tokens)}>{error ? <span>{String(error)}</span> : null}</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/nodes/_base/components/editor/code-editor', () => ({
|
||||
default: ({ title, value }: { title: React.ReactNode, value: string | object }) => (
|
||||
<div data-testid="code-editor">
|
||||
{title}
|
||||
{typeof value === 'string' ? value : JSON.stringify(value)}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-timestamp', () => ({
|
||||
default: () => ({ formatTime: (ts: number, fmt: string) => `${ts}-${fmt}` }),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/block-icon', () => ({
|
||||
default: () => <div data-testid="block-icon" />,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/icons/src/vender/line/arrows', () => ({
|
||||
ChevronRight: (props: { className?: string }) => <div data-testid="chevron-right" className={props.className} />,
|
||||
}))
|
||||
|
||||
const createMockLog = (overrides: Partial<IChatItem> = {}): IChatItem => ({
|
||||
id: 'msg-id',
|
||||
content: 'output content',
|
||||
isAnswer: false,
|
||||
conversationId: 'conv-id',
|
||||
input: 'user input',
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const createMockResponse = (overrides: Partial<AgentLogDetailResponse> = {}): AgentLogDetailResponse => ({
|
||||
meta: {
|
||||
status: 'succeeded',
|
||||
executor: 'User',
|
||||
start_time: '2023-01-01',
|
||||
elapsed_time: 1.0,
|
||||
total_tokens: 100,
|
||||
agent_mode: 'function_call',
|
||||
iterations: 1,
|
||||
},
|
||||
iterations: [
|
||||
{
|
||||
created_at: '',
|
||||
files: [],
|
||||
thought: '',
|
||||
tokens: 0,
|
||||
tool_raw: { inputs: '', outputs: '' },
|
||||
tool_calls: [{ tool_name: 'tool1', status: 'success', tool_icon: null, tool_label: { 'en-US': 'Tool 1' } }],
|
||||
},
|
||||
],
|
||||
files: [],
|
||||
...overrides,
|
||||
})
|
||||
|
||||
describe('AgentLogDetail', () => {
|
||||
const notify = vi.fn()
|
||||
|
||||
const renderComponent = (props: Partial<ComponentProps<typeof AgentLogDetail>> = {}) => {
|
||||
const defaultProps: ComponentProps<typeof AgentLogDetail> = {
|
||||
conversationID: 'conv-id',
|
||||
messageID: 'msg-id',
|
||||
log: createMockLog(),
|
||||
}
|
||||
return render(
|
||||
<ToastContext.Provider value={{ notify, close: vi.fn() } as ComponentProps<typeof ToastContext.Provider>['value']}>
|
||||
<AgentLogDetail {...defaultProps} {...props} />
|
||||
</ToastContext.Provider>,
|
||||
)
|
||||
}
|
||||
|
||||
const renderAndWaitForData = async (props: Partial<ComponentProps<typeof AgentLogDetail>> = {}) => {
|
||||
const result = renderComponent(props)
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('status')).not.toBeInTheDocument()
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should show loading indicator while fetching data', async () => {
|
||||
vi.mocked(fetchAgentLogDetail).mockReturnValue(new Promise(() => {}))
|
||||
|
||||
renderComponent()
|
||||
|
||||
expect(screen.getByRole('status')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should display result panel after data loads', async () => {
|
||||
vi.mocked(fetchAgentLogDetail).mockResolvedValue(createMockResponse())
|
||||
|
||||
await renderAndWaitForData()
|
||||
|
||||
expect(screen.getByText(/runLog.detail/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/runLog.tracing/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call fetchAgentLogDetail with correct params', async () => {
|
||||
vi.mocked(fetchAgentLogDetail).mockResolvedValue(createMockResponse())
|
||||
|
||||
await renderAndWaitForData()
|
||||
|
||||
expect(fetchAgentLogDetail).toHaveBeenCalledWith({
|
||||
appID: 'app-id',
|
||||
params: {
|
||||
conversation_id: 'conv-id',
|
||||
message_id: 'msg-id',
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Props', () => {
|
||||
it('should default to DETAIL tab when activeTab is not provided', async () => {
|
||||
vi.mocked(fetchAgentLogDetail).mockResolvedValue(createMockResponse())
|
||||
|
||||
await renderAndWaitForData()
|
||||
|
||||
const detailTab = screen.getByText(/runLog.detail/i)
|
||||
expect(detailTab.getAttribute('data-active')).toBe('true')
|
||||
})
|
||||
|
||||
it('should show TRACING tab when activeTab is TRACING', async () => {
|
||||
vi.mocked(fetchAgentLogDetail).mockResolvedValue(createMockResponse())
|
||||
|
||||
await renderAndWaitForData({ activeTab: 'TRACING' })
|
||||
|
||||
const tracingTab = screen.getByText(/runLog.tracing/i)
|
||||
expect(tracingTab.getAttribute('data-active')).toBe('true')
|
||||
})
|
||||
})
|
||||
|
||||
describe('User Interactions', () => {
|
||||
it('should switch to TRACING tab when clicked', async () => {
|
||||
vi.mocked(fetchAgentLogDetail).mockResolvedValue(createMockResponse())
|
||||
|
||||
await renderAndWaitForData()
|
||||
|
||||
fireEvent.click(screen.getByText(/runLog.tracing/i))
|
||||
|
||||
await waitFor(() => {
|
||||
const tracingTab = screen.getByText(/runLog.tracing/i)
|
||||
expect(tracingTab.getAttribute('data-active')).toBe('true')
|
||||
})
|
||||
|
||||
const detailTab = screen.getByText(/runLog.detail/i)
|
||||
expect(detailTab.getAttribute('data-active')).toBe('false')
|
||||
})
|
||||
|
||||
it('should switch back to DETAIL tab after switching to TRACING', async () => {
|
||||
vi.mocked(fetchAgentLogDetail).mockResolvedValue(createMockResponse())
|
||||
|
||||
await renderAndWaitForData()
|
||||
|
||||
fireEvent.click(screen.getByText(/runLog.tracing/i))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/runLog.tracing/i).getAttribute('data-active')).toBe('true')
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByText(/runLog.detail/i))
|
||||
|
||||
await waitFor(() => {
|
||||
const detailTab = screen.getByText(/runLog.detail/i)
|
||||
expect(detailTab.getAttribute('data-active')).toBe('true')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should notify on API error', async () => {
|
||||
vi.mocked(fetchAgentLogDetail).mockRejectedValue(new Error('API Error'))
|
||||
|
||||
renderComponent()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(notify).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
message: 'Error: API Error',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should stop loading after API error', async () => {
|
||||
vi.mocked(fetchAgentLogDetail).mockRejectedValue(new Error('Network failure'))
|
||||
|
||||
renderComponent()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('status')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle response with empty iterations', async () => {
|
||||
vi.mocked(fetchAgentLogDetail).mockResolvedValue(
|
||||
createMockResponse({ iterations: [] }),
|
||||
)
|
||||
|
||||
await renderAndWaitForData()
|
||||
})
|
||||
|
||||
it('should handle response with multiple iterations and duplicate tools', async () => {
|
||||
const response = createMockResponse({
|
||||
iterations: [
|
||||
{
|
||||
created_at: '',
|
||||
files: [],
|
||||
thought: '',
|
||||
tokens: 0,
|
||||
tool_raw: { inputs: '', outputs: '' },
|
||||
tool_calls: [
|
||||
{ tool_name: 'tool1', status: 'success', tool_icon: null, tool_label: { 'en-US': 'Tool 1' } },
|
||||
{ tool_name: 'tool2', status: 'success', tool_icon: null, tool_label: { 'en-US': 'Tool 2' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
created_at: '',
|
||||
files: [],
|
||||
thought: '',
|
||||
tokens: 0,
|
||||
tool_raw: { inputs: '', outputs: '' },
|
||||
tool_calls: [
|
||||
{ tool_name: 'tool1', status: 'success', tool_icon: null, tool_label: { 'en-US': 'Tool 1' } },
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
vi.mocked(fetchAgentLogDetail).mockResolvedValue(response)
|
||||
|
||||
await renderAndWaitForData()
|
||||
|
||||
expect(screen.getByText(/runLog.detail/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -89,6 +89,7 @@ const AgentLogDetail: FC<AgentLogDetailProps> = ({
|
|||
'mr-6 cursor-pointer border-b-2 border-transparent py-3 text-[13px] font-semibold leading-[18px] text-text-tertiary',
|
||||
currentTab === 'DETAIL' && '!border-[rgb(21,94,239)] text-text-secondary',
|
||||
)}
|
||||
data-active={currentTab === 'DETAIL'}
|
||||
onClick={() => switchTab('DETAIL')}
|
||||
>
|
||||
{t('detail', { ns: 'runLog' })}
|
||||
|
|
@ -98,6 +99,7 @@ const AgentLogDetail: FC<AgentLogDetailProps> = ({
|
|||
'mr-6 cursor-pointer border-b-2 border-transparent py-3 text-[13px] font-semibold leading-[18px] text-text-tertiary',
|
||||
currentTab === 'TRACING' && '!border-[rgb(21,94,239)] text-text-secondary',
|
||||
)}
|
||||
data-active={currentTab === 'TRACING'}
|
||||
onClick={() => switchTab('TRACING')}
|
||||
>
|
||||
{t('tracing', { ns: 'runLog' })}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,142 @@
|
|||
import type { IChatItem } from '@/app/components/base/chat/chat/type'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { useClickAway } from 'ahooks'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { fetchAgentLogDetail } from '@/service/log'
|
||||
import AgentLogModal from './index'
|
||||
|
||||
vi.mock('@/service/log', () => ({
|
||||
fetchAgentLogDetail: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/app/store', () => ({
|
||||
useStore: vi.fn(selector => selector({ appDetail: { id: 'app-id' } })),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/run/status', () => ({
|
||||
default: ({ status, time, tokens, error }: { status: string, time?: number, tokens?: number, error?: string }) => (
|
||||
<div data-testid="status-panel" data-status={String(status)} data-time={String(time)} data-tokens={String(tokens)}>{error ? <span>{String(error)}</span> : null}</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/nodes/_base/components/editor/code-editor', () => ({
|
||||
default: ({ title, value }: { title: React.ReactNode, value: string | object }) => (
|
||||
<div data-testid="code-editor">
|
||||
{title}
|
||||
{typeof value === 'string' ? value : JSON.stringify(value)}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-timestamp', () => ({
|
||||
default: () => ({ formatTime: (ts: number, fmt: string) => `${ts}-${fmt}` }),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/block-icon', () => ({
|
||||
default: () => <div data-testid="block-icon" />,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/icons/src/vender/line/arrows', () => ({
|
||||
ChevronRight: (props: { className?: string }) => <div data-testid="chevron-right" className={props.className} />,
|
||||
}))
|
||||
|
||||
vi.mock('ahooks', () => ({
|
||||
useClickAway: vi.fn(),
|
||||
}))
|
||||
|
||||
const mockLog = {
|
||||
id: 'msg-id',
|
||||
conversationId: 'conv-id',
|
||||
content: 'content',
|
||||
isAnswer: false,
|
||||
input: 'test input',
|
||||
} as IChatItem
|
||||
|
||||
const mockProps = {
|
||||
currentLogItem: mockLog,
|
||||
width: 1000,
|
||||
onCancel: vi.fn(),
|
||||
}
|
||||
|
||||
describe('AgentLogModal', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(fetchAgentLogDetail).mockResolvedValue({
|
||||
meta: {
|
||||
status: 'succeeded',
|
||||
executor: 'User',
|
||||
start_time: '2023-01-01',
|
||||
elapsed_time: 1.0,
|
||||
total_tokens: 100,
|
||||
agent_mode: 'function_call',
|
||||
iterations: 1,
|
||||
},
|
||||
iterations: [{
|
||||
created_at: '',
|
||||
files: [],
|
||||
thought: '',
|
||||
tokens: 0,
|
||||
tool_raw: { inputs: '', outputs: '' },
|
||||
tool_calls: [{ tool_name: 'tool1', status: 'success', tool_icon: null, tool_label: { 'en-US': 'Tool 1' } }],
|
||||
}],
|
||||
files: [],
|
||||
})
|
||||
})
|
||||
|
||||
it('should return null if no currentLogItem', () => {
|
||||
const { container } = render(<AgentLogModal {...mockProps} currentLogItem={undefined} />)
|
||||
expect(container.firstChild).toBeNull()
|
||||
})
|
||||
|
||||
it('should return null if no conversationId', () => {
|
||||
const { container } = render(<AgentLogModal {...mockProps} currentLogItem={{ id: '1' } as unknown as IChatItem} />)
|
||||
expect(container.firstChild).toBeNull()
|
||||
})
|
||||
|
||||
it('should render correctly when log item is provided', async () => {
|
||||
render(
|
||||
<ToastContext.Provider value={{ notify: vi.fn(), close: vi.fn() } as React.ComponentProps<typeof ToastContext.Provider>['value']}>
|
||||
<AgentLogModal {...mockProps} />
|
||||
</ToastContext.Provider>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('appLog.runDetail.workflowTitle')).toBeInTheDocument()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/runLog.detail/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should call onCancel when close button is clicked', () => {
|
||||
vi.mocked(fetchAgentLogDetail).mockReturnValue(new Promise(() => {}))
|
||||
|
||||
render(
|
||||
<ToastContext.Provider value={{ notify: vi.fn(), close: vi.fn() } as React.ComponentProps<typeof ToastContext.Provider>['value']}>
|
||||
<AgentLogModal {...mockProps} />
|
||||
</ToastContext.Provider>,
|
||||
)
|
||||
|
||||
const closeBtn = screen.getByRole('heading', { name: /appLog.runDetail.workflowTitle/i }).nextElementSibling!
|
||||
fireEvent.click(closeBtn)
|
||||
|
||||
expect(mockProps.onCancel).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should call onCancel when clicking away', () => {
|
||||
vi.mocked(fetchAgentLogDetail).mockReturnValue(new Promise(() => {}))
|
||||
|
||||
let clickAwayHandler!: (event: Event) => void
|
||||
vi.mocked(useClickAway).mockImplementation((callback) => {
|
||||
clickAwayHandler = callback
|
||||
})
|
||||
|
||||
render(
|
||||
<ToastContext.Provider value={{ notify: vi.fn(), close: vi.fn() } as React.ComponentProps<typeof ToastContext.Provider>['value']}>
|
||||
<AgentLogModal {...mockProps} />
|
||||
</ToastContext.Provider>,
|
||||
)
|
||||
clickAwayHandler(new Event('click'))
|
||||
|
||||
expect(mockProps.onCancel).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
import type { AgentIteration } from '@/models/log'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import Iteration from './iteration'
|
||||
|
||||
vi.mock('@/app/components/workflow/nodes/_base/components/editor/code-editor', () => ({
|
||||
default: ({ title, value }: { title: React.ReactNode, value: string | object }) => (
|
||||
<div data-testid="code-editor">
|
||||
<div data-testid="code-editor-title">{title}</div>
|
||||
<div data-testid="code-editor-value">{JSON.stringify(value)}</div>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/block-icon', () => ({
|
||||
default: () => <div data-testid="block-icon" />,
|
||||
}))
|
||||
|
||||
const mockIterationInfo: AgentIteration = {
|
||||
created_at: '2023-01-01',
|
||||
files: [],
|
||||
thought: 'Test thought',
|
||||
tokens: 100,
|
||||
tool_calls: [
|
||||
{
|
||||
status: 'success',
|
||||
tool_name: 'test_tool',
|
||||
tool_label: { en: 'Test Tool' },
|
||||
tool_icon: null,
|
||||
},
|
||||
],
|
||||
tool_raw: {
|
||||
inputs: '{}',
|
||||
outputs: 'test output',
|
||||
},
|
||||
}
|
||||
|
||||
describe('Iteration', () => {
|
||||
it('should render final processing when isFinal is true', () => {
|
||||
render(<Iteration iterationInfo={mockIterationInfo} isFinal={true} index={1} />)
|
||||
|
||||
expect(screen.getByText(/appLog.agentLogDetail.finalProcessing/i)).toBeInTheDocument()
|
||||
expect(screen.queryByText(/appLog.agentLogDetail.iteration/i)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render iteration index when isFinal is false', () => {
|
||||
render(<Iteration iterationInfo={mockIterationInfo} isFinal={false} index={2} />)
|
||||
|
||||
expect(screen.getByText(/APPLOG.AGENTLOGDETAIL.ITERATION 2/i)).toBeInTheDocument()
|
||||
expect(screen.queryByText(/appLog.agentLogDetail.finalProcessing/i)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render LLM tool call and subsequent tool calls', () => {
|
||||
render(<Iteration iterationInfo={mockIterationInfo} isFinal={false} index={1} />)
|
||||
expect(screen.getByTitle('LLM')).toBeInTheDocument()
|
||||
expect(screen.getByText('Test Tool')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
import { render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import ResultPanel from './result'
|
||||
|
||||
vi.mock('@/app/components/workflow/nodes/_base/components/editor/code-editor', () => ({
|
||||
default: ({ title, value }: { title: React.ReactNode, value: string | object }) => (
|
||||
<div data-testid="code-editor">
|
||||
<div data-testid="code-editor-title">{title}</div>
|
||||
<div data-testid="code-editor-value">{JSON.stringify(value)}</div>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/run/status', () => ({
|
||||
default: ({ status, time, tokens, error }: { status: string, time?: number, tokens?: number, error?: string }) => (
|
||||
<div data-testid="status-panel">
|
||||
<span>{status}</span>
|
||||
<span>{time}</span>
|
||||
<span>{tokens}</span>
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-timestamp', () => ({
|
||||
default: () => ({
|
||||
formatTime: vi.fn((ts, _format) => `formatted-${ts}`),
|
||||
}),
|
||||
}))
|
||||
|
||||
const mockProps = {
|
||||
status: 'succeeded',
|
||||
elapsed_time: 1.23456,
|
||||
total_tokens: 150,
|
||||
error: '',
|
||||
inputs: { query: 'input' },
|
||||
outputs: { answer: 'output' },
|
||||
created_by: 'User Name',
|
||||
created_at: '2023-01-01T00:00:00Z',
|
||||
agentMode: 'function_call',
|
||||
tools: ['tool1', 'tool2'],
|
||||
iterations: 3,
|
||||
}
|
||||
|
||||
describe('ResultPanel', () => {
|
||||
it('should render status panel and code editors', () => {
|
||||
render(<ResultPanel {...mockProps} />)
|
||||
|
||||
expect(screen.getByTestId('status-panel')).toBeInTheDocument()
|
||||
|
||||
const editors = screen.getAllByTestId('code-editor')
|
||||
expect(editors).toHaveLength(2)
|
||||
|
||||
expect(screen.getByText('INPUT')).toBeInTheDocument()
|
||||
expect(screen.getByText('OUTPUT')).toBeInTheDocument()
|
||||
expect(screen.getByText(JSON.stringify(mockProps.inputs))).toBeInTheDocument()
|
||||
expect(screen.getByText(JSON.stringify(mockProps.outputs))).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should display correct metadata', () => {
|
||||
render(<ResultPanel {...mockProps} />)
|
||||
|
||||
expect(screen.getByText('User Name')).toBeInTheDocument()
|
||||
expect(screen.getByText('1.235s')).toBeInTheDocument() // toFixed(3)
|
||||
expect(screen.getByText('150 Tokens')).toBeInTheDocument()
|
||||
expect(screen.getByText('appDebug.agent.agentModeType.functionCall')).toBeInTheDocument()
|
||||
expect(screen.getByText('tool1, tool2')).toBeInTheDocument()
|
||||
expect(screen.getByText('3')).toBeInTheDocument()
|
||||
|
||||
// Check formatted time
|
||||
expect(screen.getByText(/formatted-/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle missing created_by and tools', () => {
|
||||
render(<ResultPanel {...mockProps} created_by={undefined} tools={[]} />)
|
||||
|
||||
expect(screen.getByText('N/A')).toBeInTheDocument()
|
||||
expect(screen.getByText('Null')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should display ReACT mode correctly', () => {
|
||||
render(<ResultPanel {...mockProps} agentMode="react" />)
|
||||
expect(screen.getByText('appDebug.agent.agentModeType.ReACT')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import ToolCallItem from './tool-call'
|
||||
|
||||
vi.mock('@/app/components/workflow/nodes/_base/components/editor/code-editor', () => ({
|
||||
default: ({ title, value }: { title: React.ReactNode, value: string | object }) => (
|
||||
<div data-testid="code-editor">
|
||||
<div data-testid="code-editor-title">{title}</div>
|
||||
<div data-testid="code-editor-value">{JSON.stringify(value)}</div>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/block-icon', () => ({
|
||||
default: ({ type }: { type: BlockEnum }) => <div data-testid="block-icon" data-type={type} />,
|
||||
}))
|
||||
|
||||
const mockToolCall = {
|
||||
status: 'success',
|
||||
error: null,
|
||||
tool_name: 'test_tool',
|
||||
tool_label: { en: 'Test Tool Label' },
|
||||
tool_icon: 'icon',
|
||||
time_cost: 1.5,
|
||||
tool_input: { query: 'hello' },
|
||||
tool_output: { result: 'world' },
|
||||
}
|
||||
|
||||
describe('ToolCallItem', () => {
|
||||
it('should render tool name correctly for LLM', () => {
|
||||
render(<ToolCallItem toolCall={mockToolCall} isLLM={true} />)
|
||||
expect(screen.getByText('LLM')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('block-icon')).toHaveAttribute('data-type', BlockEnum.LLM)
|
||||
})
|
||||
|
||||
it('should render tool name from label for non-LLM', () => {
|
||||
render(<ToolCallItem toolCall={mockToolCall} isLLM={false} />)
|
||||
expect(screen.getByText('Test Tool Label')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('block-icon')).toHaveAttribute('data-type', BlockEnum.Tool)
|
||||
})
|
||||
|
||||
it('should format time correctly', () => {
|
||||
render(<ToolCallItem toolCall={mockToolCall} isLLM={false} />)
|
||||
expect(screen.getByText('1.500 s')).toBeInTheDocument()
|
||||
|
||||
// Test ms format
|
||||
render(<ToolCallItem toolCall={{ ...mockToolCall, time_cost: 0.5 }} isLLM={false} />)
|
||||
expect(screen.getByText('500.000 ms')).toBeInTheDocument()
|
||||
|
||||
// Test minute format
|
||||
render(<ToolCallItem toolCall={{ ...mockToolCall, time_cost: 65 }} isLLM={false} />)
|
||||
expect(screen.getByText('1 m 5.000 s')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should format token count correctly', () => {
|
||||
render(<ToolCallItem toolCall={mockToolCall} isLLM={true} tokens={1200} />)
|
||||
expect(screen.getByText('1.2K tokens')).toBeInTheDocument()
|
||||
|
||||
render(<ToolCallItem toolCall={mockToolCall} isLLM={true} tokens={800} />)
|
||||
expect(screen.getByText('800 tokens')).toBeInTheDocument()
|
||||
|
||||
render(<ToolCallItem toolCall={mockToolCall} isLLM={true} tokens={1200000} />)
|
||||
expect(screen.getByText('1.2M tokens')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle collapse/expand', () => {
|
||||
render(<ToolCallItem toolCall={mockToolCall} isLLM={false} />)
|
||||
|
||||
expect(screen.queryByTestId('code-editor')).not.toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByText(/Test Tool Label/i))
|
||||
expect(screen.getAllByTestId('code-editor')).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('should display error message when status is error', () => {
|
||||
const errorToolCall = {
|
||||
...mockToolCall,
|
||||
status: 'error',
|
||||
error: 'Something went wrong',
|
||||
}
|
||||
render(<ToolCallItem toolCall={errorToolCall} isLLM={false} />)
|
||||
|
||||
fireEvent.click(screen.getByText(/Test Tool Label/i))
|
||||
expect(screen.getByText('Something went wrong')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should display LLM specific fields when expanded', () => {
|
||||
render(
|
||||
<ToolCallItem
|
||||
toolCall={mockToolCall}
|
||||
isLLM={true}
|
||||
observation="test observation"
|
||||
finalAnswer="test final answer"
|
||||
isFinal={true}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByText('LLM'))
|
||||
|
||||
const titles = screen.getAllByTestId('code-editor-title')
|
||||
const titleTexts = titles.map(t => t.textContent)
|
||||
|
||||
expect(titleTexts).toContain('INPUT')
|
||||
expect(titleTexts).toContain('OUTPUT')
|
||||
expect(titleTexts).toContain('OBSERVATION')
|
||||
expect(titleTexts).toContain('FINAL ANSWER')
|
||||
})
|
||||
|
||||
it('should display THOUGHT instead of FINAL ANSWER when isFinal is false', () => {
|
||||
render(
|
||||
<ToolCallItem
|
||||
toolCall={mockToolCall}
|
||||
isLLM={true}
|
||||
observation="test observation"
|
||||
finalAnswer="test thought"
|
||||
isFinal={false}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByText('LLM'))
|
||||
expect(screen.getByText('THOUGHT')).toBeInTheDocument()
|
||||
expect(screen.queryByText('FINAL ANSWER')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
import type { AgentIteration } from '@/models/log'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import TracingPanel from './tracing'
|
||||
|
||||
vi.mock('@/app/components/workflow/block-icon', () => ({
|
||||
default: () => <div data-testid="block-icon" />,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/icons/src/vender/line/arrows', () => ({
|
||||
ChevronRight: (props: { className?: string }) => <div data-testid="chevron-right" className={props.className} />,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/nodes/_base/components/editor/code-editor', () => ({
|
||||
default: ({ title, value }: { title: React.ReactNode, value: string | object }) => (
|
||||
<div data-testid="code-editor">
|
||||
{title}
|
||||
{typeof value === 'string' ? value : JSON.stringify(value)}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
const createIteration = (thought: string, tokens: number): AgentIteration => ({
|
||||
created_at: '',
|
||||
files: [],
|
||||
thought,
|
||||
tokens,
|
||||
tool_calls: [{ tool_name: 'tool1', status: 'success', tool_icon: null, tool_label: { 'en-US': 'Tool 1' } }],
|
||||
tool_raw: { inputs: '', outputs: '' },
|
||||
})
|
||||
|
||||
const mockList: AgentIteration[] = [
|
||||
createIteration('Thought 1', 10),
|
||||
createIteration('Thought 2', 20),
|
||||
createIteration('Thought 3', 30),
|
||||
]
|
||||
|
||||
describe('TracingPanel', () => {
|
||||
it('should render all iterations in the list', () => {
|
||||
render(<TracingPanel list={mockList} />)
|
||||
|
||||
expect(screen.getByText(/finalProcessing/i)).toBeInTheDocument()
|
||||
expect(screen.getAllByText(/ITERATION/i).length).toBe(2)
|
||||
})
|
||||
|
||||
it('should render empty list correctly', () => {
|
||||
const { container } = render(<TracingPanel list={[]} />)
|
||||
expect(container.querySelector('.bg-background-section')?.children.length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,195 @@
|
|||
/* eslint-disable next/no-img-element */
|
||||
import type { ImgHTMLAttributes } from 'react'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import CheckboxList from '.'
|
||||
|
||||
vi.mock('next/image', () => ({
|
||||
default: (props: ImgHTMLAttributes<HTMLImageElement>) => <img {...props} />,
|
||||
}))
|
||||
|
||||
describe('checkbox list component', () => {
|
||||
const options = [
|
||||
{ label: 'Option 1', value: 'option1' },
|
||||
{ label: 'Option 2', value: 'option2' },
|
||||
{ label: 'Option 3', value: 'option3' },
|
||||
{ label: 'Apple', value: 'apple' },
|
||||
]
|
||||
|
||||
it('renders with title, description and options', () => {
|
||||
render(
|
||||
<CheckboxList
|
||||
title="Test Title"
|
||||
description="Test Description"
|
||||
options={options}
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByText('Test Title')).toBeInTheDocument()
|
||||
expect(screen.getByText('Test Description')).toBeInTheDocument()
|
||||
options.forEach((option) => {
|
||||
expect(screen.getByText(option.label)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('filters options by label', async () => {
|
||||
render(<CheckboxList options={options} />)
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
await userEvent.type(input, 'app')
|
||||
|
||||
expect(screen.getByText('Apple')).toBeInTheDocument()
|
||||
expect(screen.queryByText('Option 2')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('Option 3')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders select-all checkbox', () => {
|
||||
render(<CheckboxList options={options} showSelectAll />)
|
||||
const checkboxes = screen.getByTestId('checkbox-selectAll')
|
||||
expect(checkboxes).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('selects all options when select-all is clicked', async () => {
|
||||
const onChange = vi.fn()
|
||||
|
||||
render(
|
||||
<CheckboxList
|
||||
options={options}
|
||||
value={[]}
|
||||
onChange={onChange}
|
||||
showSelectAll
|
||||
/>,
|
||||
)
|
||||
|
||||
const selectAll = screen.getByTestId('checkbox-selectAll')
|
||||
await userEvent.click(selectAll)
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(['option1', 'option2', 'option3', 'apple'])
|
||||
})
|
||||
|
||||
it('does not select all options when select-all is clicked when disabled', async () => {
|
||||
const onChange = vi.fn()
|
||||
|
||||
render(
|
||||
<CheckboxList
|
||||
options={options}
|
||||
value={[]}
|
||||
disabled
|
||||
showSelectAll
|
||||
onChange={onChange}
|
||||
/>,
|
||||
)
|
||||
|
||||
const selectAll = screen.getByTestId('checkbox-selectAll')
|
||||
await userEvent.click(selectAll)
|
||||
|
||||
expect(onChange).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('deselects all options when select-all is clicked', async () => {
|
||||
const onChange = vi.fn()
|
||||
|
||||
render(
|
||||
<CheckboxList
|
||||
options={options}
|
||||
value={['option1', 'option2', 'option3', 'apple']}
|
||||
onChange={onChange}
|
||||
showSelectAll
|
||||
/>,
|
||||
)
|
||||
|
||||
const selectAll = screen.getByTestId('checkbox-selectAll')
|
||||
await userEvent.click(selectAll)
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith([])
|
||||
})
|
||||
|
||||
it('selects select-all when all options are clicked', async () => {
|
||||
const onChange = vi.fn()
|
||||
|
||||
render(
|
||||
<CheckboxList
|
||||
options={options}
|
||||
value={['option1', 'option2', 'option3', 'apple']}
|
||||
onChange={onChange}
|
||||
showSelectAll
|
||||
/>,
|
||||
)
|
||||
|
||||
const selectAll = screen.getByTestId('checkbox-selectAll')
|
||||
expect(selectAll.querySelector('[data-testid="check-icon-selectAll"]')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('hides select-all checkbox when searching', async () => {
|
||||
render(<CheckboxList options={options} />)
|
||||
await userEvent.type(screen.getByRole('textbox'), 'app')
|
||||
expect(screen.queryByTestId('checkbox-selectAll')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('selects options when checkbox is clicked', async () => {
|
||||
const onChange = vi.fn()
|
||||
|
||||
render(
|
||||
<CheckboxList
|
||||
options={options}
|
||||
value={[]}
|
||||
onChange={onChange}
|
||||
showSelectAll={false}
|
||||
/>,
|
||||
)
|
||||
|
||||
const selectOption = screen.getByTestId('checkbox-option1')
|
||||
await userEvent.click(selectOption)
|
||||
expect(onChange).toHaveBeenCalledWith(['option1'])
|
||||
})
|
||||
|
||||
it('deselects options when checkbox is clicked when selected', async () => {
|
||||
const onChange = vi.fn()
|
||||
|
||||
render(
|
||||
<CheckboxList
|
||||
options={options}
|
||||
value={['option1']}
|
||||
onChange={onChange}
|
||||
showSelectAll={false}
|
||||
/>,
|
||||
)
|
||||
|
||||
const selectOption = screen.getByTestId('checkbox-option1')
|
||||
await userEvent.click(selectOption)
|
||||
expect(onChange).toHaveBeenCalledWith([])
|
||||
})
|
||||
|
||||
it('does not select options when checkbox is clicked', async () => {
|
||||
const onChange = vi.fn()
|
||||
|
||||
render(
|
||||
<CheckboxList
|
||||
options={options}
|
||||
value={[]}
|
||||
onChange={onChange}
|
||||
disabled
|
||||
/>,
|
||||
)
|
||||
|
||||
const selectOption = screen.getByTestId('checkbox-option1')
|
||||
await userEvent.click(selectOption)
|
||||
expect(onChange).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('Reset button works', async () => {
|
||||
const onChange = vi.fn()
|
||||
|
||||
render(
|
||||
<CheckboxList
|
||||
options={options}
|
||||
value={[]}
|
||||
onChange={onChange}
|
||||
/>,
|
||||
)
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
await userEvent.type(input, 'ban')
|
||||
await userEvent.click(screen.getByText('common.operation.resetKeywords'))
|
||||
expect(input).toHaveValue('')
|
||||
})
|
||||
})
|
||||
|
|
@ -101,12 +101,12 @@ const CheckboxList: FC<CheckboxListProps> = ({
|
|||
return (
|
||||
<div className={cn('flex w-full flex-col gap-1', containerClassName)}>
|
||||
{label && (
|
||||
<div className="system-sm-medium text-text-secondary">
|
||||
<div className="text-text-secondary system-sm-medium">
|
||||
{label}
|
||||
</div>
|
||||
)}
|
||||
{description && (
|
||||
<div className="body-xs-regular text-text-tertiary">
|
||||
<div className="text-text-tertiary body-xs-regular">
|
||||
{description}
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -120,13 +120,14 @@ const CheckboxList: FC<CheckboxListProps> = ({
|
|||
indeterminate={isIndeterminate}
|
||||
onCheck={handleSelectAll}
|
||||
disabled={disabled}
|
||||
id="selectAll"
|
||||
/>
|
||||
)}
|
||||
{!searchQuery
|
||||
? (
|
||||
<div className="flex min-w-0 flex-1 items-center gap-1">
|
||||
{title && (
|
||||
<span className="system-xs-semibold-uppercase truncate leading-5 text-text-secondary">
|
||||
<span className="truncate leading-5 text-text-secondary system-xs-semibold-uppercase">
|
||||
{title}
|
||||
</span>
|
||||
)}
|
||||
|
|
@ -138,7 +139,7 @@ const CheckboxList: FC<CheckboxListProps> = ({
|
|||
</div>
|
||||
)
|
||||
: (
|
||||
<div className="system-sm-medium-uppercase flex-1 leading-6 text-text-secondary">
|
||||
<div className="flex-1 leading-6 text-text-secondary system-sm-medium-uppercase">
|
||||
{
|
||||
filteredOptions.length > 0
|
||||
? t('operation.searchCount', { ns: 'common', count: filteredOptions.length, content: title })
|
||||
|
|
@ -168,7 +169,7 @@ const CheckboxList: FC<CheckboxListProps> = ({
|
|||
? (
|
||||
<div className="flex flex-col items-center justify-center gap-2">
|
||||
<Image alt="search menu" src={SearchMenu} width={32} />
|
||||
<span className="system-sm-regular text-text-secondary">{t('operation.noSearchResults', { ns: 'common', content: title })}</span>
|
||||
<span className="text-text-secondary system-sm-regular">{t('operation.noSearchResults', { ns: 'common', content: title })}</span>
|
||||
<Button variant="secondary-accent" size="small" onClick={() => setSearchQuery('')}>{t('operation.resetKeywords', { ns: 'common' })}</Button>
|
||||
</div>
|
||||
)
|
||||
|
|
@ -198,9 +199,10 @@ const CheckboxList: FC<CheckboxListProps> = ({
|
|||
handleToggleOption(option.value)
|
||||
}}
|
||||
disabled={option.disabled || disabled}
|
||||
id={option.value}
|
||||
/>
|
||||
<div
|
||||
className="system-sm-medium flex-1 truncate text-text-secondary"
|
||||
className="flex-1 truncate text-text-secondary system-sm-medium"
|
||||
title={option.label}
|
||||
>
|
||||
{option.label}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,117 @@
|
|||
import { act, fireEvent, render, screen } from '@testing-library/react'
|
||||
import Confirm from '.'
|
||||
|
||||
vi.mock('react-dom', async () => {
|
||||
const actual = await vi.importActual<typeof import('react-dom')>('react-dom')
|
||||
|
||||
return {
|
||||
...actual,
|
||||
createPortal: (children: React.ReactNode) => children,
|
||||
}
|
||||
})
|
||||
|
||||
const onCancel = vi.fn()
|
||||
const onConfirm = vi.fn()
|
||||
|
||||
describe('Confirm Component', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('renders confirm correctly', () => {
|
||||
render(<Confirm isShow={true} title="test title" onCancel={onCancel} onConfirm={onConfirm} />)
|
||||
expect(screen.getByText('test title')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('does not render on isShow false', () => {
|
||||
const { container } = render(<Confirm isShow={false} title="test title" onCancel={onCancel} onConfirm={onConfirm} />)
|
||||
expect(container.firstChild).toBeNull()
|
||||
})
|
||||
|
||||
it('hides after delay when isShow changes to false', () => {
|
||||
vi.useFakeTimers()
|
||||
const { rerender } = render(<Confirm isShow={true} title="test title" onCancel={onCancel} onConfirm={onConfirm} />)
|
||||
expect(screen.getByText('test title')).toBeInTheDocument()
|
||||
|
||||
rerender(<Confirm isShow={false} title="test title" onCancel={onCancel} onConfirm={onConfirm} />)
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(200)
|
||||
})
|
||||
expect(screen.queryByText('test title')).not.toBeInTheDocument()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('renders content when provided', () => {
|
||||
render(<Confirm isShow={true} title="title" content="some description" onCancel={onCancel} onConfirm={onConfirm} />)
|
||||
expect(screen.getByText('some description')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Props', () => {
|
||||
it('showCancel prop works', () => {
|
||||
render(<Confirm isShow={true} title="test title" onCancel={onCancel} onConfirm={onConfirm} showCancel={false} />)
|
||||
expect(screen.getByRole('button', { name: 'common.operation.confirm' })).toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: 'common.operation.cancel' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('showConfirm prop works', () => {
|
||||
render(<Confirm isShow={true} title="test title" onCancel={onCancel} onConfirm={onConfirm} showConfirm={false} />)
|
||||
expect(screen.getByRole('button', { name: 'common.operation.cancel' })).toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: 'common.operation.confirm' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders custom confirm and cancel text', () => {
|
||||
render(<Confirm isShow={true} title="title" confirmText="Yes" cancelText="No" onCancel={onCancel} onConfirm={onConfirm} />)
|
||||
expect(screen.getByRole('button', { name: 'Yes' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'No' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('disables confirm button when isDisabled is true', () => {
|
||||
render(<Confirm isShow={true} title="title" isDisabled={true} onCancel={onCancel} onConfirm={onConfirm} />)
|
||||
expect(screen.getByRole('button', { name: 'common.operation.confirm' })).toBeDisabled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('User Interactions', () => {
|
||||
it('clickAway is handled properly', () => {
|
||||
render(<Confirm isShow={true} title="test title" onCancel={onCancel} onConfirm={onConfirm} />)
|
||||
const overlay = screen.getByTestId('confirm-overlay') as HTMLElement
|
||||
expect(overlay).toBeTruthy()
|
||||
fireEvent.mouseDown(overlay)
|
||||
expect(onCancel).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('overlay click stops propagation', () => {
|
||||
render(<Confirm isShow={true} title="test title" onCancel={onCancel} onConfirm={onConfirm} />)
|
||||
const overlay = screen.getByTestId('confirm-overlay') as HTMLElement
|
||||
const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true })
|
||||
const preventDefaultSpy = vi.spyOn(clickEvent, 'preventDefault')
|
||||
const stopPropagationSpy = vi.spyOn(clickEvent, 'stopPropagation')
|
||||
overlay.dispatchEvent(clickEvent)
|
||||
expect(preventDefaultSpy).toHaveBeenCalled()
|
||||
expect(stopPropagationSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not close on click away when maskClosable is false', () => {
|
||||
render(<Confirm isShow={true} title="test title" maskClosable={false} onCancel={onCancel} onConfirm={onConfirm} />)
|
||||
const overlay = screen.getByTestId('confirm-overlay') as HTMLElement
|
||||
fireEvent.mouseDown(overlay)
|
||||
expect(onCancel).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('escape keyboard event works', () => {
|
||||
render(<Confirm isShow={true} title="test title" onCancel={onCancel} onConfirm={onConfirm} />)
|
||||
fireEvent.keyDown(document, { key: 'Escape' })
|
||||
expect(onCancel).toHaveBeenCalledTimes(1)
|
||||
expect(onConfirm).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('Enter keyboard event works', () => {
|
||||
render(<Confirm isShow={true} title="test title" onCancel={onCancel} onConfirm={onConfirm} />)
|
||||
fireEvent.keyDown(document, { key: 'Enter' })
|
||||
expect(onConfirm).toHaveBeenCalledTimes(1)
|
||||
expect(onCancel).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -101,6 +101,7 @@ function Confirm({
|
|||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}}
|
||||
data-testid="confirm-overlay"
|
||||
>
|
||||
<div ref={dialogRef} className="relative w-full max-w-[480px] overflow-hidden">
|
||||
<div className="shadows-shadow-lg flex max-w-full flex-col items-start rounded-2xl border-[0.5px] border-solid border-components-panel-border bg-components-panel-bg">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,93 @@
|
|||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import CopyFeedback, { CopyFeedbackNew } from '.'
|
||||
|
||||
const mockCopy = vi.fn()
|
||||
const mockReset = vi.fn()
|
||||
let mockCopied = false
|
||||
|
||||
vi.mock('foxact/use-clipboard', () => ({
|
||||
useClipboard: () => ({
|
||||
copy: mockCopy,
|
||||
reset: mockReset,
|
||||
copied: mockCopied,
|
||||
}),
|
||||
}))
|
||||
|
||||
describe('CopyFeedback', () => {
|
||||
beforeEach(() => {
|
||||
mockCopied = false
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('renders the action button with copy icon', () => {
|
||||
render(<CopyFeedback content="test content" />)
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders the copied icon when copied is true', () => {
|
||||
mockCopied = true
|
||||
render(<CopyFeedback content="test content" />)
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('User Interactions', () => {
|
||||
it('calls copy with content when clicked', () => {
|
||||
render(<CopyFeedback content="test content" />)
|
||||
const button = screen.getByRole('button')
|
||||
fireEvent.click(button.firstChild as Element)
|
||||
expect(mockCopy).toHaveBeenCalledWith('test content')
|
||||
})
|
||||
|
||||
it('calls reset on mouse leave', () => {
|
||||
render(<CopyFeedback content="test content" />)
|
||||
const button = screen.getByRole('button')
|
||||
fireEvent.mouseLeave(button.firstChild as Element)
|
||||
expect(mockReset).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('CopyFeedbackNew', () => {
|
||||
beforeEach(() => {
|
||||
mockCopied = false
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('renders the component', () => {
|
||||
const { container } = render(<CopyFeedbackNew content="test content" />)
|
||||
expect(container.querySelector('.cursor-pointer')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('applies copied CSS class when copied is true', () => {
|
||||
mockCopied = true
|
||||
const { container } = render(<CopyFeedbackNew content="test content" />)
|
||||
const feedbackIcon = container.firstChild?.firstChild as Element
|
||||
expect(feedbackIcon).toHaveClass(/_copied_.*/)
|
||||
})
|
||||
|
||||
it('does not apply copied CSS class when not copied', () => {
|
||||
const { container } = render(<CopyFeedbackNew content="test content" />)
|
||||
const feedbackIcon = container.firstChild?.firstChild as Element
|
||||
expect(feedbackIcon).not.toHaveClass(/_copied_.*/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('User Interactions', () => {
|
||||
it('calls copy with content when clicked', () => {
|
||||
const { container } = render(<CopyFeedbackNew content="test content" />)
|
||||
const clickableArea = container.querySelector('.cursor-pointer')!.firstChild as HTMLElement
|
||||
fireEvent.click(clickableArea)
|
||||
expect(mockCopy).toHaveBeenCalledWith('test content')
|
||||
})
|
||||
|
||||
it('calls reset on mouse leave', () => {
|
||||
const { container } = render(<CopyFeedbackNew content="test content" />)
|
||||
const clickableArea = container.querySelector('.cursor-pointer')!.firstChild as HTMLElement
|
||||
fireEvent.mouseLeave(clickableArea)
|
||||
expect(mockReset).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import EmojiPickerInner from './Inner'
|
||||
|
||||
vi.mock('@emoji-mart/data', () => ({
|
||||
default: {
|
||||
categories: [
|
||||
{
|
||||
id: 'nature',
|
||||
emojis: ['rabbit', 'bear'],
|
||||
},
|
||||
{
|
||||
id: 'food',
|
||||
emojis: ['apple', 'orange'],
|
||||
},
|
||||
],
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('emoji-mart', () => ({
|
||||
init: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/emoji', () => ({
|
||||
searchEmoji: vi.fn().mockResolvedValue(['dog', 'cat']),
|
||||
}))
|
||||
|
||||
describe('EmojiPickerInner', () => {
|
||||
const mockOnSelect = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
// Define the custom element to avoid "Unknown custom element" warnings
|
||||
if (!customElements.get('em-emoji')) {
|
||||
customElements.define('em-emoji', class extends HTMLElement {
|
||||
static get observedAttributes() { return ['id'] }
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('renders initial categories and emojis correctly', () => {
|
||||
render(<EmojiPickerInner onSelect={mockOnSelect} />)
|
||||
|
||||
expect(screen.getByText('nature')).toBeInTheDocument()
|
||||
expect(screen.getByText('food')).toBeInTheDocument()
|
||||
expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('User Interactions', () => {
|
||||
it('calls searchEmoji and displays results when typing in search input', async () => {
|
||||
render(<EmojiPickerInner onSelect={mockOnSelect} />)
|
||||
const searchInput = screen.getByPlaceholderText('Search emojis...')
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.change(searchInput, { target: { value: 'anim' } })
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Search')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
const searchSection = screen.getByText('Search').parentElement
|
||||
expect(searchSection?.querySelectorAll('em-emoji').length).toBe(2)
|
||||
})
|
||||
|
||||
it('updates selected emoji and calls onSelect when an emoji is clicked', async () => {
|
||||
render(<EmojiPickerInner onSelect={mockOnSelect} />)
|
||||
const emojiContainers = screen.getAllByTestId(/^emoji-container-/)
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(emojiContainers[0])
|
||||
})
|
||||
|
||||
expect(mockOnSelect).toHaveBeenCalledWith('rabbit', expect.any(String))
|
||||
})
|
||||
|
||||
it('toggles style colors display when clicking the chevron', async () => {
|
||||
render(<EmojiPickerInner onSelect={mockOnSelect} />)
|
||||
|
||||
expect(screen.queryByText('#FFEAD5')).not.toBeInTheDocument()
|
||||
|
||||
const toggleButton = screen.getByTestId('toggle-colors')
|
||||
expect(toggleButton).toBeInTheDocument()
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(toggleButton!)
|
||||
})
|
||||
|
||||
expect(screen.getByText('Choose Style')).toBeInTheDocument()
|
||||
const colorOptions = document.querySelectorAll('[style^="background:"]')
|
||||
expect(colorOptions.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('updates background color and calls onSelect when a color is clicked', async () => {
|
||||
render(<EmojiPickerInner onSelect={mockOnSelect} />)
|
||||
|
||||
const toggleButton = screen.getByTestId('toggle-colors')
|
||||
await act(async () => {
|
||||
fireEvent.click(toggleButton!)
|
||||
})
|
||||
|
||||
const emojiContainers = screen.getAllByTestId(/^emoji-container-/)
|
||||
await act(async () => {
|
||||
fireEvent.click(emojiContainers[0])
|
||||
})
|
||||
|
||||
mockOnSelect.mockClear()
|
||||
|
||||
const colorOptions = document.querySelectorAll('[style^="background:"]')
|
||||
await act(async () => {
|
||||
fireEvent.click(colorOptions[1].parentElement!)
|
||||
})
|
||||
|
||||
expect(mockOnSelect).toHaveBeenCalledWith('rabbit', '#E4FBCC')
|
||||
})
|
||||
|
||||
it('updates selected emoji when clicking a search result', async () => {
|
||||
render(<EmojiPickerInner onSelect={mockOnSelect} />)
|
||||
const searchInput = screen.getByPlaceholderText('Search emojis...')
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.change(searchInput, { target: { value: 'anim' } })
|
||||
})
|
||||
|
||||
await screen.findByText('Search')
|
||||
|
||||
const searchEmojis = screen.getAllByTestId(/^emoji-search-result-/)
|
||||
await act(async () => {
|
||||
fireEvent.click(searchEmojis![0])
|
||||
})
|
||||
|
||||
expect(mockOnSelect).toHaveBeenCalledWith('dog', expect.any(String))
|
||||
})
|
||||
|
||||
it('toggles style colors display back and forth', async () => {
|
||||
render(<EmojiPickerInner onSelect={mockOnSelect} />)
|
||||
|
||||
const toggleButton = screen.getByTestId('toggle-colors')
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(toggleButton!)
|
||||
})
|
||||
expect(screen.getByText('Choose Style')).toBeInTheDocument()
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByTestId('toggle-colors')!) // It should be the other icon now
|
||||
})
|
||||
expect(screen.queryByText('#FFEAD5')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('clears search results when input is cleared', async () => {
|
||||
render(<EmojiPickerInner onSelect={mockOnSelect} />)
|
||||
const searchInput = screen.getByPlaceholderText('Search emojis...')
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.change(searchInput, { target: { value: 'anim' } })
|
||||
})
|
||||
|
||||
await screen.findByText('Search')
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.change(searchInput, { target: { value: '' } })
|
||||
})
|
||||
|
||||
expect(screen.queryByText('Search')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -3,8 +3,6 @@ import type { EmojiMartData } from '@emoji-mart/data'
|
|||
import type { ChangeEvent, FC } from 'react'
|
||||
import data from '@emoji-mart/data'
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
ChevronUpIcon,
|
||||
MagnifyingGlassIcon,
|
||||
} from '@heroicons/react/24/outline'
|
||||
import { init } from 'emoji-mart'
|
||||
|
|
@ -97,7 +95,7 @@ const EmojiPickerInner: FC<IEmojiPickerInnerProps> = ({
|
|||
{isSearching && (
|
||||
<>
|
||||
<div key="category-search" className="flex flex-col">
|
||||
<p className="system-xs-medium-uppercase mb-1 text-text-primary">Search</p>
|
||||
<p className="mb-1 text-text-primary system-xs-medium-uppercase">Search</p>
|
||||
<div className="grid h-full w-full grid-cols-8 gap-1">
|
||||
{searchedEmojis.map((emoji: string, index: number) => {
|
||||
return (
|
||||
|
|
@ -108,7 +106,7 @@ const EmojiPickerInner: FC<IEmojiPickerInnerProps> = ({
|
|||
setSelectedEmoji(emoji)
|
||||
}}
|
||||
>
|
||||
<div className="flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg p-1 ring-components-input-border-hover ring-offset-1 hover:ring-1">
|
||||
<div className="flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg p-1 ring-components-input-border-hover ring-offset-1 hover:ring-1" data-testid={`emoji-search-result-${emoji}`}>
|
||||
<em-emoji id={emoji} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -122,7 +120,7 @@ const EmojiPickerInner: FC<IEmojiPickerInnerProps> = ({
|
|||
{categories.map((category, index: number) => {
|
||||
return (
|
||||
<div key={`category-${index}`} className="flex flex-col">
|
||||
<p className="system-xs-medium-uppercase mb-1 text-text-primary">{category.id}</p>
|
||||
<p className="mb-1 text-text-primary system-xs-medium-uppercase">{category.id}</p>
|
||||
<div className="grid h-full w-full grid-cols-8 gap-1">
|
||||
{category.emojis.map((emoji, index: number) => {
|
||||
return (
|
||||
|
|
@ -133,7 +131,7 @@ const EmojiPickerInner: FC<IEmojiPickerInnerProps> = ({
|
|||
setSelectedEmoji(emoji)
|
||||
}}
|
||||
>
|
||||
<div className="flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg p-1 ring-components-input-border-hover ring-offset-1 hover:ring-1">
|
||||
<div className="flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg p-1 ring-components-input-border-hover ring-offset-1 hover:ring-1" data-testid={`emoji-container-${emoji}`}>
|
||||
<em-emoji id={emoji} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -148,10 +146,10 @@ const EmojiPickerInner: FC<IEmojiPickerInnerProps> = ({
|
|||
|
||||
{/* Color Select */}
|
||||
<div className={cn('flex items-center justify-between p-3 pb-0')}>
|
||||
<p className="system-xs-medium-uppercase mb-2 text-text-primary">Choose Style</p>
|
||||
<p className="mb-2 text-text-primary system-xs-medium-uppercase">Choose Style</p>
|
||||
{showStyleColors
|
||||
? <ChevronDownIcon className="h-4 w-4 cursor-pointer text-text-quaternary" onClick={() => setShowStyleColors(!showStyleColors)} />
|
||||
: <ChevronUpIcon className="h-4 w-4 cursor-pointer text-text-quaternary" onClick={() => setShowStyleColors(!showStyleColors)} />}
|
||||
? <span className="i-heroicons-chevron-down h-4 w-4 cursor-pointer text-text-quaternary" onClick={() => setShowStyleColors(!showStyleColors)} data-testid="toggle-colors" />
|
||||
: <span className="i-heroicons-chevron-up h-4 w-4 cursor-pointer text-text-quaternary" onClick={() => setShowStyleColors(!showStyleColors)} data-testid="toggle-colors" />}
|
||||
</div>
|
||||
{showStyleColors && (
|
||||
<div className="grid w-full grid-cols-8 gap-1 px-3">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,115 @@
|
|||
import { act, fireEvent, render, screen } from '@testing-library/react'
|
||||
import EmojiPicker from './index'
|
||||
|
||||
vi.mock('@emoji-mart/data', () => ({
|
||||
default: {
|
||||
categories: [
|
||||
{
|
||||
id: 'category1',
|
||||
name: 'Category 1',
|
||||
emojis: ['emoji1', 'emoji2'],
|
||||
},
|
||||
],
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('emoji-mart', () => ({
|
||||
init: vi.fn(),
|
||||
SearchIndex: {
|
||||
search: vi.fn().mockResolvedValue([{ skins: [{ native: '🔍' }] }]),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/emoji', () => ({
|
||||
searchEmoji: vi.fn().mockResolvedValue(['🔍']),
|
||||
}))
|
||||
|
||||
describe('EmojiPicker', () => {
|
||||
const mockOnSelect = vi.fn()
|
||||
const mockOnClose = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('renders nothing when isModal is false', () => {
|
||||
const { container } = render(
|
||||
<EmojiPicker isModal={false} />,
|
||||
)
|
||||
expect(container.firstChild).toBeNull()
|
||||
})
|
||||
|
||||
it('renders modal when isModal is true', async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<EmojiPicker isModal={true} />,
|
||||
)
|
||||
})
|
||||
expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument()
|
||||
expect(screen.getByText(/Cancel/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/OK/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('OK button is disabled initially', async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<EmojiPicker />,
|
||||
)
|
||||
})
|
||||
const okButton = screen.getByText(/OK/i).closest('button')
|
||||
expect(okButton).toBeDisabled()
|
||||
})
|
||||
|
||||
it('applies custom className to modal wrapper', async () => {
|
||||
const customClass = 'custom-wrapper-class'
|
||||
await act(async () => {
|
||||
render(
|
||||
<EmojiPicker className={customClass} />,
|
||||
)
|
||||
})
|
||||
const dialog = screen.getByRole('dialog')
|
||||
expect(dialog).toHaveClass(customClass)
|
||||
})
|
||||
})
|
||||
|
||||
describe('User Interactions', () => {
|
||||
it('calls onSelect with selected emoji and background when OK is clicked', async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<EmojiPicker onSelect={mockOnSelect} />,
|
||||
)
|
||||
})
|
||||
|
||||
const emojiWrappers = screen.getAllByTestId(/^emoji-container-/)
|
||||
expect(emojiWrappers.length).toBeGreaterThan(0)
|
||||
await act(async () => {
|
||||
fireEvent.click(emojiWrappers[0])
|
||||
})
|
||||
|
||||
const okButton = screen.getByText(/OK/i)
|
||||
expect(okButton.closest('button')).not.toBeDisabled()
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(okButton)
|
||||
})
|
||||
|
||||
expect(mockOnSelect).toHaveBeenCalledWith(expect.any(String), expect.any(String))
|
||||
})
|
||||
|
||||
it('calls onClose when Cancel is clicked', async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<EmojiPicker onClose={mockOnClose} />,
|
||||
)
|
||||
})
|
||||
|
||||
const cancelButton = screen.getByText(/Cancel/i)
|
||||
await act(async () => {
|
||||
fireEvent.click(cancelButton)
|
||||
})
|
||||
|
||||
expect(mockOnClose).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import { render, screen } from '@testing-library/react'
|
||||
import ImageRender from './image-render'
|
||||
|
||||
describe('ImageRender Component', () => {
|
||||
const mockProps = {
|
||||
sourceUrl: 'https://example.com/image.jpg',
|
||||
name: 'test-image.jpg',
|
||||
}
|
||||
|
||||
describe('Render', () => {
|
||||
it('renders image with correct src and alt', () => {
|
||||
render(<ImageRender {...mockProps} />)
|
||||
|
||||
const img = screen.getByRole('img')
|
||||
expect(img).toBeInTheDocument()
|
||||
expect(img).toHaveAttribute('src', mockProps.sourceUrl)
|
||||
expect(img).toHaveAttribute('alt', mockProps.name)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
/* eslint-disable next/no-img-element */
|
||||
import type { ImgHTMLAttributes } from 'react'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import FileThumb from './index'
|
||||
|
||||
vi.mock('next/image', () => ({
|
||||
__esModule: true,
|
||||
default: (props: ImgHTMLAttributes<HTMLImageElement>) => <img {...props} />,
|
||||
}))
|
||||
|
||||
describe('FileThumb Component', () => {
|
||||
const mockImageFile = {
|
||||
name: 'test-image.jpg',
|
||||
mimeType: 'image/jpeg',
|
||||
extension: '.jpg',
|
||||
size: 1024,
|
||||
sourceUrl: 'https://example.com/test-image.jpg',
|
||||
}
|
||||
|
||||
const mockNonImageFile = {
|
||||
name: 'test.pdf',
|
||||
mimeType: 'application/pdf',
|
||||
extension: '.pdf',
|
||||
size: 2048,
|
||||
sourceUrl: 'https://example.com/test.pdf',
|
||||
}
|
||||
|
||||
describe('Render', () => {
|
||||
it('renders image thumbnail correctly', () => {
|
||||
render(<FileThumb file={mockImageFile} />)
|
||||
|
||||
const img = screen.getByAltText(mockImageFile.name)
|
||||
expect(img).toBeInTheDocument()
|
||||
expect(img).toHaveAttribute('src', mockImageFile.sourceUrl)
|
||||
})
|
||||
|
||||
it('renders file type icon for non-image files', () => {
|
||||
const { container } = render(<FileThumb file={mockNonImageFile} />)
|
||||
|
||||
expect(screen.queryByAltText(mockNonImageFile.name)).not.toBeInTheDocument()
|
||||
const svgIcon = container.querySelector('svg')
|
||||
expect(svgIcon).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('wraps content inside tooltip', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<FileThumb file={mockImageFile} />)
|
||||
|
||||
const trigger = screen.getByAltText(mockImageFile.name)
|
||||
expect(trigger).toBeInTheDocument()
|
||||
|
||||
await user.hover(trigger)
|
||||
|
||||
const tooltipContent = await screen.findByText(mockImageFile.name)
|
||||
expect(tooltipContent).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Interaction', () => {
|
||||
it('calls onClick with file when clicked', () => {
|
||||
const onClick = vi.fn()
|
||||
|
||||
render(<FileThumb file={mockImageFile} onClick={onClick} />)
|
||||
|
||||
const clickable = screen.getByAltText(mockImageFile.name).closest('div') as HTMLElement
|
||||
|
||||
fireEvent.click(clickable)
|
||||
|
||||
expect(onClick).toHaveBeenCalledTimes(1)
|
||||
expect(onClick).toHaveBeenCalledWith(mockImageFile)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
import { render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { vi } from 'vitest'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import LinkedAppsPanel from './index'
|
||||
|
||||
vi.mock('next/link', () => ({
|
||||
default: ({ children, href, className }: { children: React.ReactNode, href: string, className: string }) => (
|
||||
<a href={href} className={className} data-testid="link-item">
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
}))
|
||||
|
||||
describe('LinkedAppsPanel Component', () => {
|
||||
const mockRelatedApps = [
|
||||
{
|
||||
id: 'app-1',
|
||||
name: 'Chatbot App',
|
||||
mode: AppModeEnum.CHAT,
|
||||
icon_type: 'emoji' as const,
|
||||
icon: '🤖',
|
||||
icon_background: '#FFEAD5',
|
||||
icon_url: '',
|
||||
},
|
||||
{
|
||||
id: 'app-2',
|
||||
name: 'Workflow App',
|
||||
mode: AppModeEnum.WORKFLOW,
|
||||
icon_type: 'image' as const,
|
||||
icon: 'file-id',
|
||||
icon_background: '#E4FBCC',
|
||||
icon_url: 'https://example.com/icon.png',
|
||||
},
|
||||
{
|
||||
id: 'app-3',
|
||||
name: '',
|
||||
mode: AppModeEnum.AGENT_CHAT,
|
||||
icon_type: 'emoji' as const,
|
||||
icon: '🕵️',
|
||||
icon_background: '#D3F8DF',
|
||||
icon_url: '',
|
||||
},
|
||||
]
|
||||
|
||||
describe('Render', () => {
|
||||
it('renders correctly with multiple apps', () => {
|
||||
render(<LinkedAppsPanel relatedApps={mockRelatedApps} isMobile={false} />)
|
||||
|
||||
const items = screen.getAllByTestId('link-item')
|
||||
expect(items).toHaveLength(3)
|
||||
|
||||
expect(screen.getByText('Chatbot App')).toBeInTheDocument()
|
||||
expect(screen.getByText('Workflow App')).toBeInTheDocument()
|
||||
expect(screen.getByText('--')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('displays correct app mode labels', () => {
|
||||
render(<LinkedAppsPanel relatedApps={mockRelatedApps} isMobile={false} />)
|
||||
|
||||
expect(screen.getByText('Chatbot')).toBeInTheDocument()
|
||||
expect(screen.getByText('Workflow')).toBeInTheDocument()
|
||||
expect(screen.getByText('Agent')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('hides app name and centers content in mobile mode', () => {
|
||||
render(<LinkedAppsPanel relatedApps={mockRelatedApps} isMobile={true} />)
|
||||
|
||||
expect(screen.queryByText('Chatbot App')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('Workflow App')).not.toBeInTheDocument()
|
||||
|
||||
const items = screen.getAllByTestId('link-item')
|
||||
expect(items[0]).toHaveClass('justify-center')
|
||||
})
|
||||
|
||||
it('handles empty relatedApps list gracefully', () => {
|
||||
const { container } = render(<LinkedAppsPanel relatedApps={[]} isMobile={false} />)
|
||||
const items = screen.queryAllByTestId('link-item')
|
||||
expect(items).toHaveLength(0)
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Interaction', () => {
|
||||
it('renders correct links for each app', () => {
|
||||
render(<LinkedAppsPanel relatedApps={mockRelatedApps} isMobile={false} />)
|
||||
|
||||
const items = screen.getAllByTestId('link-item')
|
||||
expect(items[0]).toHaveAttribute('href', '/app/app-1/overview')
|
||||
expect(items[1]).toHaveAttribute('href', '/app/app-2/overview')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import { render } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import HorizontalLine from './horizontal-line'
|
||||
|
||||
describe('HorizontalLine', () => {
|
||||
describe('Render', () => {
|
||||
it('renders correctly', () => {
|
||||
const { container } = render(<HorizontalLine />)
|
||||
const svg = container.querySelector('svg')
|
||||
expect(svg).toBeInTheDocument()
|
||||
expect(svg).toHaveAttribute('width', '240')
|
||||
expect(svg).toHaveAttribute('height', '2')
|
||||
})
|
||||
|
||||
it('renders linear gradient definition', () => {
|
||||
const { container } = render(<HorizontalLine />)
|
||||
const defs = container.querySelector('defs')
|
||||
const linearGradient = container.querySelector('linearGradient')
|
||||
expect(defs).toBeInTheDocument()
|
||||
expect(linearGradient).toBeInTheDocument()
|
||||
expect(linearGradient).toHaveAttribute('id', 'paint0_linear_8619_59125')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Style', () => {
|
||||
it('applies custom className', () => {
|
||||
const testClass = 'custom-test-class'
|
||||
const { container } = render(<HorizontalLine className={testClass} />)
|
||||
const svg = container.querySelector('svg')
|
||||
expect(svg).toHaveClass(testClass)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
import { render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import ListEmpty from './index'
|
||||
|
||||
describe('ListEmpty Component', () => {
|
||||
describe('Render', () => {
|
||||
it('renders default icon when no icon is provided', () => {
|
||||
const { container } = render(<ListEmpty />)
|
||||
expect(container.querySelector('[data-icon="Variable02"]')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders custom icon when provided', () => {
|
||||
const { container } = render(<ListEmpty icon={<div data-testid="custom-icon" />} />)
|
||||
expect(container.querySelector('[data-icon="Variable02"]')).not.toBeInTheDocument()
|
||||
expect(screen.getByTestId('custom-icon')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders design lines', () => {
|
||||
const { container } = render(<ListEmpty />)
|
||||
const svgs = container.querySelectorAll('svg')
|
||||
expect(svgs).toHaveLength(5)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Props', () => {
|
||||
it('renders title and description correctly', () => {
|
||||
const testTitle = 'Empty List'
|
||||
const testDescription = <span data-testid="desc">No items found</span>
|
||||
|
||||
render(<ListEmpty title={testTitle} description={testDescription} />)
|
||||
|
||||
expect(screen.getByText(testTitle)).toBeInTheDocument()
|
||||
expect(screen.getByTestId('desc')).toBeInTheDocument()
|
||||
expect(screen.getByText('No items found')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import { render } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import VerticalLine from './vertical-line'
|
||||
|
||||
describe('VerticalLine', () => {
|
||||
describe('Render', () => {
|
||||
it('renders correctly', () => {
|
||||
const { container } = render(<VerticalLine />)
|
||||
const svg = container.querySelector('svg')
|
||||
expect(svg).toBeInTheDocument()
|
||||
expect(svg).toHaveAttribute('width', '2')
|
||||
expect(svg).toHaveAttribute('height', '132')
|
||||
})
|
||||
|
||||
it('renders linear gradient definition', () => {
|
||||
const { container } = render(<VerticalLine />)
|
||||
const defs = container.querySelector('defs')
|
||||
const linearGradient = container.querySelector('linearGradient')
|
||||
expect(defs).toBeInTheDocument()
|
||||
expect(linearGradient).toBeInTheDocument()
|
||||
expect(linearGradient).toHaveAttribute('id', 'paint0_linear_8619_59128')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Style', () => {
|
||||
it('applies custom className', () => {
|
||||
const testClass = 'custom-test-class'
|
||||
const { container } = render(<VerticalLine className={testClass} />)
|
||||
const svg = container.querySelector('svg')
|
||||
expect(svg).toHaveClass(testClass)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
import { render, screen } from '@testing-library/react'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { Theme } from '@/types/app'
|
||||
import DifyLogo from './dify-logo'
|
||||
|
||||
vi.mock('@/hooks/use-theme', () => ({
|
||||
default: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/var', () => ({
|
||||
basePath: '/test-base-path',
|
||||
}))
|
||||
|
||||
describe('DifyLogo', () => {
|
||||
const mockUseTheme = {
|
||||
theme: Theme.light,
|
||||
themes: ['light', 'dark'],
|
||||
setTheme: vi.fn(),
|
||||
resolvedTheme: Theme.light,
|
||||
systemTheme: Theme.light,
|
||||
forcedTheme: undefined,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(useTheme).mockReturnValue(mockUseTheme as ReturnType<typeof useTheme>)
|
||||
})
|
||||
|
||||
describe('Render', () => {
|
||||
it('renders correctly with default props', () => {
|
||||
render(<DifyLogo />)
|
||||
const img = screen.getByRole('img', { name: /dify logo/i })
|
||||
expect(img).toBeInTheDocument()
|
||||
expect(img).toHaveAttribute('src', '/test-base-path/logo/logo.svg')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Props', () => {
|
||||
it('applies custom size correctly', () => {
|
||||
const { rerender } = render(<DifyLogo size="large" />)
|
||||
let img = screen.getByRole('img', { name: /dify logo/i })
|
||||
expect(img).toHaveClass('w-16')
|
||||
expect(img).toHaveClass('h-7')
|
||||
|
||||
rerender(<DifyLogo size="small" />)
|
||||
img = screen.getByRole('img', { name: /dify logo/i })
|
||||
expect(img).toHaveClass('w-9')
|
||||
expect(img).toHaveClass('h-4')
|
||||
})
|
||||
|
||||
it('applies custom style correctly', () => {
|
||||
render(<DifyLogo style="monochromeWhite" />)
|
||||
const img = screen.getByRole('img', { name: /dify logo/i })
|
||||
expect(img).toHaveAttribute('src', '/test-base-path/logo/logo-monochrome-white.svg')
|
||||
})
|
||||
|
||||
it('applies custom className', () => {
|
||||
render(<DifyLogo className="custom-test-class" />)
|
||||
const img = screen.getByRole('img', { name: /dify logo/i })
|
||||
expect(img).toHaveClass('custom-test-class')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Theme behavior', () => {
|
||||
it('uses monochromeWhite logo in dark theme when style is default', () => {
|
||||
vi.mocked(useTheme).mockReturnValue({
|
||||
...mockUseTheme,
|
||||
theme: Theme.dark,
|
||||
} as ReturnType<typeof useTheme>)
|
||||
render(<DifyLogo style="default" />)
|
||||
const img = screen.getByRole('img', { name: /dify logo/i })
|
||||
expect(img).toHaveAttribute('src', '/test-base-path/logo/logo-monochrome-white.svg')
|
||||
})
|
||||
|
||||
it('uses monochromeWhite logo in dark theme when style is monochromeWhite', () => {
|
||||
vi.mocked(useTheme).mockReturnValue({
|
||||
...mockUseTheme,
|
||||
theme: Theme.dark,
|
||||
} as ReturnType<typeof useTheme>)
|
||||
render(<DifyLogo style="monochromeWhite" />)
|
||||
const img = screen.getByRole('img', { name: /dify logo/i })
|
||||
expect(img).toHaveAttribute('src', '/test-base-path/logo/logo-monochrome-white.svg')
|
||||
})
|
||||
|
||||
it('uses default logo in light theme when style is default', () => {
|
||||
vi.mocked(useTheme).mockReturnValue({
|
||||
...mockUseTheme,
|
||||
theme: Theme.light,
|
||||
} as ReturnType<typeof useTheme>)
|
||||
render(<DifyLogo style="default" />)
|
||||
const img = screen.getByRole('img', { name: /dify logo/i })
|
||||
expect(img).toHaveAttribute('src', '/test-base-path/logo/logo.svg')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import { render, screen } from '@testing-library/react'
|
||||
import LogoEmbeddedChatAvatar from './logo-embedded-chat-avatar'
|
||||
|
||||
vi.mock('@/utils/var', () => ({
|
||||
basePath: '/test-base-path',
|
||||
}))
|
||||
|
||||
describe('LogoEmbeddedChatAvatar', () => {
|
||||
describe('Render', () => {
|
||||
it('renders correctly with default props', () => {
|
||||
render(<LogoEmbeddedChatAvatar />)
|
||||
const img = screen.getByRole('img', { name: /logo/i })
|
||||
expect(img).toBeInTheDocument()
|
||||
expect(img).toHaveAttribute('src', '/test-base-path/logo/logo-embedded-chat-avatar.png')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Props', () => {
|
||||
it('applies custom className correctly', () => {
|
||||
const customClass = 'custom-avatar-class'
|
||||
render(<LogoEmbeddedChatAvatar className={customClass} />)
|
||||
const img = screen.getByRole('img', { name: /logo/i })
|
||||
expect(img).toHaveClass(customClass)
|
||||
})
|
||||
|
||||
it('has valid alt text', () => {
|
||||
render(<LogoEmbeddedChatAvatar />)
|
||||
const img = screen.getByRole('img', { name: /logo/i })
|
||||
expect(img).toHaveAttribute('alt', 'logo')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import { render, screen } from '@testing-library/react'
|
||||
import LogoEmbeddedChatHeader from './logo-embedded-chat-header'
|
||||
|
||||
vi.mock('@/utils/var', () => ({
|
||||
basePath: '/test-base-path',
|
||||
}))
|
||||
|
||||
describe('LogoEmbeddedChatHeader', () => {
|
||||
it('renders correctly with default props', () => {
|
||||
const { container } = render(<LogoEmbeddedChatHeader />)
|
||||
const img = screen.getByRole('img', { name: /logo/i })
|
||||
expect(img).toBeInTheDocument()
|
||||
expect(img).toHaveAttribute('src', '/test-base-path/logo/logo-embedded-chat-header.png')
|
||||
|
||||
const sources = container.querySelectorAll('source')
|
||||
expect(sources).toHaveLength(3)
|
||||
expect(sources[0]).toHaveAttribute('srcSet', '/logo/logo-embedded-chat-header.png')
|
||||
expect(sources[1]).toHaveAttribute('srcSet', '/logo/logo-embedded-chat-header@2x.png')
|
||||
expect(sources[2]).toHaveAttribute('srcSet', '/logo/logo-embedded-chat-header@3x.png')
|
||||
})
|
||||
|
||||
it('applies custom className correctly', () => {
|
||||
const customClass = 'custom-header-class'
|
||||
render(<LogoEmbeddedChatHeader className={customClass} />)
|
||||
const img = screen.getByRole('img', { name: /logo/i })
|
||||
expect(img).toHaveClass(customClass)
|
||||
expect(img).toHaveClass('h-6')
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import { render, screen } from '@testing-library/react'
|
||||
import LogoSite from './logo-site'
|
||||
|
||||
vi.mock('@/utils/var', () => ({
|
||||
basePath: '/test-base-path',
|
||||
}))
|
||||
|
||||
describe('LogoSite', () => {
|
||||
it('renders correctly with default props', () => {
|
||||
render(<LogoSite />)
|
||||
const img = screen.getByRole('img', { name: /logo/i })
|
||||
expect(img).toBeInTheDocument()
|
||||
expect(img).toHaveAttribute('src', '/test-base-path/logo/logo.png')
|
||||
})
|
||||
|
||||
it('applies custom className correctly', () => {
|
||||
const customClass = 'custom-site-class'
|
||||
render(<LogoSite className={customClass} />)
|
||||
const img = screen.getByRole('img', { name: /logo/i })
|
||||
expect(img).toHaveClass(customClass)
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import SearchInput from '.'
|
||||
|
||||
describe('SearchInput', () => {
|
||||
describe('Render', () => {
|
||||
it('renders correctly with default props', () => {
|
||||
render(<SearchInput value="" onChange={() => {}} />)
|
||||
const input = screen.getByPlaceholderText('common.operation.search')
|
||||
expect(input).toBeInTheDocument()
|
||||
expect(input).toHaveValue('')
|
||||
})
|
||||
|
||||
it('renders custom placeholder', () => {
|
||||
render(<SearchInput value="" onChange={() => {}} placeholder="Custom Placeholder" />)
|
||||
expect(screen.getByPlaceholderText('Custom Placeholder')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows clear button when value is present', () => {
|
||||
const onChange = vi.fn()
|
||||
render(<SearchInput value="has value" onChange={onChange} />)
|
||||
|
||||
const clearButton = screen.getByLabelText('common.operation.clear')
|
||||
expect(clearButton).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Interaction', () => {
|
||||
it('calls onChange when typing', () => {
|
||||
const onChange = vi.fn()
|
||||
render(<SearchInput value="" onChange={onChange} />)
|
||||
const input = screen.getByPlaceholderText('common.operation.search')
|
||||
|
||||
fireEvent.change(input, { target: { value: 'test' } })
|
||||
expect(onChange).toHaveBeenCalledWith('test')
|
||||
})
|
||||
|
||||
it('handles composition events', () => {
|
||||
const onChange = vi.fn()
|
||||
render(<SearchInput value="initial" onChange={onChange} />)
|
||||
const input = screen.getByPlaceholderText('common.operation.search')
|
||||
|
||||
// Start composition
|
||||
fireEvent.compositionStart(input)
|
||||
fireEvent.change(input, { target: { value: 'final' } })
|
||||
|
||||
// While composing, onChange should NOT be called
|
||||
expect(onChange).not.toHaveBeenCalled()
|
||||
expect(input).toHaveValue('final')
|
||||
|
||||
// End composition
|
||||
fireEvent.compositionEnd(input)
|
||||
expect(onChange).toHaveBeenCalledTimes(1)
|
||||
expect(onChange).toHaveBeenCalledWith('final')
|
||||
})
|
||||
|
||||
it('calls onChange with empty string when clear button is clicked', () => {
|
||||
const onChange = vi.fn()
|
||||
render(<SearchInput value="has value" onChange={onChange} />)
|
||||
|
||||
const clearButton = screen.getByLabelText('common.operation.clear')
|
||||
fireEvent.click(clearButton)
|
||||
expect(onChange).toHaveBeenCalledWith('')
|
||||
})
|
||||
|
||||
it('updates focus state on focus/blur', () => {
|
||||
const { container } = render(<SearchInput value="" onChange={() => {}} />)
|
||||
const wrapper = container.firstChild as HTMLElement
|
||||
const input = screen.getByPlaceholderText('common.operation.search')
|
||||
|
||||
fireEvent.focus(input)
|
||||
expect(wrapper).toHaveClass(/bg-components-input-bg-active/)
|
||||
|
||||
fireEvent.blur(input)
|
||||
expect(wrapper).not.toHaveClass(/bg-components-input-bg-active/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Style', () => {
|
||||
it('applies white style', () => {
|
||||
const { container } = render(<SearchInput value="" onChange={() => {}} white />)
|
||||
const wrapper = container.firstChild as HTMLElement
|
||||
expect(wrapper).toHaveClass('!bg-white')
|
||||
})
|
||||
|
||||
it('applies custom className', () => {
|
||||
const { container } = render(<SearchInput value="" onChange={() => {}} className="custom-test" />)
|
||||
const wrapper = container.firstChild as HTMLElement
|
||||
expect(wrapper).toHaveClass('custom-test')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -1724,11 +1724,6 @@
|
|||
"count": 10
|
||||
}
|
||||
},
|
||||
"app/components/base/checkbox-list/index.tsx": {
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 6
|
||||
}
|
||||
},
|
||||
"app/components/base/checkbox/index.stories.tsx": {
|
||||
"no-console": {
|
||||
"count": 1
|
||||
|
|
@ -1858,9 +1853,6 @@
|
|||
"app/components/base/emoji-picker/Inner.tsx": {
|
||||
"react-hooks-extra/no-direct-set-state-in-use-effect": {
|
||||
"count": 1
|
||||
},
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 3
|
||||
}
|
||||
},
|
||||
"app/components/base/encrypted-bottom/index.tsx": {
|
||||
|
|
|
|||
Loading…
Reference in New Issue