mirror of https://github.com/langgenius/dify.git
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:
parent
23f75a1185
commit
103a5e0122
|
|
@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -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
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
Reference in New Issue