test: enhance workflow-log component tests with comprehensive coverage (#29616)

Signed-off-by: yyh <yuanyouhuilyz@gmail.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
yyh 2025-12-15 21:21:14 +08:00 committed by GitHub
parent 23f75a1185
commit 103a5e0122
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 2443 additions and 1119 deletions

View File

@ -0,0 +1,325 @@
/**
* DetailPanel Component Tests
*
* Tests the workflow run detail panel which displays:
* - Workflow run title
* - Replay button (when canReplay is true)
* - Close button
* - Run component with detail/tracing URLs
*/
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import DetailPanel from './detail'
import { useStore as useAppStore } from '@/app/components/app/store'
import type { App, AppIconType, AppModeEnum } from '@/types/app'
// ============================================================================
// Mocks
// ============================================================================
jest.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}))
const mockRouterPush = jest.fn()
jest.mock('next/navigation', () => ({
useRouter: () => ({
push: mockRouterPush,
}),
}))
// Mock the Run component as it has complex dependencies
jest.mock('@/app/components/workflow/run', () => ({
__esModule: true,
default: ({ runDetailUrl, tracingListUrl }: { runDetailUrl: string; tracingListUrl: string }) => (
<div data-testid="workflow-run">
<span data-testid="run-detail-url">{runDetailUrl}</span>
<span data-testid="tracing-list-url">{tracingListUrl}</span>
</div>
),
}))
// Mock WorkflowContextProvider
jest.mock('@/app/components/workflow/context', () => ({
WorkflowContextProvider: ({ children }: { children: React.ReactNode }) => (
<div data-testid="workflow-context-provider">{children}</div>
),
}))
// Mock ahooks for useBoolean (used by TooltipPlus)
jest.mock('ahooks', () => ({
useBoolean: (initial: boolean) => {
const setters = {
setTrue: jest.fn(),
setFalse: jest.fn(),
toggle: jest.fn(),
}
return [initial, setters] as const
},
}))
// ============================================================================
// Test Data Factories
// ============================================================================
const createMockApp = (overrides: Partial<App> = {}): App => ({
id: 'test-app-id',
name: 'Test App',
description: 'Test app description',
author_name: 'Test Author',
icon_type: 'emoji' as AppIconType,
icon: '🚀',
icon_background: '#FFEAD5',
icon_url: null,
use_icon_as_answer_icon: false,
mode: 'workflow' as AppModeEnum,
enable_site: true,
enable_api: true,
api_rpm: 60,
api_rph: 3600,
is_demo: false,
model_config: {} as App['model_config'],
app_model_config: {} as App['app_model_config'],
created_at: Date.now(),
updated_at: Date.now(),
site: {
access_token: 'token',
app_base_url: 'https://example.com',
} as App['site'],
api_base_url: 'https://api.example.com',
tags: [],
access_mode: 'public_access' as App['access_mode'],
...overrides,
})
// ============================================================================
// Tests
// ============================================================================
describe('DetailPanel', () => {
const defaultOnClose = jest.fn()
beforeEach(() => {
jest.clearAllMocks()
useAppStore.setState({ appDetail: createMockApp() })
})
// --------------------------------------------------------------------------
// Rendering Tests (REQUIRED)
// --------------------------------------------------------------------------
describe('Rendering', () => {
it('should render without crashing', () => {
render(<DetailPanel runID="run-123" onClose={defaultOnClose} />)
expect(screen.getByText('appLog.runDetail.workflowTitle')).toBeInTheDocument()
})
it('should render workflow title', () => {
render(<DetailPanel runID="run-123" onClose={defaultOnClose} />)
expect(screen.getByText('appLog.runDetail.workflowTitle')).toBeInTheDocument()
})
it('should render close button', () => {
const { container } = render(<DetailPanel runID="run-123" onClose={defaultOnClose} />)
// Close button has RiCloseLine icon
const closeButton = container.querySelector('span.cursor-pointer')
expect(closeButton).toBeInTheDocument()
})
it('should render Run component with correct URLs', () => {
useAppStore.setState({ appDetail: createMockApp({ id: 'app-456' }) })
render(<DetailPanel runID="run-789" onClose={defaultOnClose} />)
expect(screen.getByTestId('workflow-run')).toBeInTheDocument()
expect(screen.getByTestId('run-detail-url')).toHaveTextContent('/apps/app-456/workflow-runs/run-789')
expect(screen.getByTestId('tracing-list-url')).toHaveTextContent('/apps/app-456/workflow-runs/run-789/node-executions')
})
it('should render WorkflowContextProvider wrapper', () => {
render(<DetailPanel runID="run-123" onClose={defaultOnClose} />)
expect(screen.getByTestId('workflow-context-provider')).toBeInTheDocument()
})
})
// --------------------------------------------------------------------------
// Props Tests (REQUIRED)
// --------------------------------------------------------------------------
describe('Props', () => {
it('should not render replay button when canReplay is false (default)', () => {
render(<DetailPanel runID="run-123" onClose={defaultOnClose} />)
expect(screen.queryByRole('button', { name: 'appLog.runDetail.testWithParams' })).not.toBeInTheDocument()
})
it('should render replay button when canReplay is true', () => {
render(<DetailPanel runID="run-123" onClose={defaultOnClose} canReplay={true} />)
expect(screen.getByRole('button', { name: 'appLog.runDetail.testWithParams' })).toBeInTheDocument()
})
it('should use empty URL when runID is empty', () => {
render(<DetailPanel runID="" onClose={defaultOnClose} />)
expect(screen.getByTestId('run-detail-url')).toHaveTextContent('')
expect(screen.getByTestId('tracing-list-url')).toHaveTextContent('')
})
})
// --------------------------------------------------------------------------
// User Interactions
// --------------------------------------------------------------------------
describe('User Interactions', () => {
it('should call onClose when close button is clicked', async () => {
const user = userEvent.setup()
const onClose = jest.fn()
const { container } = render(<DetailPanel runID="run-123" onClose={onClose} />)
const closeButton = container.querySelector('span.cursor-pointer')
expect(closeButton).toBeInTheDocument()
await user.click(closeButton!)
expect(onClose).toHaveBeenCalledTimes(1)
})
it('should navigate to workflow page with replayRunId when replay button is clicked', async () => {
const user = userEvent.setup()
useAppStore.setState({ appDetail: createMockApp({ id: 'app-replay-test' }) })
render(<DetailPanel runID="run-to-replay" onClose={defaultOnClose} canReplay={true} />)
const replayButton = screen.getByRole('button', { name: 'appLog.runDetail.testWithParams' })
await user.click(replayButton)
expect(mockRouterPush).toHaveBeenCalledWith('/app/app-replay-test/workflow?replayRunId=run-to-replay')
})
it('should not navigate when replay clicked but appDetail is missing', async () => {
const user = userEvent.setup()
useAppStore.setState({ appDetail: undefined })
render(<DetailPanel runID="run-123" onClose={defaultOnClose} canReplay={true} />)
const replayButton = screen.getByRole('button', { name: 'appLog.runDetail.testWithParams' })
await user.click(replayButton)
expect(mockRouterPush).not.toHaveBeenCalled()
})
})
// --------------------------------------------------------------------------
// URL Generation Tests
// --------------------------------------------------------------------------
describe('URL Generation', () => {
it('should generate correct run detail URL', () => {
useAppStore.setState({ appDetail: createMockApp({ id: 'my-app' }) })
render(<DetailPanel runID="my-run" onClose={defaultOnClose} />)
expect(screen.getByTestId('run-detail-url')).toHaveTextContent('/apps/my-app/workflow-runs/my-run')
})
it('should generate correct tracing list URL', () => {
useAppStore.setState({ appDetail: createMockApp({ id: 'my-app' }) })
render(<DetailPanel runID="my-run" onClose={defaultOnClose} />)
expect(screen.getByTestId('tracing-list-url')).toHaveTextContent('/apps/my-app/workflow-runs/my-run/node-executions')
})
it('should handle special characters in runID', () => {
useAppStore.setState({ appDetail: createMockApp({ id: 'app-id' }) })
render(<DetailPanel runID="run-with-special-123" onClose={defaultOnClose} />)
expect(screen.getByTestId('run-detail-url')).toHaveTextContent('/apps/app-id/workflow-runs/run-with-special-123')
})
})
// --------------------------------------------------------------------------
// Store Integration Tests
// --------------------------------------------------------------------------
describe('Store Integration', () => {
it('should read appDetail from store', () => {
useAppStore.setState({ appDetail: createMockApp({ id: 'store-app-id' }) })
render(<DetailPanel runID="run-123" onClose={defaultOnClose} />)
expect(screen.getByTestId('run-detail-url')).toHaveTextContent('/apps/store-app-id/workflow-runs/run-123')
})
it('should handle undefined appDetail from store gracefully', () => {
useAppStore.setState({ appDetail: undefined })
render(<DetailPanel runID="run-123" onClose={defaultOnClose} />)
// Run component should still render but with undefined in URL
expect(screen.getByTestId('workflow-run')).toBeInTheDocument()
})
})
// --------------------------------------------------------------------------
// Edge Cases (REQUIRED)
// --------------------------------------------------------------------------
describe('Edge Cases', () => {
it('should handle empty runID', () => {
render(<DetailPanel runID="" onClose={defaultOnClose} />)
expect(screen.getByTestId('run-detail-url')).toHaveTextContent('')
expect(screen.getByTestId('tracing-list-url')).toHaveTextContent('')
})
it('should handle very long runID', () => {
const longRunId = 'a'.repeat(100)
useAppStore.setState({ appDetail: createMockApp({ id: 'app-id' }) })
render(<DetailPanel runID={longRunId} onClose={defaultOnClose} />)
expect(screen.getByTestId('run-detail-url')).toHaveTextContent(`/apps/app-id/workflow-runs/${longRunId}`)
})
it('should render replay button with correct aria-label', () => {
render(<DetailPanel runID="run-123" onClose={defaultOnClose} canReplay={true} />)
const replayButton = screen.getByRole('button', { name: 'appLog.runDetail.testWithParams' })
expect(replayButton).toHaveAttribute('aria-label', 'appLog.runDetail.testWithParams')
})
it('should maintain proper component structure', () => {
const { container } = render(<DetailPanel runID="run-123" onClose={defaultOnClose} />)
// Check for main container with flex layout
const mainContainer = container.querySelector('.flex.grow.flex-col')
expect(mainContainer).toBeInTheDocument()
// Check for header section
const header = container.querySelector('.flex.items-center.bg-components-panel-bg')
expect(header).toBeInTheDocument()
})
})
// --------------------------------------------------------------------------
// Tooltip Tests
// --------------------------------------------------------------------------
describe('Tooltip', () => {
it('should have tooltip on replay button', () => {
render(<DetailPanel runID="run-123" onClose={defaultOnClose} canReplay={true} />)
// The replay button should be wrapped in TooltipPlus
const replayButton = screen.getByRole('button', { name: 'appLog.runDetail.testWithParams' })
expect(replayButton).toBeInTheDocument()
// TooltipPlus wraps the button with popupContent
// We verify the button exists with the correct aria-label
expect(replayButton).toHaveAttribute('type', 'button')
})
})
})

