diff --git a/web/app/components/apps/list.spec.tsx b/web/app/components/apps/list.spec.tsx index 07c30cd588..b910cd73c8 100644 --- a/web/app/components/apps/list.spec.tsx +++ b/web/app/components/apps/list.spec.tsx @@ -1,3 +1,4 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { act, fireEvent, render, screen } from '@testing-library/react' import * as React from 'react' 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 vi.mock('@/app/components/base/tag-management/store', () => ({ useStore: (selector: (state: { tagList: any[], setTagList: any, showTagManagementModal: boolean, setShowTagManagementModal: any }) => any) => { @@ -244,6 +249,21 @@ beforeAll(() => { } as unknown as typeof IntersectionObserver }) +const renderList = () => { + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + return render( + + + , + ) +} + describe('List', () => { beforeEach(() => { vi.clearAllMocks() @@ -265,13 +285,13 @@ describe('List', () => { describe('Rendering', () => { it('should render without crashing', () => { - render() + renderList() // Tab slider renders app type tabs expect(screen.getByText('app.types.all')).toBeInTheDocument() }) it('should render tab slider with all app types', () => { - render() + renderList() expect(screen.getByText('app.types.all')).toBeInTheDocument() expect(screen.getByText('app.types.workflow')).toBeInTheDocument() @@ -282,48 +302,48 @@ describe('List', () => { }) it('should render search input', () => { - render() + renderList() // Input component renders a searchbox expect(screen.getByRole('textbox')).toBeInTheDocument() }) it('should render tag filter', () => { - render() + renderList() // Tag filter renders with placeholder text expect(screen.getByText('common.tag.placeholder')).toBeInTheDocument() }) it('should render created by me checkbox', () => { - render() + renderList() expect(screen.getByText('app.showMyCreatedAppsOnly')).toBeInTheDocument() }) it('should render app cards when apps exist', () => { - render() + renderList() expect(screen.getByTestId('app-card-app-1')).toBeInTheDocument() expect(screen.getByTestId('app-card-app-2')).toBeInTheDocument() }) it('should render new app card for editors', () => { - render() + renderList() expect(screen.getByTestId('new-app-card')).toBeInTheDocument() }) it('should render footer when branding is disabled', () => { - render() + renderList() expect(screen.getByTestId('footer')).toBeInTheDocument() }) it('should render drop DSL hint for editors', () => { - render() + renderList() expect(screen.getByText('app.newApp.dropDSLToCreateApp')).toBeInTheDocument() }) }) describe('Tab Navigation', () => { it('should call setActiveTab when tab is clicked', () => { - render() + renderList() fireEvent.click(screen.getByText('app.types.workflow')) @@ -331,7 +351,7 @@ describe('List', () => { }) it('should call setActiveTab for all tab', () => { - render() + renderList() fireEvent.click(screen.getByText('app.types.all')) @@ -341,12 +361,12 @@ describe('List', () => { describe('Search Functionality', () => { it('should render search input field', () => { - render() + renderList() expect(screen.getByRole('textbox')).toBeInTheDocument() }) it('should handle search input change', () => { - render() + renderList() const input = screen.getByRole('textbox') fireEvent.change(input, { target: { value: 'test search' } }) @@ -355,7 +375,7 @@ describe('List', () => { }) it('should handle search input interaction', () => { - render() + renderList() const input = screen.getByRole('textbox') expect(input).toBeInTheDocument() @@ -365,7 +385,7 @@ describe('List', () => { // Set initial keywords to make clear button visible mockQueryState.keywords = 'existing search' - render() + renderList() // Find and click clear button (Input component uses .group class for clear icon container) const clearButton = document.querySelector('.group') @@ -380,12 +400,12 @@ describe('List', () => { describe('Tag Filter', () => { it('should render tag filter component', () => { - render() + renderList() expect(screen.getByText('common.tag.placeholder')).toBeInTheDocument() }) it('should render tag filter with placeholder', () => { - render() + renderList() // Tag filter is rendered expect(screen.getByText('common.tag.placeholder')).toBeInTheDocument() @@ -394,12 +414,12 @@ describe('List', () => { describe('Created By Me Filter', () => { it('should render checkbox with correct label', () => { - render() + renderList() expect(screen.getByText('app.showMyCreatedAppsOnly')).toBeInTheDocument() }) it('should handle checkbox change', () => { - render() + renderList() // Checkbox component uses data-testid="checkbox-{id}" // 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', () => { mockIsCurrentWorkspaceEditor.mockReturnValue(false) - render() + renderList() expect(screen.queryByTestId('new-app-card')).not.toBeInTheDocument() }) @@ -422,7 +442,7 @@ describe('List', () => { it('should not render drop DSL hint for non-editors', () => { mockIsCurrentWorkspaceEditor.mockReturnValue(false) - render() + renderList() 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', () => { mockIsCurrentWorkspaceDatasetOperator.mockReturnValue(true) - render() + renderList() expect(mockReplace).toHaveBeenCalledWith('/datasets') }) @@ -442,7 +462,7 @@ describe('List', () => { it('should call refetch when refresh key is set in localStorage', () => { localStorage.setItem('needRefreshAppList', '1') - render() + renderList() expect(mockRefetch).toHaveBeenCalled() expect(localStorage.getItem('needRefreshAppList')).toBeNull() @@ -451,22 +471,23 @@ describe('List', () => { describe('Edge Cases', () => { it('should handle multiple renders without issues', () => { - const { rerender } = render() + const { unmount } = renderList() expect(screen.getByText('app.types.all')).toBeInTheDocument() - rerender() + unmount() + renderList() expect(screen.getByText('app.types.all')).toBeInTheDocument() }) it('should render app cards correctly', () => { - render() + renderList() expect(screen.getByText('Test App 1')).toBeInTheDocument() expect(screen.getByText('Test App 2')).toBeInTheDocument() }) it('should render with all filter options visible', () => { - render() + renderList() expect(screen.getByRole('textbox')).toBeInTheDocument() expect(screen.getByText('common.tag.placeholder')).toBeInTheDocument() @@ -476,14 +497,14 @@ describe('List', () => { describe('Dragging State', () => { it('should show drop hint when DSL feature is enabled for editors', () => { - render() + renderList() expect(screen.getByText('app.newApp.dropDSLToCreateApp')).toBeInTheDocument() }) }) describe('App Type Tabs', () => { it('should render all app type tabs', () => { - render() + renderList() expect(screen.getByText('app.types.all')).toBeInTheDocument() expect(screen.getByText('app.types.workflow')).toBeInTheDocument() @@ -494,7 +515,7 @@ describe('List', () => { }) it('should call setActiveTab for each app type', () => { - render() + renderList() const appTypeTexts = [ { mode: AppModeEnum.WORKFLOW, text: 'app.types.workflow' }, @@ -513,7 +534,7 @@ describe('List', () => { describe('Search and Filter Integration', () => { it('should display search input with correct attributes', () => { - render() + renderList() const input = screen.getByRole('textbox') expect(input).toBeInTheDocument() @@ -521,13 +542,13 @@ describe('List', () => { }) it('should have tag filter component', () => { - render() + renderList() expect(screen.getByText('common.tag.placeholder')).toBeInTheDocument() }) it('should display created by me label', () => { - render() + renderList() expect(screen.getByText('app.showMyCreatedAppsOnly')).toBeInTheDocument() }) @@ -535,14 +556,14 @@ describe('List', () => { describe('App List Display', () => { it('should display all app cards from data', () => { - render() + renderList() expect(screen.getByTestId('app-card-app-1')).toBeInTheDocument() expect(screen.getByTestId('app-card-app-2')).toBeInTheDocument() }) it('should display app names correctly', () => { - render() + renderList() expect(screen.getByText('Test App 1')).toBeInTheDocument() expect(screen.getByText('Test App 2')).toBeInTheDocument() @@ -551,7 +572,7 @@ describe('List', () => { describe('Footer Visibility', () => { it('should render footer when branding is disabled', () => { - render() + renderList() expect(screen.getByTestId('footer')).toBeInTheDocument() }) @@ -563,14 +584,14 @@ describe('List', () => { describe('Additional Coverage', () => { it('should render dragging state overlay when dragging', () => { mockDragging = true - const { container } = render() + const { container } = renderList() // Component should render successfully with dragging state expect(container).toBeInTheDocument() }) it('should handle app mode filter in query params', () => { - render() + renderList() const workflowTab = screen.getByText('app.types.workflow') fireEvent.click(workflowTab) @@ -579,7 +600,7 @@ describe('List', () => { }) it('should render new app card for editors', () => { - render() + renderList() expect(screen.getByTestId('new-app-card')).toBeInTheDocument() }) @@ -587,7 +608,7 @@ describe('List', () => { describe('DSL File Drop', () => { it('should handle DSL file drop and show modal', () => { - render() + renderList() // Simulate DSL file drop via the callback 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', () => { - render() + renderList() // Open modal via DSL file drop 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', () => { - render() + renderList() // Open modal via DSL file drop const mockFile = new File(['test content'], 'test.yml', { type: 'application/yaml' }) @@ -642,7 +663,7 @@ describe('List', () => { describe('Tag Filter Change', () => { it('should handle tag filter value change', () => { vi.useFakeTimers() - render() + renderList() // TagFilter component is rendered expect(screen.getByTestId('tag-filter')).toBeInTheDocument() @@ -666,7 +687,7 @@ describe('List', () => { it('should handle empty tag filter selection', () => { vi.useFakeTimers() - render() + renderList() // Trigger tag filter change with empty array act(() => { @@ -688,7 +709,7 @@ describe('List', () => { describe('Infinite Scroll', () => { it('should call fetchNextPage when intersection observer triggers', () => { mockServiceState.hasNextPage = true - render() + renderList() // Simulate intersection if (intersectionCallback) { @@ -705,7 +726,7 @@ describe('List', () => { it('should not call fetchNextPage when not intersecting', () => { mockServiceState.hasNextPage = true - render() + renderList() // Simulate non-intersection if (intersectionCallback) { @@ -723,7 +744,7 @@ describe('List', () => { it('should not call fetchNextPage when loading', () => { mockServiceState.hasNextPage = true mockServiceState.isLoading = true - render() + renderList() if (intersectionCallback) { act(() => { @@ -741,7 +762,7 @@ describe('List', () => { describe('Error State', () => { it('should handle error state in useEffect', () => { mockServiceState.error = new Error('Test error') - const { container } = render() + const { container } = renderList() // Component should still render expect(container).toBeInTheDocument() diff --git a/web/app/components/base/avatar/index.spec.tsx b/web/app/components/base/avatar/index.spec.tsx index e85690880b..a5b5f9e0fa 100644 --- a/web/app/components/base/avatar/index.spec.tsx +++ b/web/app/components/base/avatar/index.spec.tsx @@ -35,12 +35,14 @@ describe('Avatar', () => { it.each([ { size: undefined, expected: '30px', label: 'default (30px)' }, { 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 } render() - expect(screen.getByRole('img')).toHaveStyle({ + const img = screen.getByRole('img') + const wrapper = img.parentElement as HTMLElement + expect(wrapper).toHaveStyle({ width: expected, height: expected, fontSize: expected, @@ -60,7 +62,7 @@ describe('Avatar', () => { }) 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 = { name: 'Test', avatar: 'https://example.com/avatar.jpg', @@ -70,8 +72,9 @@ describe('Avatar', () => { render() const img = screen.getByRole('img') - expect(img).toHaveClass('custom-class') - expect(img).toHaveClass('shrink-0', 'flex', 'items-center', 'rounded-full', 'bg-primary-600') + const wrapper = img.parentElement as HTMLElement + 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', () => { @@ -277,10 +280,11 @@ describe('Avatar', () => { render() const img = screen.getByRole('img') + const wrapper = img.parentElement as HTMLElement expect(img).toHaveAttribute('alt', 'Test User') expect(img).toHaveAttribute('src', 'https://example.com/avatar.jpg') - expect(img).toHaveStyle({ width: '64px', height: '64px' }) - expect(img).toHaveClass('custom-avatar') + expect(wrapper).toHaveStyle({ width: '64px', height: '64px' }) + expect(wrapper).toHaveClass('custom-avatar') // Trigger load to verify onError callback fireEvent.load(img) diff --git a/web/vitest.setup.ts b/web/vitest.setup.ts index 597ded9559..ebec272ea3 100644 --- a/web/vitest.setup.ts +++ b/web/vitest.setup.ts @@ -132,7 +132,12 @@ const createMockLocalStorage = () => { } } -let mockLocalStorage: ReturnType +let mockLocalStorage: ReturnType = createMockLocalStorage() +Object.defineProperty(globalThis, 'localStorage', { + value: mockLocalStorage, + writable: true, + configurable: true, +}) beforeEach(() => { vi.clearAllMocks()