test: add comprehensive tests for ContentSection, hooks, and MetaSection components

This commit is contained in:
yyh
2025-12-11 18:15:07 +08:00
parent ab3a933e11
commit 78d47fa28f
4 changed files with 2177 additions and 250 deletions

View File

@@ -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()
})
})
})

View 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

View 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')
})
})
})