View File

@ -0,0 +1,533 @@
/**
* Filter Component Tests
*
* Tests the workflow log filter component which provides:
* - Status filtering (all, succeeded, failed, stopped, partial-succeeded)
* - Time period selection
* - Keyword search
*/
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import Filter, { TIME_PERIOD_MAPPING } from './filter'
import type { QueryParam } from './index'
// ============================================================================
// Mocks
// ============================================================================
jest.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}))
const mockTrackEvent = jest.fn()
jest.mock('@/app/components/base/amplitude/utils', () => ({
trackEvent: (...args: unknown[]) => mockTrackEvent(...args),
}))
// ============================================================================
// Test Data Factories
// ============================================================================
const createDefaultQueryParams = (overrides: Partial<QueryParam> = {}): QueryParam => ({
status: 'all',
period: '2', // default to last 7 days
...overrides,
})
// ============================================================================
// Tests
// ============================================================================
describe('Filter', () => {
const defaultSetQueryParams = jest.fn()
beforeEach(() => {
jest.clearAllMocks()
})
// --------------------------------------------------------------------------
// Rendering Tests (REQUIRED)
// --------------------------------------------------------------------------
describe('Rendering', () => {
it('should render without crashing', () => {
render(
<Filter
queryParams={createDefaultQueryParams()}
setQueryParams={defaultSetQueryParams}
/>,
)
// Should render status chip, period chip, and search input
expect(screen.getByText('All')).toBeInTheDocument()
expect(screen.getByPlaceholderText('common.operation.search')).toBeInTheDocument()
})
it('should render all filter components', () => {
render(
<Filter
queryParams={createDefaultQueryParams()}
setQueryParams={defaultSetQueryParams}
/>,
)
// Status chip
expect(screen.getByText('All')).toBeInTheDocument()
// Period chip (shows translated key)
expect(screen.getByText('appLog.filter.period.last7days')).toBeInTheDocument()
// Search input
expect(screen.getByPlaceholderText('common.operation.search')).toBeInTheDocument()
})
})
// --------------------------------------------------------------------------
// Status Filter Tests
// --------------------------------------------------------------------------
describe('Status Filter', () => {
it('should display current status value', () => {
render(
<Filter
queryParams={createDefaultQueryParams({ status: 'succeeded' })}
setQueryParams={defaultSetQueryParams}
/>,
)
// Chip should show Success for succeeded status
expect(screen.getByText('Success')).toBeInTheDocument()
})
it('should open status dropdown when clicked', async () => {
const user = userEvent.setup()
render(
<Filter
queryParams={createDefaultQueryParams()}
setQueryParams={defaultSetQueryParams}
/>,
)
await user.click(screen.getByText('All'))
// Should show all status options
await waitFor(() => {
expect(screen.getByText('Success')).toBeInTheDocument()
expect(screen.getByText('Fail')).toBeInTheDocument()
expect(screen.getByText('Stop')).toBeInTheDocument()
expect(screen.getByText('Partial Success')).toBeInTheDocument()
})
})
it('should call setQueryParams when status is selected', async () => {
const user = userEvent.setup()
const setQueryParams = jest.fn()
render(
<Filter
queryParams={createDefaultQueryParams()}
setQueryParams={setQueryParams}
/>,
)
await user.click(screen.getByText('All'))
await user.click(await screen.findByText('Success'))
expect(setQueryParams).toHaveBeenCalledWith({
status: 'succeeded',
period: '2',
})
})
it('should track status selection event', async () => {
const user = userEvent.setup()
render(
<Filter
queryParams={createDefaultQueryParams()}
setQueryParams={defaultSetQueryParams}
/>,
)
await user.click(screen.getByText('All'))
await user.click(await screen.findByText('Fail'))
expect(mockTrackEvent).toHaveBeenCalledWith(
'workflow_log_filter_status_selected',
{ workflow_log_filter_status: 'failed' },
)
})
it('should reset to all when status is cleared', async () => {
const user = userEvent.setup()
const setQueryParams = jest.fn()
const { container } = render(
<Filter
queryParams={createDefaultQueryParams({ status: 'succeeded' })}
setQueryParams={setQueryParams}
/>,
)
// Find the clear icon (div with group/clear class) in the status chip
const clearIcon = container.querySelector('.group\\/clear')
expect(clearIcon).toBeInTheDocument()
await user.click(clearIcon!)
expect(setQueryParams).toHaveBeenCalledWith({
status: 'all',
period: '2',
})
})
test.each([
['all', 'All'],
['succeeded', 'Success'],
['failed', 'Fail'],
['stopped', 'Stop'],
['partial-succeeded', 'Partial Success'],
])('should display correct label for %s status', (statusValue, expectedLabel) => {
render(
<Filter
queryParams={createDefaultQueryParams({ status: statusValue })}
setQueryParams={defaultSetQueryParams}
/>,
)
expect(screen.getByText(expectedLabel)).toBeInTheDocument()
})
})
// --------------------------------------------------------------------------
// Time Period Filter Tests
// --------------------------------------------------------------------------
describe('Time Period Filter', () => {
it('should display current period value', () => {
render(
<Filter
queryParams={createDefaultQueryParams({ period: '1' })}
setQueryParams={defaultSetQueryParams}
/>,
)
expect(screen.getByText('appLog.filter.period.today')).toBeInTheDocument()
})
it('should open period dropdown when clicked', async () => {
const user = userEvent.setup()
render(
<Filter
queryParams={createDefaultQueryParams()}
setQueryParams={defaultSetQueryParams}
/>,
)
await user.click(screen.getByText('appLog.filter.period.last7days'))
// Should show all period options
await waitFor(() => {
expect(screen.getByText('appLog.filter.period.today')).toBeInTheDocument()
expect(screen.getByText('appLog.filter.period.last4weeks')).toBeInTheDocument()
expect(screen.getByText('appLog.filter.period.last3months')).toBeInTheDocument()
expect(screen.getByText('appLog.filter.period.allTime')).toBeInTheDocument()
})
})
it('should call setQueryParams when period is selected', async () => {
const user = userEvent.setup()
const setQueryParams = jest.fn()
render(
<Filter
queryParams={createDefaultQueryParams()}
setQueryParams={setQueryParams}
/>,
)
await user.click(screen.getByText('appLog.filter.period.last7days'))
await user.click(await screen.findByText('appLog.filter.period.allTime'))
expect(setQueryParams).toHaveBeenCalledWith({
status: 'all',
period: '9',
})
})
it('should reset period to allTime when cleared', async () => {
const user = userEvent.setup()
const setQueryParams = jest.fn()
render(
<Filter
queryParams={createDefaultQueryParams({ period: '2' })}
setQueryParams={setQueryParams}
/>,
)
// Find the period chip's clear button
const periodChip = screen.getByText('appLog.filter.period.last7days').closest('div')
const clearButton = periodChip?.querySelector('button[type="button"]')
if (clearButton) {
await user.click(clearButton)
expect(setQueryParams).toHaveBeenCalledWith({
status: 'all',
period: '9',
})
}
})
})
// --------------------------------------------------------------------------
// Keyword Search Tests
// --------------------------------------------------------------------------
describe('Keyword Search', () => {
it('should display current keyword value', () => {
render(
<Filter
queryParams={createDefaultQueryParams({ keyword: 'test search' })}
setQueryParams={defaultSetQueryParams}
/>,
)
expect(screen.getByDisplayValue('test search')).toBeInTheDocument()
})
it('should call setQueryParams when typing in search', async () => {
const user = userEvent.setup()
const setQueryParams = jest.fn()
render(
<Filter
queryParams={createDefaultQueryParams()}
setQueryParams={setQueryParams}
/>,
)
const input = screen.getByPlaceholderText('common.operation.search')
await user.type(input, 'workflow')
// Should call setQueryParams for each character typed
expect(setQueryParams).toHaveBeenLastCalledWith(
expect.objectContaining({ keyword: 'workflow' }),
)
})
it('should clear keyword when clear button is clicked', async () => {
const user = userEvent.setup()
const setQueryParams = jest.fn()
const { container } = render(
<Filter
queryParams={createDefaultQueryParams({ keyword: 'test' })}
setQueryParams={setQueryParams}
/>,
)
// The Input component renders a clear icon div inside the input wrapper
// when showClearIcon is true and value exists
const inputWrapper = container.querySelector('.w-\\[200px\\]')
// Find the clear icon div (has cursor-pointer class and contains RiCloseCircleFill)
const clearIconDiv = inputWrapper?.querySelector('div.cursor-pointer')
expect(clearIconDiv).toBeInTheDocument()
await user.click(clearIconDiv!)
expect(setQueryParams).toHaveBeenCalledWith({
status: 'all',
period: '2',
keyword: '',
})
})
it('should update on direct input change', () => {
const setQueryParams = jest.fn()
render(
<Filter
queryParams={createDefaultQueryParams()}
setQueryParams={setQueryParams}
/>,
)
const input = screen.getByPlaceholderText('common.operation.search')
fireEvent.change(input, { target: { value: 'new search' } })
expect(setQueryParams).toHaveBeenCalledWith({
status: 'all',
period: '2',
keyword: 'new search',
})
})
})
// --------------------------------------------------------------------------
// TIME_PERIOD_MAPPING Tests
// --------------------------------------------------------------------------
describe('TIME_PERIOD_MAPPING', () => {
it('should have correct mapping for today', () => {
expect(TIME_PERIOD_MAPPING['1']).toEqual({ value: 0, name: 'today' })
})
it('should have correct mapping for last 7 days', () => {
expect(TIME_PERIOD_MAPPING['2']).toEqual({ value: 7, name: 'last7days' })
})
it('should have correct mapping for last 4 weeks', () => {
expect(TIME_PERIOD_MAPPING['3']).toEqual({ value: 28, name: 'last4weeks' })
})
it('should have correct mapping for all time', () => {
expect(TIME_PERIOD_MAPPING['9']).toEqual({ value: -1, name: 'allTime' })
})
it('should have all 9 predefined time periods', () => {
expect(Object.keys(TIME_PERIOD_MAPPING)).toHaveLength(9)
})
test.each([
['1', 'today', 0],
['2', 'last7days', 7],
['3', 'last4weeks', 28],
['9', 'allTime', -1],
])('TIME_PERIOD_MAPPING[%s] should have name=%s and correct value', (key, name, expectedValue) => {
const mapping = TIME_PERIOD_MAPPING[key]
expect(mapping.name).toBe(name)
if (expectedValue >= 0)
expect(mapping.value).toBe(expectedValue)
else
expect(mapping.value).toBe(-1)
})
})
// --------------------------------------------------------------------------
// Edge Cases (REQUIRED)
// --------------------------------------------------------------------------
describe('Edge Cases', () => {
it('should handle undefined keyword gracefully', () => {
render(
<Filter
queryParams={createDefaultQueryParams({ keyword: undefined })}
setQueryParams={defaultSetQueryParams}
/>,
)
const input = screen.getByPlaceholderText('common.operation.search')
expect(input).toHaveValue('')
})
it('should handle empty string keyword', () => {
render(
<Filter
queryParams={createDefaultQueryParams({ keyword: '' })}
setQueryParams={defaultSetQueryParams}
/>,
)
const input = screen.getByPlaceholderText('common.operation.search')
expect(input).toHaveValue('')
})
it('should preserve other query params when updating status', async () => {
const user = userEvent.setup()
const setQueryParams = jest.fn()
render(
<Filter
queryParams={createDefaultQueryParams({ keyword: 'test', period: '3' })}
setQueryParams={setQueryParams}
/>,
)
await user.click(screen.getByText('All'))
await user.click(await screen.findByText('Success'))
expect(setQueryParams).toHaveBeenCalledWith({
status: 'succeeded',
period: '3',
keyword: 'test',
})
})
it('should preserve other query params when updating period', async () => {
const user = userEvent.setup()
const setQueryParams = jest.fn()
render(
<Filter
queryParams={createDefaultQueryParams({ keyword: 'test', status: 'failed' })}
setQueryParams={setQueryParams}
/>,
)
await user.click(screen.getByText('appLog.filter.period.last7days'))
await user.click(await screen.findByText('appLog.filter.period.today'))
expect(setQueryParams).toHaveBeenCalledWith({
status: 'failed',
period: '1',
keyword: 'test',
})
})
it('should preserve other query params when updating keyword', async () => {
const user = userEvent.setup()
const setQueryParams = jest.fn()
render(
<Filter
queryParams={createDefaultQueryParams({ status: 'failed', period: '3' })}
setQueryParams={setQueryParams}
/>,
)
const input = screen.getByPlaceholderText('common.operation.search')
await user.type(input, 'a')
expect(setQueryParams).toHaveBeenCalledWith({
status: 'failed',
period: '3',
keyword: 'a',
})
})
})
// --------------------------------------------------------------------------
// Integration Tests
// --------------------------------------------------------------------------
describe('Integration', () => {
it('should render with all filters visible simultaneously', () => {
render(
<Filter
queryParams={createDefaultQueryParams({
status: 'succeeded',
period: '1',
keyword: 'integration test',
})}
setQueryParams={defaultSetQueryParams}
/>,
)
expect(screen.getByText('Success')).toBeInTheDocument()
expect(screen.getByText('appLog.filter.period.today')).toBeInTheDocument()
expect(screen.getByDisplayValue('integration test')).toBeInTheDocument()
})
it('should have proper layout with flex and gap', () => {
const { container } = render(
<Filter
queryParams={createDefaultQueryParams()}
setQueryParams={defaultSetQueryParams}
/>,
)
const filterContainer = container.firstChild as HTMLElement
expect(filterContainer).toHaveClass('flex')
expect(filterContainer).toHaveClass('flex-row')
expect(filterContainer).toHaveClass('gap-2')
})
})
})

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,757 @@
/**
* WorkflowAppLogList Component Tests
*
* Tests the workflow log list component which displays:
* - Table of workflow run logs with sortable columns
* - Status indicators (success, failed, stopped, running, partial-succeeded)
* - Trigger display for workflow apps
* - Drawer with run details
* - Loading states
*/
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import WorkflowAppLogList from './list'
import { useStore as useAppStore } from '@/app/components/app/store'
import type { App, AppIconType, AppModeEnum } from '@/types/app'
import type { WorkflowAppLogDetail, WorkflowLogsResponse, WorkflowRunDetail } from '@/models/log'
import { WorkflowRunTriggeredFrom } from '@/models/log'
import { APP_PAGE_LIMIT } from '@/config'
// ============================================================================
// Mocks
// ============================================================================
jest.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}))
const mockRouterPush = jest.fn()
jest.mock('next/navigation', () => ({
useRouter: () => ({
push: mockRouterPush,
}),
}))
// Mock useTimestamp hook
jest.mock('@/hooks/use-timestamp', () => ({
__esModule: true,
default: () => ({
formatTime: (timestamp: number, _format: string) => `formatted-${timestamp}`,
}),
}))
// Mock useBreakpoints hook
jest.mock('@/hooks/use-breakpoints', () => ({
__esModule: true,
default: () => 'pc', // Return desktop by default
MediaType: {
mobile: 'mobile',
pc: 'pc',
},
}))
// Mock the Run component
jest.mock('@/app/components/workflow/run', () => ({
__esModule: true,
default: ({ runDetailUrl, tracingListUrl }: { runDetailUrl: string; tracingListUrl: string }) => (
<div data-testid="workflow-run">
<span data-testid="run-detail-url">{runDetailUrl}</span>
<span data-testid="tracing-list-url">{tracingListUrl}</span>
</div>
),
}))
// Mock WorkflowContextProvider
jest.mock('@/app/components/workflow/context', () => ({
WorkflowContextProvider: ({ children }: { children: React.ReactNode }) => (
<div data-testid="workflow-context-provider">{children}</div>
),
}))
// Mock BlockIcon
jest.mock('@/app/components/workflow/block-icon', () => ({
__esModule: true,
default: () => <div data-testid="block-icon">BlockIcon</div>,
}))
// Mock useTheme
jest.mock('@/hooks/use-theme', () => ({
__esModule: true,
default: () => {
const { Theme } = require('@/types/app')
return { theme: Theme.light }
},
}))
// Mock ahooks
jest.mock('ahooks', () => ({
useBoolean: (initial: boolean) => {
const setters = {
setTrue: jest.fn(),
setFalse: jest.fn(),
toggle: jest.fn(),
}
return [initial, setters] as const
},
}))
// ============================================================================
// Test Data Factories
// ============================================================================
const createMockApp = (overrides: Partial<App> = {}): App => ({
id: 'test-app-id',
name: 'Test App',
description: 'Test app description',
author_name: 'Test Author',
icon_type: 'emoji' as AppIconType,
icon: '🚀',
icon_background: '#FFEAD5',
icon_url: null,
use_icon_as_answer_icon: false,
mode: 'workflow' as AppModeEnum,
enable_site: true,
enable_api: true,
api_rpm: 60,
api_rph: 3600,
is_demo: false,
model_config: {} as App['model_config'],
app_model_config: {} as App['app_model_config'],
created_at: Date.now(),
updated_at: Date.now(),
site: {
access_token: 'token',
app_base_url: 'https://example.com',
} as App['site'],
api_base_url: 'https://api.example.com',
tags: [],
access_mode: 'public_access' as App['access_mode'],
...overrides,
})
const createMockWorkflowRun = (overrides: Partial<WorkflowRunDetail> = {}): WorkflowRunDetail => ({
id: 'run-1',
version: '1.0.0',
status: 'succeeded',
elapsed_time: 1.234,
total_tokens: 100,
total_price: 0.001,
currency: 'USD',
total_steps: 5,
finished_at: Date.now(),
triggered_from: WorkflowRunTriggeredFrom.APP_RUN,
...overrides,
})
const createMockWorkflowLog = (overrides: Partial<WorkflowAppLogDetail> = {}): WorkflowAppLogDetail => ({
id: 'log-1',
workflow_run: createMockWorkflowRun(),
created_from: 'web-app',
created_by_role: 'account',
created_by_account: {
id: 'account-1',
name: 'Test User',
email: 'test@example.com',
},
created_at: Date.now(),
...overrides,
})
const createMockLogsResponse = (
data: WorkflowAppLogDetail[] = [],
total = data.length,
): WorkflowLogsResponse => ({
data,
has_more: data.length < total,
limit: APP_PAGE_LIMIT,
total,
page: 1,
})
// ============================================================================
// Tests
// ============================================================================
describe('WorkflowAppLogList', () => {
const defaultOnRefresh = jest.fn()
beforeEach(() => {
jest.clearAllMocks()
useAppStore.setState({ appDetail: createMockApp() })
})
// --------------------------------------------------------------------------
// Rendering Tests (REQUIRED)
// --------------------------------------------------------------------------
describe('Rendering', () => {
it('should render loading state when logs are undefined', () => {
const { container } = render(
<WorkflowAppLogList logs={undefined} appDetail={createMockApp()} onRefresh={defaultOnRefresh} />,
)
expect(container.querySelector('.spin-animation')).toBeInTheDocument()
})
it('should render loading state when appDetail is undefined', () => {
const logs = createMockLogsResponse([createMockWorkflowLog()])
const { container } = render(
<WorkflowAppLogList logs={logs} appDetail={undefined} onRefresh={defaultOnRefresh} />,
)
expect(container.querySelector('.spin-animation')).toBeInTheDocument()
})
it('should render table when data is available', () => {
const logs = createMockLogsResponse([createMockWorkflowLog()])
render(
<WorkflowAppLogList logs={logs} appDetail={createMockApp()} onRefresh={defaultOnRefresh} />,
)
expect(screen.getByRole('table')).toBeInTheDocument()
})
it('should render all table headers', () => {
const logs = createMockLogsResponse([createMockWorkflowLog()])
render(
<WorkflowAppLogList logs={logs} appDetail={createMockApp()} onRefresh={defaultOnRefresh} />,
)
expect(screen.getByText('appLog.table.header.startTime')).toBeInTheDocument()
expect(screen.getByText('appLog.table.header.status')).toBeInTheDocument()
expect(screen.getByText('appLog.table.header.runtime')).toBeInTheDocument()
expect(screen.getByText('appLog.table.header.tokens')).toBeInTheDocument()
expect(screen.getByText('appLog.table.header.user')).toBeInTheDocument()
})
it('should render trigger column for workflow apps', () => {
const logs = createMockLogsResponse([createMockWorkflowLog()])
const workflowApp = createMockApp({ mode: 'workflow' as AppModeEnum })
render(
<WorkflowAppLogList logs={logs} appDetail={workflowApp} onRefresh={defaultOnRefresh} />,
)
expect(screen.getByText('appLog.table.header.triggered_from')).toBeInTheDocument()
})
it('should not render trigger column for non-workflow apps', () => {
const logs = createMockLogsResponse([createMockWorkflowLog()])
const chatApp = createMockApp({ mode: 'advanced-chat' as AppModeEnum })
render(
<WorkflowAppLogList logs={logs} appDetail={chatApp} onRefresh={defaultOnRefresh} />,
)
expect(screen.queryByText('appLog.table.header.triggered_from')).not.toBeInTheDocument()
})
})
// --------------------------------------------------------------------------
// Status Display Tests
// --------------------------------------------------------------------------
describe('Status Display', () => {
it('should render success status correctly', () => {
const logs = createMockLogsResponse([
createMockWorkflowLog({
workflow_run: createMockWorkflowRun({ status: 'succeeded' }),
}),
])
render(
<WorkflowAppLogList logs={logs} appDetail={createMockApp()} onRefresh={defaultOnRefresh} />,
)
expect(screen.getByText('Success')).toBeInTheDocument()
})
it('should render failure status correctly', () => {
const logs = createMockLogsResponse([
createMockWorkflowLog({
workflow_run: createMockWorkflowRun({ status: 'failed' }),
}),
])
render(
<WorkflowAppLogList logs={logs} appDetail={createMockApp()} onRefresh={defaultOnRefresh} />,
)
expect(screen.getByText('Failure')).toBeInTheDocument()
})
it('should render stopped status correctly', () => {
const logs = createMockLogsResponse([
createMockWorkflowLog({
workflow_run: createMockWorkflowRun({ status: 'stopped' }),
}),
])
render(
<WorkflowAppLogList logs={logs} appDetail={createMockApp()} onRefresh={defaultOnRefresh} />,
)
expect(screen.getByText('Stop')).toBeInTheDocument()
})
it('should render running status correctly', () => {
const logs = createMockLogsResponse([
createMockWorkflowLog({
workflow_run: createMockWorkflowRun({ status: 'running' }),
}),
])
render(
<WorkflowAppLogList logs={logs} appDetail={createMockApp()} onRefresh={defaultOnRefresh} />,
)
expect(screen.getByText('Running')).toBeInTheDocument()
})
it('should render partial-succeeded status correctly', () => {
const logs = createMockLogsResponse([
createMockWorkflowLog({
workflow_run: createMockWorkflowRun({ status: 'partial-succeeded' as WorkflowRunDetail['status'] }),
}),
])
render(
<WorkflowAppLogList logs={logs} appDetail={createMockApp()} onRefresh={defaultOnRefresh} />,
)
expect(screen.getByText('Partial Success')).toBeInTheDocument()
})
})
// --------------------------------------------------------------------------
// User Info Display Tests
// --------------------------------------------------------------------------
describe('User Info Display', () => {
it('should display account name when created by account', () => {
const logs = createMockLogsResponse([
createMockWorkflowLog({
created_by_account: { id: 'acc-1', name: 'John Doe', email: 'john@example.com' },
created_by_end_user: undefined,
}),
])
render(
<WorkflowAppLogList logs={logs} appDetail={createMockApp()} onRefresh={defaultOnRefresh} />,
)
expect(screen.getByText('John Doe')).toBeInTheDocument()
})
it('should display end user session id when created by end user', () => {
const logs = createMockLogsResponse([
createMockWorkflowLog({
created_by_end_user: { id: 'user-1', type: 'browser', is_anonymous: false, session_id: 'session-abc-123' },
created_by_account: undefined,
}),
])
render(
<WorkflowAppLogList logs={logs} appDetail={createMockApp()} onRefresh={defaultOnRefresh} />,
)
expect(screen.getByText('session-abc-123')).toBeInTheDocument()
})
it('should display N/A when no user info', () => {
const logs = createMockLogsResponse([
createMockWorkflowLog({
created_by_account: undefined,
created_by_end_user: undefined,
}),
])
render(
<WorkflowAppLogList logs={logs} appDetail={createMockApp()} onRefresh={defaultOnRefresh} />,
)
expect(screen.getByText('N/A')).toBeInTheDocument()
})
})
// --------------------------------------------------------------------------
// Sorting Tests
// --------------------------------------------------------------------------
describe('Sorting', () => {
it('should sort logs in descending order by default', () => {
const logs = createMockLogsResponse([
createMockWorkflowLog({ id: 'log-1', created_at: 1000 }),
createMockWorkflowLog({ id: 'log-2', created_at: 2000 }),
createMockWorkflowLog({ id: 'log-3', created_at: 3000 }),
])
render(
<WorkflowAppLogList logs={logs} appDetail={createMockApp()} onRefresh={defaultOnRefresh} />,
)
const rows = screen.getAllByRole('row')
// First row is header, data rows start from index 1
// In descending order, newest (3000) should be first
expect(rows.length).toBe(4) // 1 header + 3 data rows
})
it('should toggle sort order when clicking on start time header', async () => {
const user = userEvent.setup()
const logs = createMockLogsResponse([
createMockWorkflowLog({ id: 'log-1', created_at: 1000 }),
createMockWorkflowLog({ id: 'log-2', created_at: 2000 }),
])
render(
<WorkflowAppLogList logs={logs} appDetail={createMockApp()} onRefresh={defaultOnRefresh} />,
)
// Click on the start time header to toggle sort
const startTimeHeader = screen.getByText('appLog.table.header.startTime')
await user.click(startTimeHeader)
// Arrow should rotate (indicated by class change)
// The sort icon should have rotate-180 class for ascending
const sortIcon = startTimeHeader.closest('div')?.querySelector('svg')
expect(sortIcon).toBeInTheDocument()
})
it('should render sort arrow icon', () => {
const logs = createMockLogsResponse([createMockWorkflowLog()])
const { container } = render(
<WorkflowAppLogList logs={logs} appDetail={createMockApp()} onRefresh={defaultOnRefresh} />,
)
// Check for ArrowDownIcon presence
const sortArrow = container.querySelector('svg.ml-0\\.5')
expect(sortArrow).toBeInTheDocument()
})
})
// --------------------------------------------------------------------------
// Drawer Tests
// --------------------------------------------------------------------------
describe('Drawer', () => {
it('should open drawer when clicking on a log row', async () => {
const user = userEvent.setup()
useAppStore.setState({ appDetail: createMockApp({ id: 'app-123' }) })
const logs = createMockLogsResponse([
createMockWorkflowLog({
id: 'log-1',
workflow_run: createMockWorkflowRun({ id: 'run-456' }),
}),
])
render(
<WorkflowAppLogList logs={logs} appDetail={createMockApp()} onRefresh={defaultOnRefresh} />,
)
const dataRows = screen.getAllByRole('row')
await user.click(dataRows[1]) // Click first data row
const dialog = await screen.findByRole('dialog')
expect(dialog).toBeInTheDocument()
expect(screen.getByText('appLog.runDetail.workflowTitle')).toBeInTheDocument()
})
it('should close drawer and call onRefresh when closing', async () => {
const user = userEvent.setup()
const onRefresh = jest.fn()
useAppStore.setState({ appDetail: createMockApp() })
const logs = createMockLogsResponse([createMockWorkflowLog()])
render(
<WorkflowAppLogList logs={logs} appDetail={createMockApp()} onRefresh={onRefresh} />,
)
// Open drawer
const dataRows = screen.getAllByRole('row')
await user.click(dataRows[1])
await screen.findByRole('dialog')
// Close drawer using Escape key
await user.keyboard('{Escape}')
await waitFor(() => {
expect(onRefresh).toHaveBeenCalled()
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
})
})
it('should highlight selected row', async () => {
const user = userEvent.setup()
const logs = createMockLogsResponse([createMockWorkflowLog()])
render(
<WorkflowAppLogList logs={logs} appDetail={createMockApp()} onRefresh={defaultOnRefresh} />,
)
const dataRows = screen.getAllByRole('row')
const dataRow = dataRows[1]
// Before click - no highlight
expect(dataRow).not.toHaveClass('bg-background-default-hover')
// After click - has highlight (via currentLog state)
await user.click(dataRow)
// The row should have the selected class
expect(dataRow).toHaveClass('bg-background-default-hover')
})
})
// --------------------------------------------------------------------------
// Replay Functionality Tests
// --------------------------------------------------------------------------
describe('Replay Functionality', () => {
it('should allow replay when triggered from app-run', async () => {
const user = userEvent.setup()
useAppStore.setState({ appDetail: createMockApp({ id: 'app-replay' }) })
const logs = createMockLogsResponse([
createMockWorkflowLog({
workflow_run: createMockWorkflowRun({
id: 'run-to-replay',
triggered_from: WorkflowRunTriggeredFrom.APP_RUN,
}),
}),
])
render(
<WorkflowAppLogList logs={logs} appDetail={createMockApp()} onRefresh={defaultOnRefresh} />,
)
// Open drawer
const dataRows = screen.getAllByRole('row')
await user.click(dataRows[1])
await screen.findByRole('dialog')
// Replay button should be present for app-run triggers
const replayButton = screen.getByRole('button', { name: 'appLog.runDetail.testWithParams' })
await user.click(replayButton)
expect(mockRouterPush).toHaveBeenCalledWith('/app/app-replay/workflow?replayRunId=run-to-replay')
})
it('should allow replay when triggered from debugging', async () => {
const user = userEvent.setup()
useAppStore.setState({ appDetail: createMockApp({ id: 'app-debug' }) })
const logs = createMockLogsResponse([
createMockWorkflowLog({
workflow_run: createMockWorkflowRun({
id: 'debug-run',
triggered_from: WorkflowRunTriggeredFrom.DEBUGGING,
}),
}),
])
render(
<WorkflowAppLogList logs={logs} appDetail={createMockApp()} onRefresh={defaultOnRefresh} />,
)
// Open drawer
const dataRows = screen.getAllByRole('row')
await user.click(dataRows[1])
await screen.findByRole('dialog')
// Replay button should be present for debugging triggers
const replayButton = screen.getByRole('button', { name: 'appLog.runDetail.testWithParams' })
expect(replayButton).toBeInTheDocument()
})
it('should not show replay for webhook triggers', async () => {
const user = userEvent.setup()
useAppStore.setState({ appDetail: createMockApp({ id: 'app-webhook' }) })
const logs = createMockLogsResponse([
createMockWorkflowLog({
workflow_run: createMockWorkflowRun({
id: 'webhook-run',
triggered_from: WorkflowRunTriggeredFrom.WEBHOOK,
}),
}),
])
render(
<WorkflowAppLogList logs={logs} appDetail={createMockApp()} onRefresh={defaultOnRefresh} />,
)
// Open drawer
const dataRows = screen.getAllByRole('row')
await user.click(dataRows[1])
await screen.findByRole('dialog')
// Replay button should not be present for webhook triggers
expect(screen.queryByRole('button', { name: 'appLog.runDetail.testWithParams' })).not.toBeInTheDocument()
})
})
// --------------------------------------------------------------------------
// Unread Indicator Tests
// --------------------------------------------------------------------------
describe('Unread Indicator', () => {
it('should show unread indicator for unread logs', () => {
const logs = createMockLogsResponse([
createMockWorkflowLog({
read_at: undefined,
}),
])
const { container } = render(
<WorkflowAppLogList logs={logs} appDetail={createMockApp()} onRefresh={defaultOnRefresh} />,
)
// Unread indicator is a small blue dot
const unreadDot = container.querySelector('.bg-util-colors-blue-blue-500')
expect(unreadDot).toBeInTheDocument()
})
it('should not show unread indicator for read logs', () => {
const logs = createMockLogsResponse([
createMockWorkflowLog({
read_at: Date.now(),
}),
])
const { container } = render(
<WorkflowAppLogList logs={logs} appDetail={createMockApp()} onRefresh={defaultOnRefresh} />,
)
// No unread indicator
const unreadDot = container.querySelector('.bg-util-colors-blue-blue-500')
expect(unreadDot).not.toBeInTheDocument()
})
})
// --------------------------------------------------------------------------
// Runtime Display Tests
// --------------------------------------------------------------------------
describe('Runtime Display', () => {
it('should display elapsed time with 3 decimal places', () => {
const logs = createMockLogsResponse([
createMockWorkflowLog({
workflow_run: createMockWorkflowRun({ elapsed_time: 1.23456 }),
}),
])
render(
<WorkflowAppLogList logs={logs} appDetail={createMockApp()} onRefresh={defaultOnRefresh} />,
)
expect(screen.getByText('1.235s')).toBeInTheDocument()
})
it('should display 0 elapsed time with special styling', () => {
const logs = createMockLogsResponse([
createMockWorkflowLog({
workflow_run: createMockWorkflowRun({ elapsed_time: 0 }),
}),
])
render(
<WorkflowAppLogList logs={logs} appDetail={createMockApp()} onRefresh={defaultOnRefresh} />,
)
const zeroTime = screen.getByText('0.000s')
expect(zeroTime).toBeInTheDocument()
expect(zeroTime).toHaveClass('text-text-quaternary')
})
})
// --------------------------------------------------------------------------
// Token Display Tests
// --------------------------------------------------------------------------
describe('Token Display', () => {
it('should display total tokens', () => {
const logs = createMockLogsResponse([
createMockWorkflowLog({
workflow_run: createMockWorkflowRun({ total_tokens: 12345 }),
}),
])
render(
<WorkflowAppLogList logs={logs} appDetail={createMockApp()} onRefresh={defaultOnRefresh} />,
)
expect(screen.getByText('12345')).toBeInTheDocument()
})
})
// --------------------------------------------------------------------------
// Empty State Tests
// --------------------------------------------------------------------------
describe('Empty State', () => {
it('should render empty table when logs data is empty', () => {
const logs = createMockLogsResponse([])
render(
<WorkflowAppLogList logs={logs} appDetail={createMockApp()} onRefresh={defaultOnRefresh} />,
)
const table = screen.getByRole('table')
expect(table).toBeInTheDocument()
// Should only have header row
const rows = screen.getAllByRole('row')
expect(rows).toHaveLength(1)
})
})
// --------------------------------------------------------------------------
// Edge Cases (REQUIRED)
// --------------------------------------------------------------------------
describe('Edge Cases', () => {
it('should handle multiple logs correctly', () => {
const logs = createMockLogsResponse([
createMockWorkflowLog({ id: 'log-1', created_at: 1000 }),
createMockWorkflowLog({ id: 'log-2', created_at: 2000 }),
createMockWorkflowLog({ id: 'log-3', created_at: 3000 }),
])
render(
<WorkflowAppLogList logs={logs} appDetail={createMockApp()} onRefresh={defaultOnRefresh} />,
)
const rows = screen.getAllByRole('row')
expect(rows).toHaveLength(4) // 1 header + 3 data rows
})
it('should handle logs with missing workflow_run data gracefully', () => {
const logs = createMockLogsResponse([
createMockWorkflowLog({
workflow_run: createMockWorkflowRun({
elapsed_time: 0,
total_tokens: 0,
}),
}),
])
render(
<WorkflowAppLogList logs={logs} appDetail={createMockApp()} onRefresh={defaultOnRefresh} />,
)
expect(screen.getByText('0.000s')).toBeInTheDocument()
expect(screen.getByText('0')).toBeInTheDocument()
})
it('should handle null workflow_run.triggered_from for non-workflow apps', () => {
const logs = createMockLogsResponse([
createMockWorkflowLog({
workflow_run: createMockWorkflowRun({
triggered_from: undefined as any,
}),
}),
])
const chatApp = createMockApp({ mode: 'advanced-chat' as AppModeEnum })
render(
<WorkflowAppLogList logs={logs} appDetail={chatApp} onRefresh={defaultOnRefresh} />,
)
// Should render without trigger column
expect(screen.queryByText('appLog.table.header.triggered_from')).not.toBeInTheDocument()
})
})
})

