fix webtest

This commit is contained in:
hjlarry 2026-01-18 17:27:29 +08:00
parent 511df81201
commit 1fb6d1286f
3 changed files with 86 additions and 56 deletions

View File

@ -1,3 +1,4 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { act, fireEvent, render, screen } from '@testing-library/react' import { act, fireEvent, render, screen } from '@testing-library/react'
import * as React from 'react' import * as React from 'react'
import { AppModeEnum } from '@/types/app' import { AppModeEnum } from '@/types/app'
@ -123,6 +124,10 @@ vi.mock('@/service/use-apps', () => ({
}), }),
})) }))
vi.mock('@/service/apps', () => ({
fetchWorkflowOnlineUsers: vi.fn().mockResolvedValue({}),
}))
// Mock tag store // Mock tag store
vi.mock('@/app/components/base/tag-management/store', () => ({ vi.mock('@/app/components/base/tag-management/store', () => ({
useStore: (selector: (state: { tagList: any[], setTagList: any, showTagManagementModal: boolean, setShowTagManagementModal: any }) => any) => { useStore: (selector: (state: { tagList: any[], setTagList: any, showTagManagementModal: boolean, setShowTagManagementModal: any }) => any) => {
@ -244,6 +249,21 @@ beforeAll(() => {
} as unknown as typeof IntersectionObserver } as unknown as typeof IntersectionObserver
}) })
const renderList = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
})
return render(
<QueryClientProvider client={queryClient}>
<List />
</QueryClientProvider>,
)
}
describe('List', () => { describe('List', () => {
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks() vi.clearAllMocks()
@ -265,13 +285,13 @@ describe('List', () => {
describe('Rendering', () => { describe('Rendering', () => {
it('should render without crashing', () => { it('should render without crashing', () => {
render(<List />) renderList()
// Tab slider renders app type tabs // Tab slider renders app type tabs
expect(screen.getByText('app.types.all')).toBeInTheDocument() expect(screen.getByText('app.types.all')).toBeInTheDocument()
}) })
it('should render tab slider with all app types', () => { it('should render tab slider with all app types', () => {
render(<List />) renderList()
expect(screen.getByText('app.types.all')).toBeInTheDocument() expect(screen.getByText('app.types.all')).toBeInTheDocument()
expect(screen.getByText('app.types.workflow')).toBeInTheDocument() expect(screen.getByText('app.types.workflow')).toBeInTheDocument()
@ -282,48 +302,48 @@ describe('List', () => {
}) })
it('should render search input', () => { it('should render search input', () => {
render(<List />) renderList()
// Input component renders a searchbox // Input component renders a searchbox
expect(screen.getByRole('textbox')).toBeInTheDocument() expect(screen.getByRole('textbox')).toBeInTheDocument()
}) })
it('should render tag filter', () => { it('should render tag filter', () => {
render(<List />) renderList()
// Tag filter renders with placeholder text // Tag filter renders with placeholder text
expect(screen.getByText('common.tag.placeholder')).toBeInTheDocument() expect(screen.getByText('common.tag.placeholder')).toBeInTheDocument()
}) })
it('should render created by me checkbox', () => { it('should render created by me checkbox', () => {
render(<List />) renderList()
expect(screen.getByText('app.showMyCreatedAppsOnly')).toBeInTheDocument() expect(screen.getByText('app.showMyCreatedAppsOnly')).toBeInTheDocument()
}) })
it('should render app cards when apps exist', () => { it('should render app cards when apps exist', () => {
render(<List />) renderList()
expect(screen.getByTestId('app-card-app-1')).toBeInTheDocument() expect(screen.getByTestId('app-card-app-1')).toBeInTheDocument()
expect(screen.getByTestId('app-card-app-2')).toBeInTheDocument() expect(screen.getByTestId('app-card-app-2')).toBeInTheDocument()
}) })
it('should render new app card for editors', () => { it('should render new app card for editors', () => {
render(<List />) renderList()
expect(screen.getByTestId('new-app-card')).toBeInTheDocument() expect(screen.getByTestId('new-app-card')).toBeInTheDocument()
}) })
it('should render footer when branding is disabled', () => { it('should render footer when branding is disabled', () => {
render(<List />) renderList()
expect(screen.getByTestId('footer')).toBeInTheDocument() expect(screen.getByTestId('footer')).toBeInTheDocument()
}) })
it('should render drop DSL hint for editors', () => { it('should render drop DSL hint for editors', () => {
render(<List />) renderList()
expect(screen.getByText('app.newApp.dropDSLToCreateApp')).toBeInTheDocument() expect(screen.getByText('app.newApp.dropDSLToCreateApp')).toBeInTheDocument()
}) })
}) })
describe('Tab Navigation', () => { describe('Tab Navigation', () => {
it('should call setActiveTab when tab is clicked', () => { it('should call setActiveTab when tab is clicked', () => {
render(<List />) renderList()
fireEvent.click(screen.getByText('app.types.workflow')) fireEvent.click(screen.getByText('app.types.workflow'))
@ -331,7 +351,7 @@ describe('List', () => {
}) })
it('should call setActiveTab for all tab', () => { it('should call setActiveTab for all tab', () => {
render(<List />) renderList()
fireEvent.click(screen.getByText('app.types.all')) fireEvent.click(screen.getByText('app.types.all'))
@ -341,12 +361,12 @@ describe('List', () => {
describe('Search Functionality', () => { describe('Search Functionality', () => {
it('should render search input field', () => { it('should render search input field', () => {
render(<List />) renderList()
expect(screen.getByRole('textbox')).toBeInTheDocument() expect(screen.getByRole('textbox')).toBeInTheDocument()
}) })
it('should handle search input change', () => { it('should handle search input change', () => {
render(<List />) renderList()
const input = screen.getByRole('textbox') const input = screen.getByRole('textbox')
fireEvent.change(input, { target: { value: 'test search' } }) fireEvent.change(input, { target: { value: 'test search' } })
@ -355,7 +375,7 @@ describe('List', () => {
}) })
it('should handle search input interaction', () => { it('should handle search input interaction', () => {
render(<List />) renderList()
const input = screen.getByRole('textbox') const input = screen.getByRole('textbox')
expect(input).toBeInTheDocument() expect(input).toBeInTheDocument()
@ -365,7 +385,7 @@ describe('List', () => {
// Set initial keywords to make clear button visible // Set initial keywords to make clear button visible
mockQueryState.keywords = 'existing search' mockQueryState.keywords = 'existing search'
render(<List />) renderList()
// Find and click clear button (Input component uses .group class for clear icon container) // Find and click clear button (Input component uses .group class for clear icon container)
const clearButton = document.querySelector('.group') const clearButton = document.querySelector('.group')
@ -380,12 +400,12 @@ describe('List', () => {
describe('Tag Filter', () => { describe('Tag Filter', () => {
it('should render tag filter component', () => { it('should render tag filter component', () => {
render(<List />) renderList()
expect(screen.getByText('common.tag.placeholder')).toBeInTheDocument() expect(screen.getByText('common.tag.placeholder')).toBeInTheDocument()
}) })
it('should render tag filter with placeholder', () => { it('should render tag filter with placeholder', () => {
render(<List />) renderList()
// Tag filter is rendered // Tag filter is rendered
expect(screen.getByText('common.tag.placeholder')).toBeInTheDocument() expect(screen.getByText('common.tag.placeholder')).toBeInTheDocument()
@ -394,12 +414,12 @@ describe('List', () => {
describe('Created By Me Filter', () => { describe('Created By Me Filter', () => {
it('should render checkbox with correct label', () => { it('should render checkbox with correct label', () => {
render(<List />) renderList()
expect(screen.getByText('app.showMyCreatedAppsOnly')).toBeInTheDocument() expect(screen.getByText('app.showMyCreatedAppsOnly')).toBeInTheDocument()
}) })
it('should handle checkbox change', () => { it('should handle checkbox change', () => {
render(<List />) renderList()
// Checkbox component uses data-testid="checkbox-{id}" // Checkbox component uses data-testid="checkbox-{id}"
// CheckboxWithLabel doesn't pass testId, so id is undefined // CheckboxWithLabel doesn't pass testId, so id is undefined
@ -414,7 +434,7 @@ describe('List', () => {
it('should not render new app card for non-editors', () => { it('should not render new app card for non-editors', () => {
mockIsCurrentWorkspaceEditor.mockReturnValue(false) mockIsCurrentWorkspaceEditor.mockReturnValue(false)
render(<List />) renderList()
expect(screen.queryByTestId('new-app-card')).not.toBeInTheDocument() expect(screen.queryByTestId('new-app-card')).not.toBeInTheDocument()
}) })
@ -422,7 +442,7 @@ describe('List', () => {
it('should not render drop DSL hint for non-editors', () => { it('should not render drop DSL hint for non-editors', () => {
mockIsCurrentWorkspaceEditor.mockReturnValue(false) mockIsCurrentWorkspaceEditor.mockReturnValue(false)
render(<List />) renderList()
expect(screen.queryByText(/drop dsl file to create app/i)).not.toBeInTheDocument() expect(screen.queryByText(/drop dsl file to create app/i)).not.toBeInTheDocument()
}) })
@ -432,7 +452,7 @@ describe('List', () => {
it('should redirect dataset operators to datasets page', () => { it('should redirect dataset operators to datasets page', () => {
mockIsCurrentWorkspaceDatasetOperator.mockReturnValue(true) mockIsCurrentWorkspaceDatasetOperator.mockReturnValue(true)
render(<List />) renderList()
expect(mockReplace).toHaveBeenCalledWith('/datasets') expect(mockReplace).toHaveBeenCalledWith('/datasets')
}) })
@ -442,7 +462,7 @@ describe('List', () => {
it('should call refetch when refresh key is set in localStorage', () => { it('should call refetch when refresh key is set in localStorage', () => {
localStorage.setItem('needRefreshAppList', '1') localStorage.setItem('needRefreshAppList', '1')
render(<List />) renderList()
expect(mockRefetch).toHaveBeenCalled() expect(mockRefetch).toHaveBeenCalled()
expect(localStorage.getItem('needRefreshAppList')).toBeNull() expect(localStorage.getItem('needRefreshAppList')).toBeNull()
@ -451,22 +471,23 @@ describe('List', () => {
describe('Edge Cases', () => { describe('Edge Cases', () => {
it('should handle multiple renders without issues', () => { it('should handle multiple renders without issues', () => {
const { rerender } = render(<List />) const { unmount } = renderList()
expect(screen.getByText('app.types.all')).toBeInTheDocument() expect(screen.getByText('app.types.all')).toBeInTheDocument()
rerender(<List />) unmount()
renderList()
expect(screen.getByText('app.types.all')).toBeInTheDocument() expect(screen.getByText('app.types.all')).toBeInTheDocument()
}) })
it('should render app cards correctly', () => { it('should render app cards correctly', () => {
render(<List />) renderList()
expect(screen.getByText('Test App 1')).toBeInTheDocument() expect(screen.getByText('Test App 1')).toBeInTheDocument()
expect(screen.getByText('Test App 2')).toBeInTheDocument() expect(screen.getByText('Test App 2')).toBeInTheDocument()
}) })
it('should render with all filter options visible', () => { it('should render with all filter options visible', () => {
render(<List />) renderList()
expect(screen.getByRole('textbox')).toBeInTheDocument() expect(screen.getByRole('textbox')).toBeInTheDocument()
expect(screen.getByText('common.tag.placeholder')).toBeInTheDocument() expect(screen.getByText('common.tag.placeholder')).toBeInTheDocument()
@ -476,14 +497,14 @@ describe('List', () => {
describe('Dragging State', () => { describe('Dragging State', () => {
it('should show drop hint when DSL feature is enabled for editors', () => { it('should show drop hint when DSL feature is enabled for editors', () => {
render(<List />) renderList()
expect(screen.getByText('app.newApp.dropDSLToCreateApp')).toBeInTheDocument() expect(screen.getByText('app.newApp.dropDSLToCreateApp')).toBeInTheDocument()
}) })
}) })
describe('App Type Tabs', () => { describe('App Type Tabs', () => {
it('should render all app type tabs', () => { it('should render all app type tabs', () => {
render(<List />) renderList()
expect(screen.getByText('app.types.all')).toBeInTheDocument() expect(screen.getByText('app.types.all')).toBeInTheDocument()
expect(screen.getByText('app.types.workflow')).toBeInTheDocument() expect(screen.getByText('app.types.workflow')).toBeInTheDocument()
@ -494,7 +515,7 @@ describe('List', () => {
}) })
it('should call setActiveTab for each app type', () => { it('should call setActiveTab for each app type', () => {
render(<List />) renderList()
const appTypeTexts = [ const appTypeTexts = [
{ mode: AppModeEnum.WORKFLOW, text: 'app.types.workflow' }, { mode: AppModeEnum.WORKFLOW, text: 'app.types.workflow' },
@ -513,7 +534,7 @@ describe('List', () => {
describe('Search and Filter Integration', () => { describe('Search and Filter Integration', () => {
it('should display search input with correct attributes', () => { it('should display search input with correct attributes', () => {
render(<List />) renderList()
const input = screen.getByRole('textbox') const input = screen.getByRole('textbox')
expect(input).toBeInTheDocument() expect(input).toBeInTheDocument()
@ -521,13 +542,13 @@ describe('List', () => {
}) })
it('should have tag filter component', () => { it('should have tag filter component', () => {
render(<List />) renderList()
expect(screen.getByText('common.tag.placeholder')).toBeInTheDocument() expect(screen.getByText('common.tag.placeholder')).toBeInTheDocument()
}) })
it('should display created by me label', () => { it('should display created by me label', () => {
render(<List />) renderList()
expect(screen.getByText('app.showMyCreatedAppsOnly')).toBeInTheDocument() expect(screen.getByText('app.showMyCreatedAppsOnly')).toBeInTheDocument()
}) })
@ -535,14 +556,14 @@ describe('List', () => {
describe('App List Display', () => { describe('App List Display', () => {
it('should display all app cards from data', () => { it('should display all app cards from data', () => {
render(<List />) renderList()
expect(screen.getByTestId('app-card-app-1')).toBeInTheDocument() expect(screen.getByTestId('app-card-app-1')).toBeInTheDocument()
expect(screen.getByTestId('app-card-app-2')).toBeInTheDocument() expect(screen.getByTestId('app-card-app-2')).toBeInTheDocument()
}) })
it('should display app names correctly', () => { it('should display app names correctly', () => {
render(<List />) renderList()
expect(screen.getByText('Test App 1')).toBeInTheDocument() expect(screen.getByText('Test App 1')).toBeInTheDocument()
expect(screen.getByText('Test App 2')).toBeInTheDocument() expect(screen.getByText('Test App 2')).toBeInTheDocument()
@ -551,7 +572,7 @@ describe('List', () => {
describe('Footer Visibility', () => { describe('Footer Visibility', () => {
it('should render footer when branding is disabled', () => { it('should render footer when branding is disabled', () => {
render(<List />) renderList()
expect(screen.getByTestId('footer')).toBeInTheDocument() expect(screen.getByTestId('footer')).toBeInTheDocument()
}) })
@ -563,14 +584,14 @@ describe('List', () => {
describe('Additional Coverage', () => { describe('Additional Coverage', () => {
it('should render dragging state overlay when dragging', () => { it('should render dragging state overlay when dragging', () => {
mockDragging = true mockDragging = true
const { container } = render(<List />) const { container } = renderList()
// Component should render successfully with dragging state // Component should render successfully with dragging state
expect(container).toBeInTheDocument() expect(container).toBeInTheDocument()
}) })
it('should handle app mode filter in query params', () => { it('should handle app mode filter in query params', () => {
render(<List />) renderList()
const workflowTab = screen.getByText('app.types.workflow') const workflowTab = screen.getByText('app.types.workflow')
fireEvent.click(workflowTab) fireEvent.click(workflowTab)
@ -579,7 +600,7 @@ describe('List', () => {
}) })
it('should render new app card for editors', () => { it('should render new app card for editors', () => {
render(<List />) renderList()
expect(screen.getByTestId('new-app-card')).toBeInTheDocument() expect(screen.getByTestId('new-app-card')).toBeInTheDocument()
}) })
@ -587,7 +608,7 @@ describe('List', () => {
describe('DSL File Drop', () => { describe('DSL File Drop', () => {
it('should handle DSL file drop and show modal', () => { it('should handle DSL file drop and show modal', () => {
render(<List />) renderList()
// Simulate DSL file drop via the callback // Simulate DSL file drop via the callback
const mockFile = new File(['test content'], 'test.yml', { type: 'application/yaml' }) const mockFile = new File(['test content'], 'test.yml', { type: 'application/yaml' })
@ -601,7 +622,7 @@ describe('List', () => {
}) })
it('should close DSL modal when onClose is called', () => { it('should close DSL modal when onClose is called', () => {
render(<List />) renderList()
// Open modal via DSL file drop // Open modal via DSL file drop
const mockFile = new File(['test content'], 'test.yml', { type: 'application/yaml' }) const mockFile = new File(['test content'], 'test.yml', { type: 'application/yaml' })
@ -619,7 +640,7 @@ describe('List', () => {
}) })
it('should close DSL modal and refetch when onSuccess is called', () => { it('should close DSL modal and refetch when onSuccess is called', () => {
render(<List />) renderList()
// Open modal via DSL file drop // Open modal via DSL file drop
const mockFile = new File(['test content'], 'test.yml', { type: 'application/yaml' }) const mockFile = new File(['test content'], 'test.yml', { type: 'application/yaml' })
@ -642,7 +663,7 @@ describe('List', () => {
describe('Tag Filter Change', () => { describe('Tag Filter Change', () => {
it('should handle tag filter value change', () => { it('should handle tag filter value change', () => {
vi.useFakeTimers() vi.useFakeTimers()
render(<List />) renderList()
// TagFilter component is rendered // TagFilter component is rendered
expect(screen.getByTestId('tag-filter')).toBeInTheDocument() expect(screen.getByTestId('tag-filter')).toBeInTheDocument()
@ -666,7 +687,7 @@ describe('List', () => {
it('should handle empty tag filter selection', () => { it('should handle empty tag filter selection', () => {
vi.useFakeTimers() vi.useFakeTimers()
render(<List />) renderList()
// Trigger tag filter change with empty array // Trigger tag filter change with empty array
act(() => { act(() => {
@ -688,7 +709,7 @@ describe('List', () => {
describe('Infinite Scroll', () => { describe('Infinite Scroll', () => {
it('should call fetchNextPage when intersection observer triggers', () => { it('should call fetchNextPage when intersection observer triggers', () => {
mockServiceState.hasNextPage = true mockServiceState.hasNextPage = true
render(<List />) renderList()
// Simulate intersection // Simulate intersection
if (intersectionCallback) { if (intersectionCallback) {
@ -705,7 +726,7 @@ describe('List', () => {
it('should not call fetchNextPage when not intersecting', () => { it('should not call fetchNextPage when not intersecting', () => {
mockServiceState.hasNextPage = true mockServiceState.hasNextPage = true
render(<List />) renderList()
// Simulate non-intersection // Simulate non-intersection
if (intersectionCallback) { if (intersectionCallback) {
@ -723,7 +744,7 @@ describe('List', () => {
it('should not call fetchNextPage when loading', () => { it('should not call fetchNextPage when loading', () => {
mockServiceState.hasNextPage = true mockServiceState.hasNextPage = true
mockServiceState.isLoading = true mockServiceState.isLoading = true
render(<List />) renderList()
if (intersectionCallback) { if (intersectionCallback) {
act(() => { act(() => {
@ -741,7 +762,7 @@ describe('List', () => {
describe('Error State', () => { describe('Error State', () => {
it('should handle error state in useEffect', () => { it('should handle error state in useEffect', () => {
mockServiceState.error = new Error('Test error') mockServiceState.error = new Error('Test error')
const { container } = render(<List />) const { container } = renderList()
// Component should still render // Component should still render
expect(container).toBeInTheDocument() expect(container).toBeInTheDocument()

View File

@ -35,12 +35,14 @@ describe('Avatar', () => {
it.each([ it.each([
{ size: undefined, expected: '30px', label: 'default (30px)' }, { size: undefined, expected: '30px', label: 'default (30px)' },
{ size: 50, expected: '50px', label: 'custom (50px)' }, { size: 50, expected: '50px', label: 'custom (50px)' },
])('should apply $label size to img element', ({ size, expected }) => { ])('should apply $label size to avatar container', ({ size, expected }) => {
const props = { name: 'Test', avatar: 'https://example.com/avatar.jpg', size } const props = { name: 'Test', avatar: 'https://example.com/avatar.jpg', size }
render(<Avatar {...props} />) render(<Avatar {...props} />)
expect(screen.getByRole('img')).toHaveStyle({ const img = screen.getByRole('img')
const wrapper = img.parentElement as HTMLElement
expect(wrapper).toHaveStyle({
width: expected, width: expected,
height: expected, height: expected,
fontSize: expected, fontSize: expected,
@ -60,7 +62,7 @@ describe('Avatar', () => {
}) })
describe('className prop', () => { describe('className prop', () => {
it('should merge className with default avatar classes on img', () => { it('should merge className with default avatar classes on container', () => {
const props = { const props = {
name: 'Test', name: 'Test',
avatar: 'https://example.com/avatar.jpg', avatar: 'https://example.com/avatar.jpg',
@ -70,8 +72,9 @@ describe('Avatar', () => {
render(<Avatar {...props} />) render(<Avatar {...props} />)
const img = screen.getByRole('img') const img = screen.getByRole('img')
expect(img).toHaveClass('custom-class') const wrapper = img.parentElement as HTMLElement
expect(img).toHaveClass('shrink-0', 'flex', 'items-center', 'rounded-full', 'bg-primary-600') expect(wrapper).toHaveClass('custom-class')
expect(wrapper).toHaveClass('shrink-0', 'flex', 'items-center', 'rounded-full', 'bg-primary-600')
}) })
it('should merge className with default avatar classes on fallback div', () => { it('should merge className with default avatar classes on fallback div', () => {
@ -277,10 +280,11 @@ describe('Avatar', () => {
render(<Avatar {...props} />) render(<Avatar {...props} />)
const img = screen.getByRole('img') const img = screen.getByRole('img')
const wrapper = img.parentElement as HTMLElement
expect(img).toHaveAttribute('alt', 'Test User') expect(img).toHaveAttribute('alt', 'Test User')
expect(img).toHaveAttribute('src', 'https://example.com/avatar.jpg') expect(img).toHaveAttribute('src', 'https://example.com/avatar.jpg')
expect(img).toHaveStyle({ width: '64px', height: '64px' }) expect(wrapper).toHaveStyle({ width: '64px', height: '64px' })
expect(img).toHaveClass('custom-avatar') expect(wrapper).toHaveClass('custom-avatar')
// Trigger load to verify onError callback // Trigger load to verify onError callback
fireEvent.load(img) fireEvent.load(img)

View File

@ -132,7 +132,12 @@ const createMockLocalStorage = () => {
} }
} }
let mockLocalStorage: ReturnType<typeof createMockLocalStorage> let mockLocalStorage: ReturnType<typeof createMockLocalStorage> = createMockLocalStorage()
Object.defineProperty(globalThis, 'localStorage', {
value: mockLocalStorage,
writable: true,
configurable: true,
})
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks() vi.clearAllMocks()