test: add integration tests for app card operations, list browsing, and create app flows (#32298)

Co-authored-by: CodingOnStar <hanxujiang@dify.com>
This commit is contained in:
Coding On Star
2026-02-13 13:21:09 +08:00
committed by GitHub
parent 84d090db33
commit a4e03d6284
17 changed files with 1509 additions and 359 deletions

View File

@@ -1,16 +1,13 @@
import type { Mock } from 'vitest'
import type { App } from '@/types/app'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import * as React from 'react'
import { AccessMode } from '@/models/access-control'
// Mock API services - import for direct manipulation
import * as appsService from '@/service/apps'
import * as exploreService from '@/service/explore'
import * as workflowService from '@/service/workflow'
import { AppModeEnum } from '@/types/app'
// Import component after mocks
import AppCard from './app-card'
import AppCard from '../app-card'
// Mock next/navigation
const mockPush = vi.fn()
@@ -24,11 +21,11 @@ vi.mock('next/navigation', () => ({
// Include createContext for components that use it (like Toast)
const mockNotify = vi.fn()
vi.mock('use-context-selector', () => ({
createContext: (defaultValue: any) => React.createContext(defaultValue),
createContext: <T,>(defaultValue: T) => React.createContext(defaultValue),
useContext: () => ({
notify: mockNotify,
}),
useContextSelector: (_context: any, selector: any) => selector({
useContextSelector: (_context: unknown, selector: (state: Record<string, unknown>) => unknown) => selector({
notify: mockNotify,
}),
}))
@@ -51,7 +48,7 @@ vi.mock('@/context/provider-context', () => ({
// Mock global public store - allow dynamic configuration
let mockWebappAuthEnabled = false
vi.mock('@/context/global-public-context', () => ({
useGlobalPublicStore: (selector: (s: any) => any) => selector({
useGlobalPublicStore: (selector: (s: Record<string, unknown>) => unknown) => selector({
systemFeatures: {
webapp_auth: { enabled: mockWebappAuthEnabled },
branding: { enabled: false },
@@ -106,11 +103,11 @@ vi.mock('@/utils/time', () => ({
// Mock dynamic imports
vi.mock('next/dynamic', () => ({
default: (importFn: () => Promise<any>) => {
default: (importFn: () => Promise<unknown>) => {
const fnString = importFn.toString()
if (fnString.includes('create-app-modal') || fnString.includes('explore/create-app-modal')) {
return function MockEditAppModal({ show, onHide, onConfirm }: any) {
return function MockEditAppModal({ show, onHide, onConfirm }: { show: boolean, onHide: () => void, onConfirm?: (data: Record<string, unknown>) => void }) {
if (!show)
return null
return React.createElement('div', { 'data-testid': 'edit-app-modal' }, React.createElement('button', { 'onClick': onHide, 'data-testid': 'close-edit-modal' }, 'Close'), React.createElement('button', {
@@ -128,7 +125,7 @@ vi.mock('next/dynamic', () => ({
}
}
if (fnString.includes('duplicate-modal')) {
return function MockDuplicateAppModal({ show, onHide, onConfirm }: any) {
return function MockDuplicateAppModal({ show, onHide, onConfirm }: { show: boolean, onHide: () => void, onConfirm?: (data: Record<string, unknown>) => void }) {
if (!show)
return null
return React.createElement('div', { 'data-testid': 'duplicate-modal' }, React.createElement('button', { 'onClick': onHide, 'data-testid': 'close-duplicate-modal' }, 'Close'), React.createElement('button', {
@@ -143,26 +140,26 @@ vi.mock('next/dynamic', () => ({
}
}
if (fnString.includes('switch-app-modal')) {
return function MockSwitchAppModal({ show, onClose, onSuccess }: any) {
return function MockSwitchAppModal({ show, onClose, onSuccess }: { show: boolean, onClose: () => void, onSuccess: () => void }) {
if (!show)
return null
return React.createElement('div', { 'data-testid': 'switch-modal' }, React.createElement('button', { 'onClick': onClose, 'data-testid': 'close-switch-modal' }, 'Close'), React.createElement('button', { 'onClick': onSuccess, 'data-testid': 'confirm-switch-modal' }, 'Switch'))
}
}
if (fnString.includes('base/confirm')) {
return function MockConfirm({ isShow, onCancel, onConfirm }: any) {
return function MockConfirm({ isShow, onCancel, onConfirm }: { isShow: boolean, onCancel: () => void, onConfirm: () => void }) {
if (!isShow)
return null
return React.createElement('div', { 'data-testid': 'confirm-dialog' }, React.createElement('button', { 'onClick': onCancel, 'data-testid': 'cancel-confirm' }, 'Cancel'), React.createElement('button', { 'onClick': onConfirm, 'data-testid': 'confirm-confirm' }, 'Confirm'))
}
}
if (fnString.includes('dsl-export-confirm-modal')) {
return function MockDSLExportModal({ onClose, onConfirm }: any) {
return function MockDSLExportModal({ onClose, onConfirm }: { onClose?: () => void, onConfirm?: (withSecrets: boolean) => void }) {
return React.createElement('div', { 'data-testid': 'dsl-export-modal' }, React.createElement('button', { 'onClick': () => onClose?.(), 'data-testid': 'close-dsl-export' }, 'Close'), React.createElement('button', { 'onClick': () => onConfirm?.(true), 'data-testid': 'confirm-dsl-export' }, 'Export with secrets'), React.createElement('button', { 'onClick': () => onConfirm?.(false), 'data-testid': 'confirm-dsl-export-no-secrets' }, 'Export without secrets'))
}
}
if (fnString.includes('app-access-control')) {
return function MockAccessControl({ onClose, onConfirm }: any) {
return function MockAccessControl({ onClose, onConfirm }: { onClose: () => void, onConfirm: () => void }) {
return React.createElement('div', { 'data-testid': 'access-control-modal' }, React.createElement('button', { 'onClick': onClose, 'data-testid': 'close-access-control' }, 'Close'), React.createElement('button', { 'onClick': onConfirm, 'data-testid': 'confirm-access-control' }, 'Confirm'))
}
}
@@ -172,7 +169,9 @@ vi.mock('next/dynamic', () => ({
// Popover uses @headlessui/react portals - mock for controlled interaction testing
vi.mock('@/app/components/base/popover', () => {
const MockPopover = ({ htmlContent, btnElement, btnClassName }: any) => {
type PopoverHtmlContent = React.ReactNode | ((state: { open: boolean, onClose: () => void, onClick: () => void }) => React.ReactNode)
type MockPopoverProps = { htmlContent: PopoverHtmlContent, btnElement: React.ReactNode, btnClassName?: string | ((open: boolean) => string) }
const MockPopover = ({ htmlContent, btnElement, btnClassName }: MockPopoverProps) => {
const [isOpen, setIsOpen] = React.useState(false)
const computedClassName = typeof btnClassName === 'function' ? btnClassName(isOpen) : ''
return React.createElement('div', { 'data-testid': 'custom-popover', 'className': computedClassName }, React.createElement('div', {
@@ -188,13 +187,13 @@ vi.mock('@/app/components/base/popover', () => {
// Tooltip uses portals - minimal mock preserving popup content as title attribute
vi.mock('@/app/components/base/tooltip', () => ({
default: ({ children, popupContent }: any) => React.createElement('div', { title: popupContent }, children),
default: ({ children, popupContent }: { children: React.ReactNode, popupContent: React.ReactNode }) => React.createElement('div', { title: popupContent }, children),
}))
// TagSelector has API dependency (service/tag) - mock for isolated testing
vi.mock('@/app/components/base/tag-management/selector', () => ({
default: ({ tags }: any) => {
return React.createElement('div', { 'aria-label': 'tag-selector' }, tags?.map((tag: any) => React.createElement('span', { key: tag.id }, tag.name)))
default: ({ tags }: { tags?: { id: string, name: string }[] }) => {
return React.createElement('div', { 'aria-label': 'tag-selector' }, tags?.map((tag: { id: string, name: string }) => React.createElement('span', { key: tag.id }, tag.name)))
},
}))
@@ -203,11 +202,7 @@ vi.mock('@/app/components/app/type-selector', () => ({
AppTypeIcon: () => React.createElement('div', { 'data-testid': 'app-type-icon' }),
}))
// ============================================================================
// Test Data Factories
// ============================================================================
const createMockApp = (overrides: Record<string, any> = {}) => ({
const createMockApp = (overrides: Partial<App> = {}): App => ({
id: 'test-app-id',
name: 'Test App',
description: 'Test app description',
@@ -229,16 +224,8 @@ const createMockApp = (overrides: Record<string, any> = {}) => ({
api_rpm: 60,
api_rph: 3600,
is_demo: false,
model_config: {} as any,
app_model_config: {} as any,
site: {} as any,
api_base_url: 'https://api.example.com',
...overrides,
})
// ============================================================================
// Tests
// ============================================================================
} as App)
describe('AppCard', () => {
const mockApp = createMockApp()
@@ -1171,7 +1158,7 @@ describe('AppCard', () => {
(exploreService.fetchInstalledAppList as Mock).mockRejectedValueOnce(new Error('API Error'))
// Configure mockOpenAsyncWindow to call the callback and trigger error
mockOpenAsyncWindow.mockImplementationOnce(async (callback: () => Promise<string>, options: any) => {
mockOpenAsyncWindow.mockImplementationOnce(async (callback: () => Promise<string>, options?: { onError?: (err: unknown) => void }) => {
try {
await callback()
}
@@ -1213,7 +1200,7 @@ describe('AppCard', () => {
(exploreService.fetchInstalledAppList as Mock).mockResolvedValueOnce({ installed_apps: [] })
// Configure mockOpenAsyncWindow to call the callback and trigger error
mockOpenAsyncWindow.mockImplementationOnce(async (callback: () => Promise<string>, options: any) => {
mockOpenAsyncWindow.mockImplementationOnce(async (callback: () => Promise<string>, options?: { onError?: (err: unknown) => void }) => {
try {
await callback()
}

View File

@@ -1,6 +1,6 @@
import { render, screen } from '@testing-library/react'
import * as React from 'react'
import Empty from './empty'
import Empty from '../empty'
describe('Empty', () => {
beforeEach(() => {
@@ -21,7 +21,6 @@ describe('Empty', () => {
it('should display the no apps found message', () => {
render(<Empty />)
// Use pattern matching for resilient text assertions
expect(screen.getByText('app.newApp.noAppsFound')).toBeInTheDocument()
})
})

View File

@@ -1,6 +1,6 @@
import { render, screen } from '@testing-library/react'
import * as React from 'react'
import Footer from './footer'
import Footer from '../footer'
describe('Footer', () => {
beforeEach(() => {
@@ -15,7 +15,6 @@ describe('Footer', () => {
it('should display the community heading', () => {
render(<Footer />)
// Use pattern matching for resilient text assertions
expect(screen.getByText('app.join')).toBeInTheDocument()
})

View File

@@ -3,21 +3,17 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { render, screen } from '@testing-library/react'
import * as React from 'react'
// Import after mocks
import Apps from './index'
import Apps from '../index'
// Track mock calls
let documentTitleCalls: string[] = []
let educationInitCalls: number = 0
// Mock useDocumentTitle hook
vi.mock('@/hooks/use-document-title', () => ({
default: (title: string) => {
documentTitleCalls.push(title)
},
}))
// Mock useEducationInit hook
vi.mock('@/app/education-apply/hooks', () => ({
useEducationInit: () => {
educationInitCalls++
@@ -33,8 +29,7 @@ vi.mock('@/hooks/use-import-dsl', () => ({
}),
}))
// Mock List component
vi.mock('./list', () => ({
vi.mock('../list', () => ({
default: () => {
return React.createElement('div', { 'data-testid': 'apps-list' }, 'Apps List')
},
@@ -100,10 +95,7 @@ describe('Apps', () => {
it('should render full component tree', () => {
renderWithClient(<Apps />)
// Verify container exists
expect(screen.getByTestId('apps-list')).toBeInTheDocument()
// Verify hooks were called
expect(documentTitleCalls.length).toBeGreaterThanOrEqual(1)
expect(educationInitCalls).toBeGreaterThanOrEqual(1)
})

View File

@@ -1,12 +1,13 @@
import type { UrlUpdateEvent } from 'nuqs/adapters/testing'
import type { ReactNode } from 'react'
import { act, fireEvent, render, screen } from '@testing-library/react'
import { NuqsTestingAdapter } from 'nuqs/adapters/testing'
import * as React from 'react'
import { useStore as useTagStore } from '@/app/components/base/tag-management/store'
import { AppModeEnum } from '@/types/app'
// Import after mocks
import List from './list'
import List from '../list'
// Mock next/navigation
const mockReplace = vi.fn()
const mockRouter = { replace: mockReplace }
vi.mock('next/navigation', () => ({
@@ -14,7 +15,6 @@ vi.mock('next/navigation', () => ({
useSearchParams: () => new URLSearchParams(''),
}))
// Mock app context
const mockIsCurrentWorkspaceEditor = vi.fn(() => true)
const mockIsCurrentWorkspaceDatasetOperator = vi.fn(() => false)
vi.mock('@/context/app-context', () => ({
@@ -24,7 +24,6 @@ vi.mock('@/context/app-context', () => ({
}),
}))
// Mock global public store
vi.mock('@/context/global-public-context', () => ({
useGlobalPublicStore: () => ({
systemFeatures: {
@@ -33,41 +32,28 @@ vi.mock('@/context/global-public-context', () => ({
}),
}))
// Mock custom hooks - allow dynamic query state
const mockSetQuery = vi.fn()
const mockQueryState = {
tagIDs: [] as string[],
keywords: '',
isCreatedByMe: false,
}
vi.mock('./hooks/use-apps-query-state', () => ({
vi.mock('../hooks/use-apps-query-state', () => ({
default: () => ({
query: mockQueryState,
setQuery: mockSetQuery,
}),
}))
// Store callback for testing DSL file drop
let mockOnDSLFileDropped: ((file: File) => void) | null = null
let mockDragging = false
vi.mock('./hooks/use-dsl-drag-drop', () => ({
vi.mock('../hooks/use-dsl-drag-drop', () => ({
useDSLDragDrop: ({ onDSLFileDropped }: { onDSLFileDropped: (file: File) => void }) => {
mockOnDSLFileDropped = onDSLFileDropped
return { dragging: mockDragging }
},
}))
const mockSetActiveTab = vi.fn()
vi.mock('nuqs', () => ({
useQueryState: () => ['all', mockSetActiveTab],
parseAsString: {
withDefault: () => ({
withOptions: () => ({}),
}),
},
}))
// Mock service hooks - use object for mutable state (vi.mock is hoisted)
const mockRefetch = vi.fn()
const mockFetchNextPage = vi.fn()
@@ -124,47 +110,20 @@ vi.mock('@/service/use-apps', () => ({
}),
}))
// Use real tag store - global zustand mock will auto-reset between tests
// Mock tag service to avoid API calls in TagFilter
vi.mock('@/service/tag', () => ({
fetchTagList: vi.fn().mockResolvedValue([{ id: 'tag-1', name: 'Test Tag', type: 'app' }]),
}))
// Store TagFilter onChange callback for testing
let mockTagFilterOnChange: ((value: string[]) => void) | null = null
vi.mock('@/app/components/base/tag-management/filter', () => ({
default: ({ onChange }: { onChange: (value: string[]) => void }) => {
mockTagFilterOnChange = onChange
return React.createElement('div', { 'data-testid': 'tag-filter' }, 'common.tag.placeholder')
},
}))
// Mock config
vi.mock('@/config', () => ({
NEED_REFRESH_APP_LIST_KEY: 'needRefreshAppList',
}))
// Mock pay hook
vi.mock('@/hooks/use-pay', () => ({
CheckModal: () => null,
}))
// Mock ahooks - useMount only executes once on mount, not on fn change
vi.mock('ahooks', () => ({
useDebounceFn: (fn: () => void) => ({ run: fn }),
useMount: (fn: () => void) => {
const fnRef = React.useRef(fn)
fnRef.current = fn
React.useEffect(() => {
fnRef.current()
}, [])
},
}))
// Mock dynamic imports
vi.mock('next/dynamic', () => ({
default: (importFn: () => Promise<any>) => {
default: (importFn: () => Promise<unknown>) => {
const fnString = importFn.toString()
if (fnString.includes('tag-management')) {
@@ -173,7 +132,7 @@ vi.mock('next/dynamic', () => ({
}
}
if (fnString.includes('create-from-dsl-modal')) {
return function MockCreateFromDSLModal({ show, onClose, onSuccess }: any) {
return function MockCreateFromDSLModal({ show, onClose, onSuccess }: { show: boolean, onClose: () => void, onSuccess: () => void }) {
if (!show)
return null
return React.createElement('div', { 'data-testid': 'create-dsl-modal' }, React.createElement('button', { 'onClick': onClose, 'data-testid': 'close-dsl-modal' }, 'Close'), React.createElement('button', { 'onClick': onSuccess, 'data-testid': 'success-dsl-modal' }, 'Success'))
@@ -183,41 +142,34 @@ vi.mock('next/dynamic', () => ({
},
}))
/**
* Mock child components for focused List component testing.
* These mocks isolate the List component's behavior from its children.
* Each child component (AppCard, NewAppCard, Empty, Footer) has its own dedicated tests.
*/
vi.mock('./app-card', () => ({
default: ({ app }: any) => {
vi.mock('../app-card', () => ({
default: ({ app }: { app: { id: string, name: string } }) => {
return React.createElement('div', { 'data-testid': `app-card-${app.id}`, 'role': 'article' }, app.name)
},
}))
vi.mock('./new-app-card', () => ({
default: React.forwardRef((_props: any, _ref: any) => {
vi.mock('../new-app-card', () => ({
default: React.forwardRef((_props: unknown, _ref: React.ForwardedRef<unknown>) => {
return React.createElement('div', { 'data-testid': 'new-app-card', 'role': 'button' }, 'New App Card')
}),
}))
vi.mock('./empty', () => ({
vi.mock('../empty', () => ({
default: () => {
return React.createElement('div', { 'data-testid': 'empty-state', 'role': 'status' }, 'No apps found')
},
}))
vi.mock('./footer', () => ({
vi.mock('../footer', () => ({
default: () => {
return React.createElement('footer', { 'data-testid': 'footer', 'role': 'contentinfo' }, 'Footer')
},
}))
// Store IntersectionObserver callback
let intersectionCallback: IntersectionObserverCallback | null = null
const mockObserve = vi.fn()
const mockDisconnect = vi.fn()
// Mock IntersectionObserver
beforeAll(() => {
globalThis.IntersectionObserver = class MockIntersectionObserver {
constructor(callback: IntersectionObserverCallback) {
@@ -234,10 +186,21 @@ beforeAll(() => {
} as unknown as typeof IntersectionObserver
})
// Render helper wrapping with NuqsTestingAdapter
const onUrlUpdate = vi.fn<(event: UrlUpdateEvent) => void>()
const renderList = (searchParams = '') => {
const wrapper = ({ children }: { children: ReactNode }) => (
<NuqsTestingAdapter searchParams={searchParams} onUrlUpdate={onUrlUpdate}>
{children}
</NuqsTestingAdapter>
)
return render(<List />, { wrapper })
}
describe('List', () => {
beforeEach(() => {
vi.clearAllMocks()
// Set up tag store state
onUrlUpdate.mockClear()
useTagStore.setState({
tagList: [{ id: 'tag-1', name: 'Test Tag', type: 'app', binding_count: 0 }],
showTagManagementModal: false,
@@ -246,7 +209,6 @@ describe('List', () => {
mockIsCurrentWorkspaceDatasetOperator.mockReturnValue(false)
mockDragging = false
mockOnDSLFileDropped = null
mockTagFilterOnChange = null
mockServiceState.error = null
mockServiceState.hasNextPage = false
mockServiceState.isLoading = false
@@ -260,13 +222,12 @@ describe('List', () => {
describe('Rendering', () => {
it('should render without crashing', () => {
render(<List />)
// Tab slider renders app type tabs
renderList()
expect(screen.getByText('app.types.all')).toBeInTheDocument()
})
it('should render tab slider with all app types', () => {
render(<List />)
renderList()
expect(screen.getByText('app.types.all')).toBeInTheDocument()
expect(screen.getByText('app.types.workflow')).toBeInTheDocument()
@@ -277,71 +238,74 @@ describe('List', () => {
})
it('should render search input', () => {
render(<List />)
// Input component renders a searchbox
renderList()
expect(screen.getByRole('textbox')).toBeInTheDocument()
})
it('should render tag filter', () => {
render(<List />)
// Tag filter renders with placeholder text
renderList()
expect(screen.getByText('common.tag.placeholder')).toBeInTheDocument()
})
it('should render created by me checkbox', () => {
render(<List />)
renderList()
expect(screen.getByText('app.showMyCreatedAppsOnly')).toBeInTheDocument()
})
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-2')).toBeInTheDocument()
})
it('should render new app card for editors', () => {
render(<List />)
renderList()
expect(screen.getByTestId('new-app-card')).toBeInTheDocument()
})
it('should render footer when branding is disabled', () => {
render(<List />)
renderList()
expect(screen.getByTestId('footer')).toBeInTheDocument()
})
it('should render drop DSL hint for editors', () => {
render(<List />)
renderList()
expect(screen.getByText('app.newApp.dropDSLToCreateApp')).toBeInTheDocument()
})
})
describe('Tab Navigation', () => {
it('should call setActiveTab when tab is clicked', () => {
render(<List />)
it('should update URL when workflow tab is clicked', async () => {
renderList()
fireEvent.click(screen.getByText('app.types.workflow'))
expect(mockSetActiveTab).toHaveBeenCalledWith(AppModeEnum.WORKFLOW)
await vi.waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
const lastCall = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0]
expect(lastCall.searchParams.get('category')).toBe(AppModeEnum.WORKFLOW)
})
it('should call setActiveTab for all tab', () => {
render(<List />)
it('should update URL when all tab is clicked', async () => {
renderList('?category=workflow')
fireEvent.click(screen.getByText('app.types.all'))
expect(mockSetActiveTab).toHaveBeenCalledWith('all')
await vi.waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
const lastCall = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0]
// nuqs removes the default value ('all') from URL params
expect(lastCall.searchParams.has('category')).toBe(false)
})
})
describe('Search Functionality', () => {
it('should render search input field', () => {
render(<List />)
renderList()
expect(screen.getByRole('textbox')).toBeInTheDocument()
})
it('should handle search input change', () => {
render(<List />)
renderList()
const input = screen.getByRole('textbox')
fireEvent.change(input, { target: { value: 'test search' } })
@@ -349,55 +313,36 @@ describe('List', () => {
expect(mockSetQuery).toHaveBeenCalled()
})
it('should handle search input interaction', () => {
render(<List />)
const input = screen.getByRole('textbox')
expect(input).toBeInTheDocument()
})
it('should handle search clear button click', () => {
// Set initial keywords to make clear button visible
mockQueryState.keywords = 'existing search'
render(<List />)
renderList()
// Find and click clear button (Input component uses .group class for clear icon container)
const clearButton = document.querySelector('.group')
expect(clearButton).toBeInTheDocument()
if (clearButton)
fireEvent.click(clearButton)
// handleKeywordsChange should be called with empty string
expect(mockSetQuery).toHaveBeenCalled()
})
})
describe('Tag Filter', () => {
it('should render tag filter component', () => {
render(<List />)
expect(screen.getByText('common.tag.placeholder')).toBeInTheDocument()
})
it('should render tag filter with placeholder', () => {
render(<List />)
// Tag filter is rendered
renderList()
expect(screen.getByText('common.tag.placeholder')).toBeInTheDocument()
})
})
describe('Created By Me Filter', () => {
it('should render checkbox with correct label', () => {
render(<List />)
renderList()
expect(screen.getByText('app.showMyCreatedAppsOnly')).toBeInTheDocument()
})
it('should handle checkbox change', () => {
render(<List />)
renderList()
// Checkbox component uses data-testid="checkbox-{id}"
// CheckboxWithLabel doesn't pass testId, so id is undefined
const checkbox = screen.getByTestId('checkbox-undefined')
fireEvent.click(checkbox)
@@ -409,7 +354,7 @@ describe('List', () => {
it('should not render new app card for non-editors', () => {
mockIsCurrentWorkspaceEditor.mockReturnValue(false)
render(<List />)
renderList()
expect(screen.queryByTestId('new-app-card')).not.toBeInTheDocument()
})
@@ -417,7 +362,7 @@ describe('List', () => {
it('should not render drop DSL hint for non-editors', () => {
mockIsCurrentWorkspaceEditor.mockReturnValue(false)
render(<List />)
renderList()
expect(screen.queryByText(/drop dsl file to create app/i)).not.toBeInTheDocument()
})
@@ -427,7 +372,7 @@ describe('List', () => {
it('should redirect dataset operators to datasets page', () => {
mockIsCurrentWorkspaceDatasetOperator.mockReturnValue(true)
render(<List />)
renderList()
expect(mockReplace).toHaveBeenCalledWith('/datasets')
})
@@ -437,7 +382,7 @@ describe('List', () => {
it('should call refetch when refresh key is set in localStorage', () => {
localStorage.setItem('needRefreshAppList', '1')
render(<List />)
renderList()
expect(mockRefetch).toHaveBeenCalled()
expect(localStorage.getItem('needRefreshAppList')).toBeNull()
@@ -446,22 +391,30 @@ describe('List', () => {
describe('Edge Cases', () => {
it('should handle multiple renders without issues', () => {
const { rerender } = render(<List />)
const { rerender } = render(
<NuqsTestingAdapter>
<List />
</NuqsTestingAdapter>,
)
expect(screen.getByText('app.types.all')).toBeInTheDocument()
rerender(<List />)
rerender(
<NuqsTestingAdapter>
<List />
</NuqsTestingAdapter>,
)
expect(screen.getByText('app.types.all')).toBeInTheDocument()
})
it('should render app cards correctly', () => {
render(<List />)
renderList()
expect(screen.getByText('Test App 1')).toBeInTheDocument()
expect(screen.getByText('Test App 2')).toBeInTheDocument()
})
it('should render with all filter options visible', () => {
render(<List />)
renderList()
expect(screen.getByRole('textbox')).toBeInTheDocument()
expect(screen.getByText('common.tag.placeholder')).toBeInTheDocument()
@@ -471,14 +424,20 @@ describe('List', () => {
describe('Dragging State', () => {
it('should show drop hint when DSL feature is enabled for editors', () => {
render(<List />)
renderList()
expect(screen.getByText('app.newApp.dropDSLToCreateApp')).toBeInTheDocument()
})
it('should render dragging state overlay when dragging', () => {
mockDragging = true
const { container } = renderList()
expect(container).toBeInTheDocument()
})
})
describe('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.workflow')).toBeInTheDocument()
@@ -488,8 +447,8 @@ describe('List', () => {
expect(screen.getByText('app.types.completion')).toBeInTheDocument()
})
it('should call setActiveTab for each app type', () => {
render(<List />)
it('should update URL for each app type tab click', async () => {
renderList()
const appTypeTexts = [
{ mode: AppModeEnum.WORKFLOW, text: 'app.types.workflow' },
@@ -499,45 +458,26 @@ describe('List', () => {
{ mode: AppModeEnum.COMPLETION, text: 'app.types.completion' },
]
appTypeTexts.forEach(({ mode, text }) => {
for (const { mode, text } of appTypeTexts) {
onUrlUpdate.mockClear()
fireEvent.click(screen.getByText(text))
expect(mockSetActiveTab).toHaveBeenCalledWith(mode)
})
})
})
describe('Search and Filter Integration', () => {
it('should display search input with correct attributes', () => {
render(<List />)
const input = screen.getByRole('textbox')
expect(input).toBeInTheDocument()
expect(input).toHaveAttribute('value', '')
})
it('should have tag filter component', () => {
render(<List />)
expect(screen.getByText('common.tag.placeholder')).toBeInTheDocument()
})
it('should display created by me label', () => {
render(<List />)
expect(screen.getByText('app.showMyCreatedAppsOnly')).toBeInTheDocument()
await vi.waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
const lastCall = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0]
expect(lastCall.searchParams.get('category')).toBe(mode)
}
})
})
describe('App List Display', () => {
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-2')).toBeInTheDocument()
})
it('should display app names correctly', () => {
render(<List />)
renderList()
expect(screen.getByText('Test App 1')).toBeInTheDocument()
expect(screen.getByText('Test App 2')).toBeInTheDocument()
@@ -546,59 +486,27 @@ describe('List', () => {
describe('Footer Visibility', () => {
it('should render footer when branding is disabled', () => {
render(<List />)
renderList()
expect(screen.getByTestId('footer')).toBeInTheDocument()
})
})
// --------------------------------------------------------------------------
// Additional Coverage Tests
// --------------------------------------------------------------------------
describe('Additional Coverage', () => {
it('should render dragging state overlay when dragging', () => {
mockDragging = true
const { container } = render(<List />)
// Component should render successfully with dragging state
expect(container).toBeInTheDocument()
})
it('should handle app mode filter in query params', () => {
render(<List />)
const workflowTab = screen.getByText('app.types.workflow')
fireEvent.click(workflowTab)
expect(mockSetActiveTab).toHaveBeenCalledWith(AppModeEnum.WORKFLOW)
})
it('should render new app card for editors', () => {
render(<List />)
expect(screen.getByTestId('new-app-card')).toBeInTheDocument()
})
})
describe('DSL File Drop', () => {
it('should handle DSL file drop and show modal', () => {
render(<List />)
renderList()
// Simulate DSL file drop via the callback
const mockFile = new File(['test content'], 'test.yml', { type: 'application/yaml' })
act(() => {
if (mockOnDSLFileDropped)
mockOnDSLFileDropped(mockFile)
})
// Modal should be shown
expect(screen.getByTestId('create-dsl-modal')).toBeInTheDocument()
})
it('should close DSL modal when onClose is called', () => {
render(<List />)
renderList()
// Open modal via DSL file drop
const mockFile = new File(['test content'], 'test.yml', { type: 'application/yaml' })
act(() => {
if (mockOnDSLFileDropped)
@@ -607,16 +515,14 @@ describe('List', () => {
expect(screen.getByTestId('create-dsl-modal')).toBeInTheDocument()
// Close modal
fireEvent.click(screen.getByTestId('close-dsl-modal'))
expect(screen.queryByTestId('create-dsl-modal')).not.toBeInTheDocument()
})
it('should close DSL modal and refetch when onSuccess is called', () => {
render(<List />)
renderList()
// Open modal via DSL file drop
const mockFile = new File(['test content'], 'test.yml', { type: 'application/yaml' })
act(() => {
if (mockOnDSLFileDropped)
@@ -625,67 +531,18 @@ describe('List', () => {
expect(screen.getByTestId('create-dsl-modal')).toBeInTheDocument()
// Click success button
fireEvent.click(screen.getByTestId('success-dsl-modal'))
// Modal should be closed and refetch should be called
expect(screen.queryByTestId('create-dsl-modal')).not.toBeInTheDocument()
expect(mockRefetch).toHaveBeenCalled()
})
})
describe('Tag Filter Change', () => {
it('should handle tag filter value change', () => {
vi.useFakeTimers()
render(<List />)
// TagFilter component is rendered
expect(screen.getByTestId('tag-filter')).toBeInTheDocument()
// Trigger tag filter change via captured callback
act(() => {
if (mockTagFilterOnChange)
mockTagFilterOnChange(['tag-1', 'tag-2'])
})
// Advance timers to trigger debounced setTagIDs
act(() => {
vi.advanceTimersByTime(500)
})
// setQuery should have been called with updated tagIDs
expect(mockSetQuery).toHaveBeenCalled()
vi.useRealTimers()
})
it('should handle empty tag filter selection', () => {
vi.useFakeTimers()
render(<List />)
// Trigger tag filter change with empty array
act(() => {
if (mockTagFilterOnChange)
mockTagFilterOnChange([])
})
// Advance timers
act(() => {
vi.advanceTimersByTime(500)
})
expect(mockSetQuery).toHaveBeenCalled()
vi.useRealTimers()
})
})
describe('Infinite Scroll', () => {
it('should call fetchNextPage when intersection observer triggers', () => {
mockServiceState.hasNextPage = true
render(<List />)
renderList()
// Simulate intersection
if (intersectionCallback) {
act(() => {
intersectionCallback!(
@@ -700,9 +557,8 @@ describe('List', () => {
it('should not call fetchNextPage when not intersecting', () => {
mockServiceState.hasNextPage = true
render(<List />)
renderList()
// Simulate non-intersection
if (intersectionCallback) {
act(() => {
intersectionCallback!(
@@ -718,7 +574,7 @@ describe('List', () => {
it('should not call fetchNextPage when loading', () => {
mockServiceState.hasNextPage = true
mockServiceState.isLoading = true
render(<List />)
renderList()
if (intersectionCallback) {
act(() => {
@@ -736,11 +592,8 @@ describe('List', () => {
describe('Error State', () => {
it('should handle error state in useEffect', () => {
mockServiceState.error = new Error('Test error')
const { container } = render(<List />)
// Component should still render
const { container } = renderList()
expect(container).toBeInTheDocument()
// Disconnect should be called when there's an error (cleanup)
})
})
})

View File

@@ -1,10 +1,8 @@
import { fireEvent, render, screen } from '@testing-library/react'
import * as React from 'react'
// Import after mocks
import CreateAppCard from './new-app-card'
import CreateAppCard from '../new-app-card'
// Mock next/navigation
const mockReplace = vi.fn()
vi.mock('next/navigation', () => ({
useRouter: () => ({
@@ -13,7 +11,6 @@ vi.mock('next/navigation', () => ({
useSearchParams: () => new URLSearchParams(),
}))
// Mock provider context
const mockOnPlanInfoChanged = vi.fn()
vi.mock('@/context/provider-context', () => ({
useProviderContext: () => ({
@@ -21,37 +18,35 @@ vi.mock('@/context/provider-context', () => ({
}),
}))
// Mock next/dynamic to immediately resolve components
vi.mock('next/dynamic', () => ({
default: (importFn: () => Promise<any>) => {
default: (importFn: () => Promise<{ default: React.ComponentType }>) => {
const fnString = importFn.toString()
if (fnString.includes('create-app-modal') && !fnString.includes('create-from-dsl-modal')) {
return function MockCreateAppModal({ show, onClose, onSuccess, onCreateFromTemplate }: any) {
return function MockCreateAppModal({ show, onClose, onSuccess, onCreateFromTemplate }: Record<string, unknown>) {
if (!show)
return null
return React.createElement('div', { 'data-testid': 'create-app-modal' }, React.createElement('button', { 'onClick': onClose, 'data-testid': 'close-create-modal' }, 'Close'), React.createElement('button', { 'onClick': onSuccess, 'data-testid': 'success-create-modal' }, 'Success'), React.createElement('button', { 'onClick': onCreateFromTemplate, 'data-testid': 'to-template-modal' }, 'To Template'))
return React.createElement('div', { 'data-testid': 'create-app-modal' }, React.createElement('button', { 'onClick': onClose as () => void, 'data-testid': 'close-create-modal' }, 'Close'), React.createElement('button', { 'onClick': onSuccess as () => void, 'data-testid': 'success-create-modal' }, 'Success'), React.createElement('button', { 'onClick': onCreateFromTemplate as () => void, 'data-testid': 'to-template-modal' }, 'To Template'))
}
}
if (fnString.includes('create-app-dialog')) {
return function MockCreateAppTemplateDialog({ show, onClose, onSuccess, onCreateFromBlank }: any) {
return function MockCreateAppTemplateDialog({ show, onClose, onSuccess, onCreateFromBlank }: Record<string, unknown>) {
if (!show)
return null
return React.createElement('div', { 'data-testid': 'create-template-dialog' }, React.createElement('button', { 'onClick': onClose, 'data-testid': 'close-template-dialog' }, 'Close'), React.createElement('button', { 'onClick': onSuccess, 'data-testid': 'success-template-dialog' }, 'Success'), React.createElement('button', { 'onClick': onCreateFromBlank, 'data-testid': 'to-blank-modal' }, 'To Blank'))
return React.createElement('div', { 'data-testid': 'create-template-dialog' }, React.createElement('button', { 'onClick': onClose as () => void, 'data-testid': 'close-template-dialog' }, 'Close'), React.createElement('button', { 'onClick': onSuccess as () => void, 'data-testid': 'success-template-dialog' }, 'Success'), React.createElement('button', { 'onClick': onCreateFromBlank as () => void, 'data-testid': 'to-blank-modal' }, 'To Blank'))
}
}
if (fnString.includes('create-from-dsl-modal')) {
return function MockCreateFromDSLModal({ show, onClose, onSuccess }: any) {
return function MockCreateFromDSLModal({ show, onClose, onSuccess }: Record<string, unknown>) {
if (!show)
return null
return React.createElement('div', { 'data-testid': 'create-dsl-modal' }, React.createElement('button', { 'onClick': onClose, 'data-testid': 'close-dsl-modal' }, 'Close'), React.createElement('button', { 'onClick': onSuccess, 'data-testid': 'success-dsl-modal' }, 'Success'))
return React.createElement('div', { 'data-testid': 'create-dsl-modal' }, React.createElement('button', { 'onClick': onClose as () => void, 'data-testid': 'close-dsl-modal' }, 'Close'), React.createElement('button', { 'onClick': onSuccess as () => void, 'data-testid': 'success-dsl-modal' }, 'Success'))
}
}
return () => null
},
}))
// Mock CreateFromDSLModalTab enum
vi.mock('@/app/components/app/create-from-dsl-modal', () => ({
CreateFromDSLModalTab: {
FROM_URL: 'from-url',
@@ -68,7 +63,6 @@ describe('CreateAppCard', () => {
describe('Rendering', () => {
it('should render without crashing', () => {
render(<CreateAppCard ref={defaultRef} />)
// Use pattern matching for resilient text assertions
expect(screen.getByText('app.createApp')).toBeInTheDocument()
})
@@ -245,19 +239,15 @@ describe('CreateAppCard', () => {
it('should handle multiple modal opens/closes', () => {
render(<CreateAppCard ref={defaultRef} />)
// Open and close create modal
fireEvent.click(screen.getByText('app.newApp.startFromBlank'))
fireEvent.click(screen.getByTestId('close-create-modal'))
// Open and close template dialog
fireEvent.click(screen.getByText('app.newApp.startFromTemplate'))
fireEvent.click(screen.getByTestId('close-template-dialog'))
// Open and close DSL modal
fireEvent.click(screen.getByText('app.importDSL'))
fireEvent.click(screen.getByTestId('close-dsl-modal'))
// No modals should be visible
expect(screen.queryByTestId('create-app-modal')).not.toBeInTheDocument()
expect(screen.queryByTestId('create-template-dialog')).not.toBeInTheDocument()
expect(screen.queryByTestId('create-dsl-modal')).not.toBeInTheDocument()
@@ -267,7 +257,6 @@ describe('CreateAppCard', () => {
render(<CreateAppCard ref={defaultRef} />)
fireEvent.click(screen.getByText('app.newApp.startFromBlank'))
// This should not throw an error
expect(() => {
fireEvent.click(screen.getByTestId('success-create-modal'))
}).not.toThrow()

View File

@@ -1,16 +1,8 @@
import type { UrlUpdateEvent } from 'nuqs/adapters/testing'
import type { ReactNode } from 'react'
/**
* Test suite for useAppsQueryState hook
*
* This hook manages app filtering state through URL search parameters, enabling:
* - Bookmarkable filter states (users can share URLs with specific filters active)
* - Browser history integration (back/forward buttons work with filters)
* - Multiple filter types: tagIDs, keywords, isCreatedByMe
*/
import { act, renderHook, waitFor } from '@testing-library/react'
import { NuqsTestingAdapter } from 'nuqs/adapters/testing'
import useAppsQueryState from './use-apps-query-state'
import useAppsQueryState from '../use-apps-query-state'
const renderWithAdapter = (searchParams = '') => {
const onUrlUpdate = vi.fn<(event: UrlUpdateEvent) => void>()
@@ -23,13 +15,11 @@ const renderWithAdapter = (searchParams = '') => {
return { result, onUrlUpdate }
}
// Groups scenarios for useAppsQueryState behavior.
describe('useAppsQueryState', () => {
beforeEach(() => {
vi.clearAllMocks()
})
// Covers the hook return shape and default values.
describe('Initialization', () => {
it('should expose query and setQuery when initialized', () => {
const { result } = renderWithAdapter()
@@ -47,7 +37,6 @@ describe('useAppsQueryState', () => {
})
})
// Covers parsing of existing URL search params.
describe('Parsing search params', () => {
it('should parse tagIDs when URL includes tagIDs', () => {
const { result } = renderWithAdapter('?tagIDs=tag1;tag2;tag3')
@@ -78,7 +67,6 @@ describe('useAppsQueryState', () => {
})
})
// Covers updates driven by setQuery.
describe('Updating query state', () => {
it('should update keywords when setQuery receives keywords', () => {
const { result } = renderWithAdapter()
@@ -126,7 +114,6 @@ describe('useAppsQueryState', () => {
})
})
// Covers URL updates triggered by query changes.
describe('URL synchronization', () => {
it('should sync keywords to URL when keywords change', async () => {
const { result, onUrlUpdate } = renderWithAdapter()
@@ -202,7 +189,6 @@ describe('useAppsQueryState', () => {
})
})
// Covers decoding and empty values.
describe('Edge cases', () => {
it('should treat empty tagIDs as empty list when URL param is empty', () => {
const { result } = renderWithAdapter('?tagIDs=')
@@ -223,7 +209,6 @@ describe('useAppsQueryState', () => {
})
})
// Covers multi-step updates that mimic real usage.
describe('Integration scenarios', () => {
it('should keep accumulated filters when updates are sequential', () => {
const { result } = renderWithAdapter()

View File

@@ -1,15 +1,6 @@
/**
* Test suite for useDSLDragDrop hook
*
* This hook provides drag-and-drop functionality for DSL files, enabling:
* - File drag detection with visual feedback (dragging state)
* - YAML/YML file filtering (only accepts .yaml and .yml files)
* - Enable/disable toggle for conditional drag-and-drop
* - Cleanup on unmount (removes event listeners)
*/
import type { Mock } from 'vitest'
import { act, renderHook } from '@testing-library/react'
import { useDSLDragDrop } from './use-dsl-drag-drop'
import { useDSLDragDrop } from '../use-dsl-drag-drop'
describe('useDSLDragDrop', () => {
let container: HTMLDivElement
@@ -26,7 +17,6 @@ describe('useDSLDragDrop', () => {
document.body.removeChild(container)
})
// Helper to create drag events
const createDragEvent = (type: string, files: File[] = []) => {
const dataTransfer = {
types: files.length > 0 ? ['Files'] : [],
@@ -50,7 +40,6 @@ describe('useDSLDragDrop', () => {
return event
}
// Helper to create a mock file
const createMockFile = (name: string) => {
return new File(['content'], name, { type: 'application/x-yaml' })
}
@@ -147,14 +136,12 @@ describe('useDSLDragDrop', () => {
}),
)
// First, enter with files
const enterEvent = createDragEvent('dragenter', [createMockFile('test.yaml')])
act(() => {
container.dispatchEvent(enterEvent)
})
expect(result.current.dragging).toBe(true)
// Then leave with null relatedTarget (leaving container)
const leaveEvent = createDragEvent('dragleave')
Object.defineProperty(leaveEvent, 'relatedTarget', {
value: null,
@@ -180,14 +167,12 @@ describe('useDSLDragDrop', () => {
}),
)
// First, enter with files
const enterEvent = createDragEvent('dragenter', [createMockFile('test.yaml')])
act(() => {
container.dispatchEvent(enterEvent)
})
expect(result.current.dragging).toBe(true)
// Then leave but to a child element
const leaveEvent = createDragEvent('dragleave')
Object.defineProperty(leaveEvent, 'relatedTarget', {
value: childElement,
@@ -290,14 +275,12 @@ describe('useDSLDragDrop', () => {
}),
)
// First, enter with files
const enterEvent = createDragEvent('dragenter', [createMockFile('test.yaml')])
act(() => {
container.dispatchEvent(enterEvent)
})
expect(result.current.dragging).toBe(true)
// Then drop
const dropEvent = createDragEvent('drop', [createMockFile('test.yaml')])
act(() => {
container.dispatchEvent(dropEvent)
@@ -409,14 +392,12 @@ describe('useDSLDragDrop', () => {
{ initialProps: { enabled: true } },
)
// Set dragging state
const enterEvent = createDragEvent('dragenter', [createMockFile('test.yaml')])
act(() => {
container.dispatchEvent(enterEvent)
})
expect(result.current.dragging).toBe(true)
// Disable the hook
rerender({ enabled: false })
expect(result.current.dragging).toBe(false)
})

View File

@@ -6,12 +6,10 @@ vi.mock('@/utils/clipboard', () => ({
writeTextToClipboard: vi.fn().mockResolvedValue(undefined),
}))
// Suppress expected React act() warnings and jsdom unimplemented API errors
vi.spyOn(console, 'error').mockImplementation(() => {})
describe('code.tsx components', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.spyOn(console, 'error').mockImplementation(() => {})
vi.useFakeTimers({ shouldAdvanceTime: true })
// jsdom does not implement scrollBy; mock it to prevent stderr noise
window.scrollBy = vi.fn()
@@ -20,6 +18,7 @@ describe('code.tsx components', () => {
afterEach(() => {
vi.runOnlyPendingTimers()
vi.useRealTimers()
vi.restoreAllMocks()
})
describe('Code', () => {

View File

@@ -2,9 +2,6 @@ import { act, render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import InputCopy from '../input-copy'
// Suppress expected React act() warnings from CopyFeedback timer-based state updates
vi.spyOn(console, 'error').mockImplementation(() => {})
async function renderAndFlush(ui: React.ReactElement) {
const result = render(ui)
await act(async () => {
@@ -18,6 +15,7 @@ const execCommandMock = vi.fn().mockReturnValue(true)
describe('InputCopy', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.spyOn(console, 'error').mockImplementation(() => {})
vi.useFakeTimers({ shouldAdvanceTime: true })
execCommandMock.mockReturnValue(true)
document.execCommand = execCommandMock
@@ -26,6 +24,7 @@ describe('InputCopy', () => {
afterEach(() => {
vi.runOnlyPendingTimers()
vi.useRealTimers()
vi.restoreAllMocks()
})
describe('rendering', () => {

View File

@@ -3,9 +3,6 @@ import userEvent from '@testing-library/user-event'
import { afterEach } from 'vitest'
import SecretKeyModal from '../secret-key-modal'
// Suppress expected React act() warnings from Headless UI Dialog transitions and async API state updates
vi.spyOn(console, 'error').mockImplementation(() => {})
async function renderModal(ui: React.ReactElement) {
const result = render(ui)
await act(async () => {
@@ -91,6 +88,8 @@ describe('SecretKeyModal', () => {
beforeEach(() => {
vi.clearAllMocks()
// Suppress expected React act() warnings from Headless UI Dialog transitions and async API state updates
vi.spyOn(console, 'error').mockImplementation(() => {})
vi.useFakeTimers({ shouldAdvanceTime: true })
mockCurrentWorkspace.mockReturnValue({ id: 'workspace-1', name: 'Test Workspace' })
mockIsCurrentWorkspaceManager.mockReturnValue(true)
@@ -104,6 +103,7 @@ describe('SecretKeyModal', () => {
afterEach(() => {
vi.runOnlyPendingTimers()
vi.useRealTimers()
vi.restoreAllMocks()
})
describe('rendering when shown', () => {

View File

@@ -4,9 +4,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import TryApp from '../index'
import { TypeEnum } from '../tab'
// Suppress expected React act() warnings from internal async state updates
vi.spyOn(console, 'error').mockImplementation(() => {})
vi.mock('@/config', async (importOriginal) => {
const actual = await importOriginal() as object
return {
@@ -91,6 +88,9 @@ const createMockAppDetail = (mode: string = 'chat'): TryAppInfo => ({
describe('TryApp (main index.tsx)', () => {
beforeEach(() => {
vi.clearAllMocks()
// Suppress expected React act() warnings from internal async state updates
vi.spyOn(console, 'error').mockImplementation(() => {})
mockUseGetTryAppInfo.mockReturnValue({
data: createMockAppDetail(),
isLoading: false,
@@ -99,7 +99,7 @@ describe('TryApp (main index.tsx)', () => {
afterEach(() => {
cleanup()
vi.clearAllMocks()
vi.restoreAllMocks()
})
describe('loading state', () => {

View File

@@ -23,6 +23,10 @@ describe('VersionMismatchModal', () => {
vi.spyOn(console, 'error').mockImplementation(() => {})
})
afterEach(() => {
vi.restoreAllMocks()
})
describe('rendering', () => {
it('should render dialog when isShow is true', () => {
render(<VersionMismatchModal {...defaultProps} />)

View File

@@ -83,7 +83,7 @@ describe('usePipelineInit', () => {
})
afterEach(() => {
vi.clearAllMocks()
vi.restoreAllMocks()
})
describe('hook initialization', () => {