View File

@ -0,0 +1,377 @@
/**
* TriggerByDisplay Component Tests
*
* Tests the display of workflow trigger sources with appropriate icons and labels.
* Covers all trigger types: app-run, debugging, webhook, schedule, plugin, rag-pipeline.
*/
import { render, screen } from '@testing-library/react'
import TriggerByDisplay from './trigger-by-display'
import { WorkflowRunTriggeredFrom } from '@/models/log'
import type { TriggerMetadata } from '@/models/log'
import { Theme } from '@/types/app'
// ============================================================================
// Mocks
// ============================================================================
jest.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}))
let mockTheme = Theme.light
jest.mock('@/hooks/use-theme', () => ({
__esModule: true,
default: () => ({ theme: mockTheme }),
}))
// Mock BlockIcon as it has complex dependencies
jest.mock('@/app/components/workflow/block-icon', () => ({
__esModule: true,
default: ({ type, toolIcon }: { type: string; toolIcon?: string }) => (
<div data-testid="block-icon" data-type={type} data-tool-icon={toolIcon || ''}>
BlockIcon
</div>
),
}))
// ============================================================================
// Test Data Factories
// ============================================================================
const createTriggerMetadata = (overrides: Partial<TriggerMetadata> = {}): TriggerMetadata => ({
...overrides,
})
// ============================================================================
// Tests
// ============================================================================
describe('TriggerByDisplay', () => {
beforeEach(() => {
jest.clearAllMocks()
mockTheme = Theme.light
})
// --------------------------------------------------------------------------
// Rendering Tests (REQUIRED)
// --------------------------------------------------------------------------
describe('Rendering', () => {
it('should render without crashing', () => {
render(<TriggerByDisplay triggeredFrom={WorkflowRunTriggeredFrom.APP_RUN} />)
expect(screen.getByText('appLog.triggerBy.appRun')).toBeInTheDocument()
})
it('should render icon container', () => {
const { container } = render(
<TriggerByDisplay triggeredFrom={WorkflowRunTriggeredFrom.APP_RUN} />,
)
// Should have icon container with flex layout
const iconContainer = container.querySelector('.flex.items-center.justify-center')
expect(iconContainer).toBeInTheDocument()
})
})
// --------------------------------------------------------------------------
// Props Tests (REQUIRED)
// --------------------------------------------------------------------------
describe('Props', () => {
it('should apply custom className', () => {
const { container } = render(
<TriggerByDisplay
triggeredFrom={WorkflowRunTriggeredFrom.APP_RUN}
className="custom-class"
/>,
)
const wrapper = container.firstChild as HTMLElement
expect(wrapper).toHaveClass('custom-class')
})
it('should show text by default (showText defaults to true)', () => {
render(<TriggerByDisplay triggeredFrom={WorkflowRunTriggeredFrom.APP_RUN} />)
expect(screen.getByText('appLog.triggerBy.appRun')).toBeInTheDocument()
})
it('should hide text when showText is false', () => {
render(
<TriggerByDisplay
triggeredFrom={WorkflowRunTriggeredFrom.APP_RUN}
showText={false}
/>,
)
expect(screen.queryByText('appLog.triggerBy.appRun')).not.toBeInTheDocument()
})
})
// --------------------------------------------------------------------------
// Trigger Type Display Tests
// --------------------------------------------------------------------------
describe('Trigger Types', () => {
it('should display app-run trigger correctly', () => {
render(<TriggerByDisplay triggeredFrom={WorkflowRunTriggeredFrom.APP_RUN} />)
expect(screen.getByText('appLog.triggerBy.appRun')).toBeInTheDocument()
})
it('should display debugging trigger correctly', () => {
render(<TriggerByDisplay triggeredFrom={WorkflowRunTriggeredFrom.DEBUGGING} />)
expect(screen.getByText('appLog.triggerBy.debugging')).toBeInTheDocument()
})
it('should display webhook trigger correctly', () => {
render(<TriggerByDisplay triggeredFrom={WorkflowRunTriggeredFrom.WEBHOOK} />)
expect(screen.getByText('appLog.triggerBy.webhook')).toBeInTheDocument()
})
it('should display schedule trigger correctly', () => {
render(<TriggerByDisplay triggeredFrom={WorkflowRunTriggeredFrom.SCHEDULE} />)
expect(screen.getByText('appLog.triggerBy.schedule')).toBeInTheDocument()
})
it('should display plugin trigger correctly', () => {
render(<TriggerByDisplay triggeredFrom={WorkflowRunTriggeredFrom.PLUGIN} />)
expect(screen.getByText('appLog.triggerBy.plugin')).toBeInTheDocument()
})
it('should display rag-pipeline-run trigger correctly', () => {
render(<TriggerByDisplay triggeredFrom={WorkflowRunTriggeredFrom.RAG_PIPELINE_RUN} />)
expect(screen.getByText('appLog.triggerBy.ragPipelineRun')).toBeInTheDocument()
})
it('should display rag-pipeline-debugging trigger correctly', () => {
render(<TriggerByDisplay triggeredFrom={WorkflowRunTriggeredFrom.RAG_PIPELINE_DEBUGGING} />)
expect(screen.getByText('appLog.triggerBy.ragPipelineDebugging')).toBeInTheDocument()
})
})
// --------------------------------------------------------------------------
// Plugin Metadata Tests
// --------------------------------------------------------------------------
describe('Plugin Metadata', () => {
it('should display custom event name from plugin metadata', () => {
const metadata = createTriggerMetadata({ event_name: 'Custom Plugin Event' })
render(
<TriggerByDisplay
triggeredFrom={WorkflowRunTriggeredFrom.PLUGIN}
triggerMetadata={metadata}
/>,
)
expect(screen.getByText('Custom Plugin Event')).toBeInTheDocument()
})
it('should fallback to default plugin text when no event_name', () => {
const metadata = createTriggerMetadata({})
render(
<TriggerByDisplay
triggeredFrom={WorkflowRunTriggeredFrom.PLUGIN}
triggerMetadata={metadata}
/>,
)
expect(screen.getByText('appLog.triggerBy.plugin')).toBeInTheDocument()
})
it('should use plugin icon from metadata in light theme', () => {
mockTheme = Theme.light
const metadata = createTriggerMetadata({ icon: 'light-icon.png', icon_dark: 'dark-icon.png' })
render(
<TriggerByDisplay
triggeredFrom={WorkflowRunTriggeredFrom.PLUGIN}
triggerMetadata={metadata}
/>,
)
const blockIcon = screen.getByTestId('block-icon')
expect(blockIcon).toHaveAttribute('data-tool-icon', 'light-icon.png')
})
it('should use dark plugin icon in dark theme', () => {
mockTheme = Theme.dark
const metadata = createTriggerMetadata({ icon: 'light-icon.png', icon_dark: 'dark-icon.png' })
render(
<TriggerByDisplay
triggeredFrom={WorkflowRunTriggeredFrom.PLUGIN}
triggerMetadata={metadata}
/>,
)
const blockIcon = screen.getByTestId('block-icon')
expect(blockIcon).toHaveAttribute('data-tool-icon', 'dark-icon.png')
})
it('should fallback to light icon when dark icon not available in dark theme', () => {
mockTheme = Theme.dark
const metadata = createTriggerMetadata({ icon: 'light-icon.png' })
render(
<TriggerByDisplay
triggeredFrom={WorkflowRunTriggeredFrom.PLUGIN}
triggerMetadata={metadata}
/>,
)
const blockIcon = screen.getByTestId('block-icon')
expect(blockIcon).toHaveAttribute('data-tool-icon', 'light-icon.png')
})
it('should use default BlockIcon when plugin has no icon metadata', () => {
const metadata = createTriggerMetadata({})
render(
<TriggerByDisplay
triggeredFrom={WorkflowRunTriggeredFrom.PLUGIN}
triggerMetadata={metadata}
/>,
)
const blockIcon = screen.getByTestId('block-icon')
expect(blockIcon).toHaveAttribute('data-tool-icon', '')
})
})
// --------------------------------------------------------------------------
// Icon Rendering Tests
// --------------------------------------------------------------------------
describe('Icon Rendering', () => {
it('should render WindowCursor icon for app-run trigger', () => {
const { container } = render(
<TriggerByDisplay triggeredFrom={WorkflowRunTriggeredFrom.APP_RUN} />,
)
// Check for the blue brand background used for app-run icon
const iconWrapper = container.querySelector('.bg-util-colors-blue-brand-blue-brand-500')
expect(iconWrapper).toBeInTheDocument()
})
it('should render Code icon for debugging trigger', () => {
const { container } = render(
<TriggerByDisplay triggeredFrom={WorkflowRunTriggeredFrom.DEBUGGING} />,
)
// Check for the blue background used for debugging icon
const iconWrapper = container.querySelector('.bg-util-colors-blue-blue-500')
expect(iconWrapper).toBeInTheDocument()
})
it('should render WebhookLine icon for webhook trigger', () => {
const { container } = render(
<TriggerByDisplay triggeredFrom={WorkflowRunTriggeredFrom.WEBHOOK} />,
)
// Check for the blue background used for webhook icon
const iconWrapper = container.querySelector('.bg-util-colors-blue-blue-500')
expect(iconWrapper).toBeInTheDocument()
})
it('should render Schedule icon for schedule trigger', () => {
const { container } = render(
<TriggerByDisplay triggeredFrom={WorkflowRunTriggeredFrom.SCHEDULE} />,
)
// Check for the violet background used for schedule icon
const iconWrapper = container.querySelector('.bg-util-colors-violet-violet-500')
expect(iconWrapper).toBeInTheDocument()
})
it('should render KnowledgeRetrieval icon for rag-pipeline triggers', () => {
const { container } = render(
<TriggerByDisplay triggeredFrom={WorkflowRunTriggeredFrom.RAG_PIPELINE_RUN} />,
)
// Check for the green background used for rag pipeline icon
const iconWrapper = container.querySelector('.bg-util-colors-green-green-500')
expect(iconWrapper).toBeInTheDocument()
})
})
// --------------------------------------------------------------------------
// Edge Cases (REQUIRED)
// --------------------------------------------------------------------------
describe('Edge Cases', () => {
it('should handle unknown trigger type gracefully', () => {
// Test with a type cast to simulate unknown trigger type
render(<TriggerByDisplay triggeredFrom={'unknown-type' as WorkflowRunTriggeredFrom} />)
// Should fallback to default (app-run) icon styling
expect(screen.getByText('unknown-type')).toBeInTheDocument()
})
it('should handle undefined triggerMetadata', () => {
render(
<TriggerByDisplay
triggeredFrom={WorkflowRunTriggeredFrom.PLUGIN}
triggerMetadata={undefined}
/>,
)
expect(screen.getByText('appLog.triggerBy.plugin')).toBeInTheDocument()
})
it('should handle empty className', () => {
const { container } = render(
<TriggerByDisplay
triggeredFrom={WorkflowRunTriggeredFrom.APP_RUN}
className=""
/>,
)
const wrapper = container.firstChild as HTMLElement
expect(wrapper).toHaveClass('flex', 'items-center', 'gap-1.5')
})
it('should render correctly when both showText is false and metadata is provided', () => {
const metadata = createTriggerMetadata({ event_name: 'Test Event' })
render(
<TriggerByDisplay
triggeredFrom={WorkflowRunTriggeredFrom.PLUGIN}
triggerMetadata={metadata}
showText={false}
/>,
)
// Text should not be visible even with metadata
expect(screen.queryByText('Test Event')).not.toBeInTheDocument()
expect(screen.queryByText('appLog.triggerBy.plugin')).not.toBeInTheDocument()
})
})
// --------------------------------------------------------------------------
// Theme Switching Tests
// --------------------------------------------------------------------------
describe('Theme Switching', () => {
it('should render correctly in light theme', () => {
mockTheme = Theme.light
render(<TriggerByDisplay triggeredFrom={WorkflowRunTriggeredFrom.APP_RUN} />)
expect(screen.getByText('appLog.triggerBy.appRun')).toBeInTheDocument()
})
it('should render correctly in dark theme', () => {
mockTheme = Theme.dark
render(<TriggerByDisplay triggeredFrom={WorkflowRunTriggeredFrom.APP_RUN} />)
expect(screen.getByText('appLog.triggerBy.appRun')).toBeInTheDocument()
})
})
})