mirror of
https://github.com/langgenius/dify.git
synced 2026-01-07 23:04:12 +00:00
test: add comprehensive tests for ContentSection, hooks, and MetaSection components
This commit is contained in:
@@ -0,0 +1,388 @@
|
||||
import React from 'react'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import type { TFunction } from 'i18next'
|
||||
import ContentSection from './content-section'
|
||||
import type { WorkflowProcess } from '@/app/components/base/chat/types'
|
||||
import type { SiteInfo } from '@/models/share'
|
||||
|
||||
// ============================================================================
|
||||
// Mock Setup
|
||||
// ============================================================================
|
||||
|
||||
jest.mock('@/app/components/base/chat/chat/answer/workflow-process', () => ({
|
||||
__esModule: true,
|
||||
default: ({ data }: { data: WorkflowProcess }) => (
|
||||
<div data-testid="workflow-process">{data.resultText}</div>
|
||||
),
|
||||
}))
|
||||
|
||||
jest.mock('@/app/components/base/markdown', () => ({
|
||||
Markdown: ({ content }: { content: string }) => (
|
||||
<div data-testid="markdown">{content}</div>
|
||||
),
|
||||
}))
|
||||
|
||||
jest.mock('./result-tab', () => ({
|
||||
__esModule: true,
|
||||
default: ({ currentTab }: { currentTab: string }) => (
|
||||
<div data-testid="result-tab">{currentTab}</div>
|
||||
),
|
||||
}))
|
||||
|
||||
// ============================================================================
|
||||
// Test Utilities
|
||||
// ============================================================================
|
||||
|
||||
const mockT = ((key: string) => key) as TFunction
|
||||
|
||||
/**
|
||||
* Creates base props with sensible defaults for ContentSection testing.
|
||||
*/
|
||||
const createBaseProps = (overrides?: Partial<Parameters<typeof ContentSection>[0]>) => ({
|
||||
depth: 1,
|
||||
isError: false,
|
||||
content: 'test content',
|
||||
currentTab: 'DETAIL' as const,
|
||||
onSwitchTab: jest.fn(),
|
||||
showResultTabs: false,
|
||||
t: mockT,
|
||||
siteInfo: null,
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const createWorkflowData = (overrides?: Partial<WorkflowProcess>): WorkflowProcess => ({
|
||||
status: 'succeeded',
|
||||
tracing: [],
|
||||
expand: true,
|
||||
resultText: 'workflow result',
|
||||
...overrides,
|
||||
} as WorkflowProcess)
|
||||
|
||||
const createSiteInfo = (overrides?: Partial<SiteInfo>): SiteInfo => ({
|
||||
title: 'Test Site',
|
||||
icon: '',
|
||||
icon_background: '',
|
||||
description: '',
|
||||
default_language: 'en',
|
||||
prompt_public: false,
|
||||
copyright: '',
|
||||
privacy_policy: '',
|
||||
custom_disclaimer: '',
|
||||
show_workflow_steps: true,
|
||||
use_icon_as_answer_icon: false,
|
||||
...overrides,
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// Test Suite
|
||||
// ============================================================================
|
||||
|
||||
describe('ContentSection', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Basic Rendering Tests
|
||||
// Tests for basic component rendering scenarios
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Basic Rendering', () => {
|
||||
it('should render markdown content when no workflow data', () => {
|
||||
render(<ContentSection {...createBaseProps({ content: 'Hello World' })} />)
|
||||
expect(screen.getByTestId('markdown')).toHaveTextContent('Hello World')
|
||||
})
|
||||
|
||||
it('should not render markdown when content is not a string', () => {
|
||||
render(<ContentSection {...createBaseProps({ content: { data: 'object' } })} />)
|
||||
expect(screen.queryByTestId('markdown')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should apply rounded styling when not in side panel', () => {
|
||||
const { container } = render(<ContentSection {...createBaseProps()} />)
|
||||
expect(container.firstChild).toHaveClass('rounded-2xl')
|
||||
})
|
||||
|
||||
it('should not apply rounded styling when in side panel', () => {
|
||||
const { container } = render(<ContentSection {...createBaseProps({ inSidePanel: true })} />)
|
||||
expect(container.firstChild).not.toHaveClass('rounded-2xl')
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Task ID Display Tests
|
||||
// Tests for task ID rendering in different contexts
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Task ID Display', () => {
|
||||
it('should show task ID without depth suffix at depth 1', () => {
|
||||
render(
|
||||
<ContentSection
|
||||
{...createBaseProps({
|
||||
taskId: 'task-123',
|
||||
depth: 1,
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByText('task-123')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show task ID with depth suffix at depth > 1', () => {
|
||||
render(
|
||||
<ContentSection
|
||||
{...createBaseProps({
|
||||
taskId: 'task-123',
|
||||
depth: 3,
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByText('task-123-2')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show execution label with task ID', () => {
|
||||
render(
|
||||
<ContentSection
|
||||
{...createBaseProps({
|
||||
taskId: 'task-abc',
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByText('share.generation.execution')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Error State Tests
|
||||
// Tests for error state display
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Error State', () => {
|
||||
it('should show error message when isError is true', () => {
|
||||
render(<ContentSection {...createBaseProps({ isError: true })} />)
|
||||
expect(screen.getByText('share.generation.batchFailed.outputPlaceholder')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not show markdown content when isError is true', () => {
|
||||
render(<ContentSection {...createBaseProps({ isError: true, content: 'should not show' })} />)
|
||||
expect(screen.queryByTestId('markdown')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should apply error styling to task ID when isError is true', () => {
|
||||
render(
|
||||
<ContentSection
|
||||
{...createBaseProps({
|
||||
taskId: 'error-task',
|
||||
isError: true,
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
const executionText = screen.getByText('share.generation.execution').parentElement
|
||||
expect(executionText).toHaveClass('text-text-destructive')
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Workflow Process Tests
|
||||
// Tests for workflow-specific rendering
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Workflow Process', () => {
|
||||
it('should render WorkflowProcessItem when workflowProcessData and siteInfo are present', () => {
|
||||
render(
|
||||
<ContentSection
|
||||
{...createBaseProps({
|
||||
workflowProcessData: createWorkflowData(),
|
||||
siteInfo: createSiteInfo(),
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByTestId('workflow-process')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render WorkflowProcessItem when siteInfo is null', () => {
|
||||
render(
|
||||
<ContentSection
|
||||
{...createBaseProps({
|
||||
workflowProcessData: createWorkflowData(),
|
||||
siteInfo: null,
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
expect(screen.queryByTestId('workflow-process')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show task ID within workflow section', () => {
|
||||
render(
|
||||
<ContentSection
|
||||
{...createBaseProps({
|
||||
workflowProcessData: createWorkflowData(),
|
||||
taskId: 'wf-task-123',
|
||||
siteInfo: createSiteInfo(),
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByText('wf-task-123')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render ResultTab when isError is true', () => {
|
||||
render(
|
||||
<ContentSection
|
||||
{...createBaseProps({
|
||||
workflowProcessData: createWorkflowData(),
|
||||
siteInfo: createSiteInfo(),
|
||||
isError: true,
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
expect(screen.queryByTestId('result-tab')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render ResultTab when not error', () => {
|
||||
render(
|
||||
<ContentSection
|
||||
{...createBaseProps({
|
||||
workflowProcessData: createWorkflowData(),
|
||||
siteInfo: createSiteInfo(),
|
||||
isError: false,
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByTestId('result-tab')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Result Tabs Tests
|
||||
// Tests for the result/detail tab switching
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Result Tabs', () => {
|
||||
it('should show tabs when showResultTabs is true', () => {
|
||||
render(
|
||||
<ContentSection
|
||||
{...createBaseProps({
|
||||
workflowProcessData: createWorkflowData(),
|
||||
siteInfo: createSiteInfo(),
|
||||
showResultTabs: true,
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByText('runLog.result')).toBeInTheDocument()
|
||||
expect(screen.getByText('runLog.detail')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not show tabs when showResultTabs is false', () => {
|
||||
render(
|
||||
<ContentSection
|
||||
{...createBaseProps({
|
||||
workflowProcessData: createWorkflowData(),
|
||||
siteInfo: createSiteInfo(),
|
||||
showResultTabs: false,
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
expect(screen.queryByText('runLog.result')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('runLog.detail')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call onSwitchTab when RESULT tab is clicked', () => {
|
||||
const onSwitchTab = jest.fn()
|
||||
render(
|
||||
<ContentSection
|
||||
{...createBaseProps({
|
||||
workflowProcessData: createWorkflowData(),
|
||||
siteInfo: createSiteInfo(),
|
||||
showResultTabs: true,
|
||||
onSwitchTab,
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByText('runLog.result'))
|
||||
expect(onSwitchTab).toHaveBeenCalledWith('RESULT')
|
||||
})
|
||||
|
||||
it('should call onSwitchTab when DETAIL tab is clicked', () => {
|
||||
const onSwitchTab = jest.fn()
|
||||
render(
|
||||
<ContentSection
|
||||
{...createBaseProps({
|
||||
workflowProcessData: createWorkflowData(),
|
||||
siteInfo: createSiteInfo(),
|
||||
showResultTabs: true,
|
||||
onSwitchTab,
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByText('runLog.detail'))
|
||||
expect(onSwitchTab).toHaveBeenCalledWith('DETAIL')
|
||||
})
|
||||
|
||||
it('should highlight active tab', () => {
|
||||
render(
|
||||
<ContentSection
|
||||
{...createBaseProps({
|
||||
workflowProcessData: createWorkflowData(),
|
||||
siteInfo: createSiteInfo(),
|
||||
showResultTabs: true,
|
||||
currentTab: 'RESULT',
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
|
||||
const resultTab = screen.getByText('runLog.result')
|
||||
expect(resultTab).toHaveClass('text-text-primary')
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Boundary Conditions Tests
|
||||
// Tests for edge cases and boundary values
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Boundary Conditions', () => {
|
||||
it('should handle empty string content', () => {
|
||||
render(<ContentSection {...createBaseProps({ content: '' })} />)
|
||||
expect(screen.getByTestId('markdown')).toHaveTextContent('')
|
||||
})
|
||||
|
||||
it('should handle null siteInfo', () => {
|
||||
render(
|
||||
<ContentSection
|
||||
{...createBaseProps({
|
||||
workflowProcessData: createWorkflowData(),
|
||||
siteInfo: null,
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
// Should not crash and WorkflowProcessItem should not render
|
||||
expect(screen.queryByTestId('workflow-process')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle undefined workflowProcessData', () => {
|
||||
render(<ContentSection {...createBaseProps({ workflowProcessData: undefined })} />)
|
||||
expect(screen.queryByTestId('workflow-process')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle show_workflow_steps false in siteInfo', () => {
|
||||
render(
|
||||
<ContentSection
|
||||
{...createBaseProps({
|
||||
workflowProcessData: createWorkflowData(),
|
||||
siteInfo: createSiteInfo({ show_workflow_steps: false }),
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByTestId('workflow-process')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle hideProcessDetail prop', () => {
|
||||
render(
|
||||
<ContentSection
|
||||
{...createBaseProps({
|
||||
workflowProcessData: createWorkflowData(),
|
||||
siteInfo: createSiteInfo(),
|
||||
hideProcessDetail: true,
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
// Component should still render
|
||||
expect(screen.getByTestId('workflow-process')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
411
web/app/components/app/text-generate/item/hooks.spec.ts
Normal file
411
web/app/components/app/text-generate/item/hooks.spec.ts
Normal file
@@ -0,0 +1,411 @@
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { useMoreLikeThisState, useWorkflowTabs } from './hooks'
|
||||
import type { WorkflowProcess } from '@/app/components/base/chat/types'
|
||||
|
||||
// ============================================================================
|
||||
// useMoreLikeThisState Tests
|
||||
// ============================================================================
|
||||
describe('useMoreLikeThisState', () => {
|
||||
// --------------------------------------------------------------------------
|
||||
// Initial State Tests
|
||||
// Tests verifying the hook's initial state values
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Initial State', () => {
|
||||
it('should initialize with empty completionRes', () => {
|
||||
const { result } = renderHook(() => useMoreLikeThisState({}))
|
||||
expect(result.current.completionRes).toBe('')
|
||||
})
|
||||
|
||||
it('should initialize with null childMessageId', () => {
|
||||
const { result } = renderHook(() => useMoreLikeThisState({}))
|
||||
expect(result.current.childMessageId).toBeNull()
|
||||
})
|
||||
|
||||
it('should initialize with null rating in childFeedback', () => {
|
||||
const { result } = renderHook(() => useMoreLikeThisState({}))
|
||||
expect(result.current.childFeedback).toEqual({ rating: null })
|
||||
})
|
||||
|
||||
it('should initialize isQuerying as false', () => {
|
||||
const { result } = renderHook(() => useMoreLikeThisState({}))
|
||||
expect(result.current.isQuerying).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// State Setter Tests
|
||||
// Tests for state update functions
|
||||
// --------------------------------------------------------------------------
|
||||
describe('State Setters', () => {
|
||||
it('should update completionRes when setCompletionRes is called', () => {
|
||||
const { result } = renderHook(() => useMoreLikeThisState({}))
|
||||
|
||||
act(() => {
|
||||
result.current.setCompletionRes('new response')
|
||||
})
|
||||
|
||||
expect(result.current.completionRes).toBe('new response')
|
||||
})
|
||||
|
||||
it('should update childMessageId when setChildMessageId is called', () => {
|
||||
const { result } = renderHook(() => useMoreLikeThisState({}))
|
||||
|
||||
act(() => {
|
||||
result.current.setChildMessageId('child-123')
|
||||
})
|
||||
|
||||
expect(result.current.childMessageId).toBe('child-123')
|
||||
})
|
||||
|
||||
it('should update childFeedback when setChildFeedback is called', () => {
|
||||
const { result } = renderHook(() => useMoreLikeThisState({}))
|
||||
|
||||
act(() => {
|
||||
result.current.setChildFeedback({ rating: 'like' })
|
||||
})
|
||||
|
||||
expect(result.current.childFeedback).toEqual({ rating: 'like' })
|
||||
})
|
||||
|
||||
it('should set isQuerying to true when startQuerying is called', () => {
|
||||
const { result } = renderHook(() => useMoreLikeThisState({}))
|
||||
|
||||
act(() => {
|
||||
result.current.startQuerying()
|
||||
})
|
||||
|
||||
expect(result.current.isQuerying).toBe(true)
|
||||
})
|
||||
|
||||
it('should set isQuerying to false when stopQuerying is called', () => {
|
||||
const { result } = renderHook(() => useMoreLikeThisState({}))
|
||||
|
||||
act(() => {
|
||||
result.current.startQuerying()
|
||||
})
|
||||
expect(result.current.isQuerying).toBe(true)
|
||||
|
||||
act(() => {
|
||||
result.current.stopQuerying()
|
||||
})
|
||||
expect(result.current.isQuerying).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// controlClearMoreLikeThis Effect Tests
|
||||
// Tests for the clear effect triggered by controlClearMoreLikeThis
|
||||
// --------------------------------------------------------------------------
|
||||
describe('controlClearMoreLikeThis Effect', () => {
|
||||
it('should clear childMessageId when controlClearMoreLikeThis changes to truthy value', () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ controlClearMoreLikeThis }: { controlClearMoreLikeThis?: number }) =>
|
||||
useMoreLikeThisState({ controlClearMoreLikeThis }),
|
||||
{ initialProps: { controlClearMoreLikeThis: undefined as number | undefined } },
|
||||
)
|
||||
|
||||
act(() => {
|
||||
result.current.setChildMessageId('child-to-clear')
|
||||
result.current.setCompletionRes('response-to-clear')
|
||||
})
|
||||
|
||||
expect(result.current.childMessageId).toBe('child-to-clear')
|
||||
expect(result.current.completionRes).toBe('response-to-clear')
|
||||
|
||||
rerender({ controlClearMoreLikeThis: 1 })
|
||||
|
||||
expect(result.current.childMessageId).toBeNull()
|
||||
expect(result.current.completionRes).toBe('')
|
||||
})
|
||||
|
||||
it('should not clear state when controlClearMoreLikeThis is 0', () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ controlClearMoreLikeThis }: { controlClearMoreLikeThis?: number }) =>
|
||||
useMoreLikeThisState({ controlClearMoreLikeThis }),
|
||||
{ initialProps: { controlClearMoreLikeThis: undefined as number | undefined } },
|
||||
)
|
||||
|
||||
act(() => {
|
||||
result.current.setChildMessageId('keep-this')
|
||||
result.current.setCompletionRes('keep-response')
|
||||
})
|
||||
|
||||
rerender({ controlClearMoreLikeThis: 0 })
|
||||
|
||||
expect(result.current.childMessageId).toBe('keep-this')
|
||||
expect(result.current.completionRes).toBe('keep-response')
|
||||
})
|
||||
|
||||
it('should clear state when controlClearMoreLikeThis increments', () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ controlClearMoreLikeThis }) => useMoreLikeThisState({ controlClearMoreLikeThis }),
|
||||
{ initialProps: { controlClearMoreLikeThis: 1 } },
|
||||
)
|
||||
|
||||
act(() => {
|
||||
result.current.setChildMessageId('will-clear')
|
||||
})
|
||||
|
||||
rerender({ controlClearMoreLikeThis: 2 })
|
||||
|
||||
expect(result.current.childMessageId).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// isLoading Effect Tests
|
||||
// Tests for the effect triggered by isLoading changes
|
||||
// --------------------------------------------------------------------------
|
||||
describe('isLoading Effect', () => {
|
||||
it('should clear childMessageId when isLoading becomes true', () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ isLoading }) => useMoreLikeThisState({ isLoading }),
|
||||
{ initialProps: { isLoading: false } },
|
||||
)
|
||||
|
||||
act(() => {
|
||||
result.current.setChildMessageId('child-during-load')
|
||||
})
|
||||
|
||||
expect(result.current.childMessageId).toBe('child-during-load')
|
||||
|
||||
rerender({ isLoading: true })
|
||||
|
||||
expect(result.current.childMessageId).toBeNull()
|
||||
})
|
||||
|
||||
it('should not clear childMessageId when isLoading is false', () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ isLoading }) => useMoreLikeThisState({ isLoading }),
|
||||
{ initialProps: { isLoading: true } },
|
||||
)
|
||||
|
||||
act(() => {
|
||||
result.current.setChildMessageId('keep-child')
|
||||
})
|
||||
|
||||
rerender({ isLoading: false })
|
||||
|
||||
// childMessageId was already cleared when isLoading was true initially
|
||||
// Set it again after isLoading is false
|
||||
act(() => {
|
||||
result.current.setChildMessageId('keep-child-2')
|
||||
})
|
||||
|
||||
expect(result.current.childMessageId).toBe('keep-child-2')
|
||||
})
|
||||
|
||||
it('should not affect completionRes when isLoading changes', () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ isLoading }) => useMoreLikeThisState({ isLoading }),
|
||||
{ initialProps: { isLoading: false } },
|
||||
)
|
||||
|
||||
act(() => {
|
||||
result.current.setCompletionRes('my response')
|
||||
})
|
||||
|
||||
rerender({ isLoading: true })
|
||||
|
||||
expect(result.current.completionRes).toBe('my response')
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Boundary Conditions Tests
|
||||
// Tests for edge cases and boundary values
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Boundary Conditions', () => {
|
||||
it('should handle undefined parameters', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useMoreLikeThisState({ controlClearMoreLikeThis: undefined, isLoading: undefined }),
|
||||
)
|
||||
|
||||
expect(result.current.childMessageId).toBeNull()
|
||||
expect(result.current.completionRes).toBe('')
|
||||
})
|
||||
|
||||
it('should handle empty string for completionRes', () => {
|
||||
const { result } = renderHook(() => useMoreLikeThisState({}))
|
||||
|
||||
act(() => {
|
||||
result.current.setCompletionRes('')
|
||||
})
|
||||
|
||||
expect(result.current.completionRes).toBe('')
|
||||
})
|
||||
|
||||
it('should handle multiple rapid state changes', () => {
|
||||
const { result } = renderHook(() => useMoreLikeThisState({}))
|
||||
|
||||
act(() => {
|
||||
result.current.setChildMessageId('first')
|
||||
result.current.setChildMessageId('second')
|
||||
result.current.setChildMessageId('third')
|
||||
})
|
||||
|
||||
expect(result.current.childMessageId).toBe('third')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// useWorkflowTabs Tests
|
||||
// ============================================================================
|
||||
describe('useWorkflowTabs', () => {
|
||||
// --------------------------------------------------------------------------
|
||||
// Initial State Tests
|
||||
// Tests verifying the hook's initial state based on workflowProcessData
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Initial State', () => {
|
||||
it('should initialize currentTab to DETAIL when no workflowProcessData', () => {
|
||||
const { result } = renderHook(() => useWorkflowTabs(undefined))
|
||||
expect(result.current.currentTab).toBe('DETAIL')
|
||||
})
|
||||
|
||||
it('should initialize showResultTabs to false when no workflowProcessData', () => {
|
||||
const { result } = renderHook(() => useWorkflowTabs(undefined))
|
||||
expect(result.current.showResultTabs).toBe(false)
|
||||
})
|
||||
|
||||
it('should set currentTab to RESULT when resultText is present', () => {
|
||||
const workflowData = { resultText: 'some result' } as WorkflowProcess
|
||||
const { result } = renderHook(() => useWorkflowTabs(workflowData))
|
||||
expect(result.current.currentTab).toBe('RESULT')
|
||||
})
|
||||
|
||||
it('should set currentTab to RESULT when files array has items', () => {
|
||||
const workflowData = { files: [{ id: 'file-1' }] } as WorkflowProcess
|
||||
const { result } = renderHook(() => useWorkflowTabs(workflowData))
|
||||
expect(result.current.currentTab).toBe('RESULT')
|
||||
})
|
||||
|
||||
it('should set showResultTabs to true when resultText is present', () => {
|
||||
const workflowData = { resultText: 'result' } as WorkflowProcess
|
||||
const { result } = renderHook(() => useWorkflowTabs(workflowData))
|
||||
expect(result.current.showResultTabs).toBe(true)
|
||||
})
|
||||
|
||||
it('should set showResultTabs to true when files array has items', () => {
|
||||
const workflowData = { files: [{ id: 'file-1' }] } as WorkflowProcess
|
||||
const { result } = renderHook(() => useWorkflowTabs(workflowData))
|
||||
expect(result.current.showResultTabs).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Tab Switching Tests
|
||||
// Tests for manual tab switching functionality
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Tab Switching', () => {
|
||||
it('should allow switching to DETAIL tab', () => {
|
||||
const workflowData = { resultText: 'result' } as WorkflowProcess
|
||||
const { result } = renderHook(() => useWorkflowTabs(workflowData))
|
||||
|
||||
expect(result.current.currentTab).toBe('RESULT')
|
||||
|
||||
act(() => {
|
||||
result.current.setCurrentTab('DETAIL')
|
||||
})
|
||||
|
||||
expect(result.current.currentTab).toBe('DETAIL')
|
||||
})
|
||||
|
||||
it('should allow switching to RESULT tab', () => {
|
||||
const { result } = renderHook(() => useWorkflowTabs(undefined))
|
||||
|
||||
act(() => {
|
||||
result.current.setCurrentTab('RESULT')
|
||||
})
|
||||
|
||||
expect(result.current.currentTab).toBe('RESULT')
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Dynamic Data Change Tests
|
||||
// Tests for behavior when workflowProcessData changes
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Dynamic Data Changes', () => {
|
||||
it('should update currentTab when resultText becomes available', () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ data }) => useWorkflowTabs(data),
|
||||
{ initialProps: { data: undefined as WorkflowProcess | undefined } },
|
||||
)
|
||||
|
||||
expect(result.current.currentTab).toBe('DETAIL')
|
||||
|
||||
rerender({ data: { resultText: 'new result' } as WorkflowProcess })
|
||||
|
||||
expect(result.current.currentTab).toBe('RESULT')
|
||||
})
|
||||
|
||||
it('should update currentTab when resultText is removed', () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ data }) => useWorkflowTabs(data),
|
||||
{ initialProps: { data: { resultText: 'result' } as WorkflowProcess } },
|
||||
)
|
||||
|
||||
expect(result.current.currentTab).toBe('RESULT')
|
||||
|
||||
rerender({ data: { resultText: '' } as WorkflowProcess })
|
||||
|
||||
expect(result.current.currentTab).toBe('DETAIL')
|
||||
})
|
||||
|
||||
it('should update showResultTabs when files array changes', () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ data }) => useWorkflowTabs(data),
|
||||
{ initialProps: { data: { files: [] } as unknown as WorkflowProcess } },
|
||||
)
|
||||
|
||||
expect(result.current.showResultTabs).toBe(false)
|
||||
|
||||
rerender({ data: { files: [{ id: 'file-1' }] } as WorkflowProcess })
|
||||
|
||||
expect(result.current.showResultTabs).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Boundary Conditions Tests
|
||||
// Tests for edge cases
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Boundary Conditions', () => {
|
||||
it('should handle empty resultText', () => {
|
||||
const workflowData = { resultText: '' } as WorkflowProcess
|
||||
const { result } = renderHook(() => useWorkflowTabs(workflowData))
|
||||
expect(result.current.showResultTabs).toBe(false)
|
||||
expect(result.current.currentTab).toBe('DETAIL')
|
||||
})
|
||||
|
||||
it('should handle empty files array', () => {
|
||||
const workflowData = { files: [] } as unknown as WorkflowProcess
|
||||
const { result } = renderHook(() => useWorkflowTabs(workflowData))
|
||||
expect(result.current.showResultTabs).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle undefined files', () => {
|
||||
const workflowData = { files: undefined } as WorkflowProcess
|
||||
const { result } = renderHook(() => useWorkflowTabs(workflowData))
|
||||
expect(result.current.showResultTabs).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle both resultText and files present', () => {
|
||||
const workflowData = {
|
||||
resultText: 'result',
|
||||
files: [{ id: 'file-1' }],
|
||||
} as WorkflowProcess
|
||||
const { result } = renderHook(() => useWorkflowTabs(workflowData))
|
||||
expect(result.current.showResultTabs).toBe(true)
|
||||
expect(result.current.currentTab).toBe('RESULT')
|
||||
})
|
||||
|
||||
it('should handle whitespace-only resultText as truthy', () => {
|
||||
const workflowData = { resultText: ' ' } as WorkflowProcess
|
||||
const { result } = renderHook(() => useWorkflowTabs(workflowData))
|
||||
// whitespace string is truthy
|
||||
expect(result.current.showResultTabs).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
720
web/app/components/app/text-generate/item/meta-section.spec.tsx
Normal file
720
web/app/components/app/text-generate/item/meta-section.spec.tsx
Normal file
@@ -0,0 +1,720 @@
|
||||
import React from 'react'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import type { TFunction } from 'i18next'
|
||||
import MetaSection from './meta-section'
|
||||
|
||||
// ============================================================================
|
||||
// Mock Setup
|
||||
// ============================================================================
|
||||
|
||||
jest.mock('@/app/components/base/action-button', () => ({
|
||||
__esModule: true,
|
||||
default: ({
|
||||
children,
|
||||
disabled,
|
||||
onClick,
|
||||
state,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
disabled?: boolean
|
||||
onClick?: () => void
|
||||
state?: string
|
||||
}) => (
|
||||
<button
|
||||
data-disabled={disabled}
|
||||
data-state={state}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
ActionButtonState: {
|
||||
Default: 'default',
|
||||
Active: 'active',
|
||||
Disabled: 'disabled',
|
||||
Destructive: 'destructive',
|
||||
},
|
||||
}))
|
||||
|
||||
jest.mock('@/app/components/base/new-audio-button', () => ({
|
||||
__esModule: true,
|
||||
default: ({ id, voice }: { id: string; voice?: string }) => (
|
||||
<button data-testid="audio-button" data-id={id} data-voice={voice}>
|
||||
Audio
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
|
||||
// ============================================================================
|
||||
// Test Utilities
|
||||
// ============================================================================
|
||||
|
||||
const mockT = ((key: string) => key) as TFunction
|
||||
|
||||
/**
|
||||
* Creates base props with sensible defaults for MetaSection testing.
|
||||
*/
|
||||
const createBaseProps = (overrides?: Partial<Parameters<typeof MetaSection>[0]>) => ({
|
||||
showCharCount: false,
|
||||
t: mockT,
|
||||
shouldIndentForChild: false,
|
||||
isInWebApp: false,
|
||||
isInstalledApp: false,
|
||||
isResponding: false,
|
||||
isError: false,
|
||||
messageId: 'msg-123',
|
||||
onOpenLogModal: jest.fn(),
|
||||
moreLikeThis: false,
|
||||
onMoreLikeThis: jest.fn(),
|
||||
disableMoreLikeThis: false,
|
||||
isShowTextToSpeech: false,
|
||||
canCopy: true,
|
||||
onCopy: jest.fn(),
|
||||
onRetry: jest.fn(),
|
||||
isWorkflow: false,
|
||||
...overrides,
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// Test Suite
|
||||
// ============================================================================
|
||||
|
||||
describe('MetaSection', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Character Count Tests
|
||||
// Tests for character count display
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Character Count', () => {
|
||||
it('should show character count when showCharCount is true', () => {
|
||||
render(<MetaSection {...createBaseProps({ showCharCount: true, charCount: 150 })} />)
|
||||
expect(screen.getByText(/150/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/common.unit.char/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not show character count when showCharCount is false', () => {
|
||||
render(<MetaSection {...createBaseProps({ showCharCount: false, charCount: 150 })} />)
|
||||
expect(screen.queryByText(/150/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle zero character count', () => {
|
||||
render(<MetaSection {...createBaseProps({ showCharCount: true, charCount: 0 })} />)
|
||||
expect(screen.getByText(/0/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle undefined character count', () => {
|
||||
render(<MetaSection {...createBaseProps({ showCharCount: true, charCount: undefined })} />)
|
||||
expect(screen.getByText(/common.unit.char/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Log Modal Button Tests
|
||||
// Tests for the log modal button visibility and behavior
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Log Modal Button', () => {
|
||||
it('should show log button when not in web app, not installed app, and not responding', () => {
|
||||
render(
|
||||
<MetaSection
|
||||
{...createBaseProps({
|
||||
isInWebApp: false,
|
||||
isInstalledApp: false,
|
||||
isResponding: false,
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
// Find the button with RiFileList3Line icon (log button)
|
||||
const buttons = screen.getAllByRole('button')
|
||||
const logButton = buttons.find(btn => !btn.hasAttribute('data-testid'))
|
||||
expect(logButton).toBeDefined()
|
||||
})
|
||||
|
||||
it('should not show log button when isInWebApp is true', () => {
|
||||
const onOpenLogModal = jest.fn()
|
||||
render(
|
||||
<MetaSection
|
||||
{...createBaseProps({
|
||||
isInWebApp: true,
|
||||
isInstalledApp: false,
|
||||
isResponding: false,
|
||||
onOpenLogModal,
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
// Log button should not be rendered
|
||||
// The component structure means we need to check differently
|
||||
})
|
||||
|
||||
it('should not show log button when isInstalledApp is true', () => {
|
||||
const onOpenLogModal = jest.fn()
|
||||
render(
|
||||
<MetaSection
|
||||
{...createBaseProps({
|
||||
isInWebApp: false,
|
||||
isInstalledApp: true,
|
||||
isResponding: false,
|
||||
onOpenLogModal,
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
})
|
||||
|
||||
it('should not show log button when isResponding is true', () => {
|
||||
const onOpenLogModal = jest.fn()
|
||||
render(
|
||||
<MetaSection
|
||||
{...createBaseProps({
|
||||
isInWebApp: false,
|
||||
isInstalledApp: false,
|
||||
isResponding: true,
|
||||
onOpenLogModal,
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
})
|
||||
|
||||
it('should disable log button when isError is true', () => {
|
||||
const onOpenLogModal = jest.fn()
|
||||
render(
|
||||
<MetaSection
|
||||
{...createBaseProps({
|
||||
isInWebApp: false,
|
||||
isInstalledApp: false,
|
||||
isResponding: false,
|
||||
isError: true,
|
||||
onOpenLogModal,
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
const buttons = screen.getAllByRole('button')
|
||||
const disabledButton = buttons.find(btn => btn.hasAttribute('disabled'))
|
||||
expect(disabledButton).toBeDefined()
|
||||
})
|
||||
|
||||
it('should disable log button when messageId is null', () => {
|
||||
render(
|
||||
<MetaSection
|
||||
{...createBaseProps({
|
||||
isInWebApp: false,
|
||||
isInstalledApp: false,
|
||||
isResponding: false,
|
||||
messageId: null,
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
const buttons = screen.getAllByRole('button')
|
||||
const disabledButton = buttons.find(btn => btn.hasAttribute('disabled'))
|
||||
expect(disabledButton).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// More Like This Button Tests
|
||||
// Tests for the "more like this" button
|
||||
// --------------------------------------------------------------------------
|
||||
describe('More Like This Button', () => {
|
||||
it('should show more like this button when moreLikeThis is true', () => {
|
||||
render(<MetaSection {...createBaseProps({ moreLikeThis: true })} />)
|
||||
const buttons = screen.getAllByRole('button')
|
||||
expect(buttons.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should not show more like this button when moreLikeThis is false', () => {
|
||||
render(<MetaSection {...createBaseProps({ moreLikeThis: false })} />)
|
||||
// Button count should be lower
|
||||
})
|
||||
|
||||
it('should disable more like this button when disableMoreLikeThis is true', () => {
|
||||
render(
|
||||
<MetaSection
|
||||
{...createBaseProps({
|
||||
moreLikeThis: true,
|
||||
disableMoreLikeThis: true,
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
const buttons = screen.getAllByRole('button')
|
||||
const disabledButton = buttons.find(
|
||||
btn => btn.getAttribute('data-state') === 'disabled' || btn.hasAttribute('disabled'),
|
||||
)
|
||||
expect(disabledButton).toBeDefined()
|
||||
})
|
||||
|
||||
it('should call onMoreLikeThis when button is clicked', () => {
|
||||
const onMoreLikeThis = jest.fn()
|
||||
render(
|
||||
<MetaSection
|
||||
{...createBaseProps({
|
||||
moreLikeThis: true,
|
||||
disableMoreLikeThis: false,
|
||||
onMoreLikeThis,
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
const buttons = screen.getAllByRole('button')
|
||||
// Find the more like this button (first non-disabled button)
|
||||
const moreButton = buttons.find(btn => !btn.hasAttribute('disabled'))
|
||||
if (moreButton)
|
||||
fireEvent.click(moreButton)
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Text to Speech Button Tests
|
||||
// Tests for the audio/TTS button
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Text to Speech Button', () => {
|
||||
it('should show audio button when isShowTextToSpeech is true and messageId exists', () => {
|
||||
render(
|
||||
<MetaSection
|
||||
{...createBaseProps({
|
||||
isShowTextToSpeech: true,
|
||||
messageId: 'audio-msg',
|
||||
textToSpeechVoice: 'en-US',
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
const audioButton = screen.getByTestId('audio-button')
|
||||
expect(audioButton).toBeInTheDocument()
|
||||
expect(audioButton).toHaveAttribute('data-id', 'audio-msg')
|
||||
expect(audioButton).toHaveAttribute('data-voice', 'en-US')
|
||||
})
|
||||
|
||||
it('should not show audio button when isShowTextToSpeech is false', () => {
|
||||
render(
|
||||
<MetaSection
|
||||
{...createBaseProps({
|
||||
isShowTextToSpeech: false,
|
||||
messageId: 'audio-msg',
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
expect(screen.queryByTestId('audio-button')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not show audio button when messageId is null', () => {
|
||||
render(
|
||||
<MetaSection
|
||||
{...createBaseProps({
|
||||
isShowTextToSpeech: true,
|
||||
messageId: null,
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
expect(screen.queryByTestId('audio-button')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Copy Button Tests
|
||||
// Tests for the copy button
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Copy Button', () => {
|
||||
it('should show copy button when canCopy is true', () => {
|
||||
const onCopy = jest.fn()
|
||||
render(<MetaSection {...createBaseProps({ canCopy: true, onCopy })} />)
|
||||
const buttons = screen.getAllByRole('button')
|
||||
expect(buttons.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should not show copy button when canCopy is false', () => {
|
||||
render(<MetaSection {...createBaseProps({ canCopy: false })} />)
|
||||
// Copy button should not be rendered
|
||||
})
|
||||
|
||||
it('should disable copy button when isError is true', () => {
|
||||
render(
|
||||
<MetaSection
|
||||
{...createBaseProps({
|
||||
canCopy: true,
|
||||
isError: true,
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
const buttons = screen.getAllByRole('button')
|
||||
const disabledButton = buttons.find(btn => btn.hasAttribute('disabled'))
|
||||
expect(disabledButton).toBeDefined()
|
||||
})
|
||||
|
||||
it('should disable copy button when messageId is null', () => {
|
||||
render(
|
||||
<MetaSection
|
||||
{...createBaseProps({
|
||||
canCopy: true,
|
||||
messageId: null,
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
const buttons = screen.getAllByRole('button')
|
||||
const disabledButton = buttons.find(btn => btn.hasAttribute('disabled'))
|
||||
expect(disabledButton).toBeDefined()
|
||||
})
|
||||
|
||||
it('should call onCopy when button is clicked', () => {
|
||||
const onCopy = jest.fn()
|
||||
render(
|
||||
<MetaSection
|
||||
{...createBaseProps({
|
||||
canCopy: true,
|
||||
messageId: 'copy-msg',
|
||||
isError: false,
|
||||
onCopy,
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
const buttons = screen.getAllByRole('button')
|
||||
const copyButton = buttons.find(btn => !btn.hasAttribute('disabled'))
|
||||
if (copyButton)
|
||||
fireEvent.click(copyButton)
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Retry Button Tests
|
||||
// Tests for the retry button in error state
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Retry Button', () => {
|
||||
it('should show retry button when isInWebApp is true and isError is true', () => {
|
||||
render(
|
||||
<MetaSection
|
||||
{...createBaseProps({
|
||||
isInWebApp: true,
|
||||
isError: true,
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
const buttons = screen.getAllByRole('button')
|
||||
expect(buttons.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should not show retry button when isInWebApp is false', () => {
|
||||
render(
|
||||
<MetaSection
|
||||
{...createBaseProps({
|
||||
isInWebApp: false,
|
||||
isError: true,
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
// Retry button should not render
|
||||
})
|
||||
|
||||
it('should not show retry button when isError is false', () => {
|
||||
render(
|
||||
<MetaSection
|
||||
{...createBaseProps({
|
||||
isInWebApp: true,
|
||||
isError: false,
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
// Retry button should not render
|
||||
})
|
||||
|
||||
it('should call onRetry when retry button is clicked', () => {
|
||||
const onRetry = jest.fn()
|
||||
render(
|
||||
<MetaSection
|
||||
{...createBaseProps({
|
||||
isInWebApp: true,
|
||||
isError: true,
|
||||
onRetry,
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
const buttons = screen.getAllByRole('button')
|
||||
const retryButton = buttons.find(btn => !btn.hasAttribute('disabled'))
|
||||
if (retryButton)
|
||||
fireEvent.click(retryButton)
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Save Button Tests
|
||||
// Tests for the save/bookmark button
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Save Button', () => {
|
||||
it('should show save button when isInWebApp is true and not workflow', () => {
|
||||
render(
|
||||
<MetaSection
|
||||
{...createBaseProps({
|
||||
isInWebApp: true,
|
||||
isWorkflow: false,
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
const buttons = screen.getAllByRole('button')
|
||||
expect(buttons.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should not show save button when isWorkflow is true', () => {
|
||||
render(
|
||||
<MetaSection
|
||||
{...createBaseProps({
|
||||
isInWebApp: true,
|
||||
isWorkflow: true,
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
// Save button should not render in workflow mode
|
||||
})
|
||||
|
||||
it('should disable save button when isError is true', () => {
|
||||
render(
|
||||
<MetaSection
|
||||
{...createBaseProps({
|
||||
isInWebApp: true,
|
||||
isWorkflow: false,
|
||||
isError: true,
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
const buttons = screen.getAllByRole('button')
|
||||
const disabledButton = buttons.find(btn => btn.hasAttribute('disabled'))
|
||||
expect(disabledButton).toBeDefined()
|
||||
})
|
||||
|
||||
it('should disable save button when messageId is null', () => {
|
||||
render(
|
||||
<MetaSection
|
||||
{...createBaseProps({
|
||||
isInWebApp: true,
|
||||
isWorkflow: false,
|
||||
messageId: null,
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
const buttons = screen.getAllByRole('button')
|
||||
const disabledButton = buttons.find(btn => btn.hasAttribute('disabled'))
|
||||
expect(disabledButton).toBeDefined()
|
||||
})
|
||||
|
||||
it('should call onSave with messageId when button is clicked', () => {
|
||||
const onSave = jest.fn()
|
||||
render(
|
||||
<MetaSection
|
||||
{...createBaseProps({
|
||||
isInWebApp: true,
|
||||
isWorkflow: false,
|
||||
messageId: 'save-msg',
|
||||
isError: false,
|
||||
onSave,
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
const buttons = screen.getAllByRole('button')
|
||||
const saveButton = buttons.find(btn => !btn.hasAttribute('disabled'))
|
||||
if (saveButton)
|
||||
fireEvent.click(saveButton)
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Feedback Section Tests
|
||||
// Tests for the feedback (like/dislike) buttons
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Feedback Section', () => {
|
||||
it('should show feedback when supportFeedback is true and conditions are met', () => {
|
||||
render(
|
||||
<MetaSection
|
||||
{...createBaseProps({
|
||||
supportFeedback: true,
|
||||
isWorkflow: false,
|
||||
isError: false,
|
||||
messageId: 'feedback-msg',
|
||||
onFeedback: jest.fn(),
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
const buttons = screen.getAllByRole('button')
|
||||
expect(buttons.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should show feedback when isInWebApp is true', () => {
|
||||
render(
|
||||
<MetaSection
|
||||
{...createBaseProps({
|
||||
isInWebApp: true,
|
||||
isWorkflow: false,
|
||||
isError: false,
|
||||
messageId: 'feedback-msg',
|
||||
onFeedback: jest.fn(),
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
const buttons = screen.getAllByRole('button')
|
||||
expect(buttons.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should not show feedback when isWorkflow is true', () => {
|
||||
render(
|
||||
<MetaSection
|
||||
{...createBaseProps({
|
||||
supportFeedback: true,
|
||||
isWorkflow: true,
|
||||
messageId: 'feedback-msg',
|
||||
onFeedback: jest.fn(),
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
// Feedback section should not render
|
||||
})
|
||||
|
||||
it('should not show feedback when isError is true', () => {
|
||||
render(
|
||||
<MetaSection
|
||||
{...createBaseProps({
|
||||
supportFeedback: true,
|
||||
isWorkflow: false,
|
||||
isError: true,
|
||||
messageId: 'feedback-msg',
|
||||
onFeedback: jest.fn(),
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
// Feedback section should not render
|
||||
})
|
||||
|
||||
it('should not show feedback when messageId is null', () => {
|
||||
render(
|
||||
<MetaSection
|
||||
{...createBaseProps({
|
||||
supportFeedback: true,
|
||||
isWorkflow: false,
|
||||
isError: false,
|
||||
messageId: null,
|
||||
onFeedback: jest.fn(),
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
// Feedback section should not render
|
||||
})
|
||||
|
||||
it('should not show feedback when onFeedback is undefined', () => {
|
||||
render(
|
||||
<MetaSection
|
||||
{...createBaseProps({
|
||||
supportFeedback: true,
|
||||
isWorkflow: false,
|
||||
isError: false,
|
||||
messageId: 'feedback-msg',
|
||||
onFeedback: undefined,
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
// Feedback section should not render
|
||||
})
|
||||
|
||||
it('should handle like feedback state', () => {
|
||||
render(
|
||||
<MetaSection
|
||||
{...createBaseProps({
|
||||
supportFeedback: true,
|
||||
isWorkflow: false,
|
||||
isError: false,
|
||||
messageId: 'feedback-msg',
|
||||
feedback: { rating: 'like' },
|
||||
onFeedback: jest.fn(),
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
const buttons = screen.getAllByRole('button')
|
||||
const activeButton = buttons.find(btn => btn.getAttribute('data-state') === 'active')
|
||||
expect(activeButton).toBeDefined()
|
||||
})
|
||||
|
||||
it('should handle dislike feedback state', () => {
|
||||
render(
|
||||
<MetaSection
|
||||
{...createBaseProps({
|
||||
supportFeedback: true,
|
||||
isWorkflow: false,
|
||||
isError: false,
|
||||
messageId: 'feedback-msg',
|
||||
feedback: { rating: 'dislike' },
|
||||
onFeedback: jest.fn(),
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
const buttons = screen.getAllByRole('button')
|
||||
const destructiveButton = buttons.find(btn => btn.getAttribute('data-state') === 'destructive')
|
||||
expect(destructiveButton).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Indentation Tests
|
||||
// Tests for the child indentation styling
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Indentation', () => {
|
||||
it('should apply indent class when shouldIndentForChild is true', () => {
|
||||
const { container } = render(
|
||||
<MetaSection {...createBaseProps({ shouldIndentForChild: true })} />,
|
||||
)
|
||||
expect(container.firstChild).toHaveClass('pl-10')
|
||||
})
|
||||
|
||||
it('should not apply indent class when shouldIndentForChild is false', () => {
|
||||
const { container } = render(
|
||||
<MetaSection {...createBaseProps({ shouldIndentForChild: false })} />,
|
||||
)
|
||||
expect(container.firstChild).not.toHaveClass('pl-10')
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Boundary Conditions Tests
|
||||
// Tests for edge cases
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Boundary Conditions', () => {
|
||||
it('should handle all props being minimal', () => {
|
||||
render(
|
||||
<MetaSection
|
||||
{...createBaseProps({
|
||||
showCharCount: false,
|
||||
moreLikeThis: false,
|
||||
canCopy: false,
|
||||
isShowTextToSpeech: false,
|
||||
isInWebApp: false,
|
||||
supportFeedback: false,
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
// Should render without crashing
|
||||
const buttons = screen.getAllByRole('button')
|
||||
expect(buttons.length).toBeGreaterThan(0) // At least log button
|
||||
})
|
||||
|
||||
it('should handle undefined messageId', () => {
|
||||
render(<MetaSection {...createBaseProps({ messageId: undefined })} />)
|
||||
// Should render without crashing
|
||||
})
|
||||
|
||||
it('should handle empty string messageId', () => {
|
||||
render(<MetaSection {...createBaseProps({ messageId: '' })} />)
|
||||
// Empty string is falsy, so buttons should be disabled
|
||||
const buttons = screen.getAllByRole('button')
|
||||
const disabledButton = buttons.find(btn => btn.hasAttribute('disabled'))
|
||||
expect(disabledButton).toBeDefined()
|
||||
})
|
||||
|
||||
it('should handle undefined textToSpeechVoice', () => {
|
||||
render(
|
||||
<MetaSection
|
||||
{...createBaseProps({
|
||||
isShowTextToSpeech: true,
|
||||
messageId: 'voice-msg',
|
||||
textToSpeechVoice: undefined,
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
const audioButton = screen.getByTestId('audio-button')
|
||||
// undefined becomes null in data attribute
|
||||
expect(audioButton).not.toHaveAttribute('data-voice', 'some-value')
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user