refactor(web): migrate to Vitest and esm (#29974)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: yyh <yuanyouhuilyz@gmail.com>
This commit is contained in:
Stephen Zhou
2025-12-22 16:35:22 +08:00
committed by GitHub
parent 42f7ecda12
commit eabdc5f0eb
268 changed files with 5455 additions and 6307 deletions

View File

@@ -1,11 +1,12 @@
import type { Mock } from 'vitest'
import React from 'react'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { AppModeEnum } from '@/types/app'
import { AccessMode } from '@/models/access-control'
// Mock next/navigation
const mockPush = jest.fn()
jest.mock('next/navigation', () => ({
const mockPush = vi.fn()
vi.mock('next/navigation', () => ({
useRouter: () => ({
push: mockPush,
}),
@@ -13,8 +14,8 @@ jest.mock('next/navigation', () => ({
// Mock use-context-selector with stable mockNotify reference for tracking calls
// Include createContext for components that use it (like Toast)
const mockNotify = jest.fn()
jest.mock('use-context-selector', () => {
const mockNotify = vi.fn()
vi.mock('use-context-selector', () => {
const React = require('react')
return {
createContext: (defaultValue: any) => React.createContext(defaultValue),
@@ -28,15 +29,15 @@ jest.mock('use-context-selector', () => {
})
// Mock app context
jest.mock('@/context/app-context', () => ({
vi.mock('@/context/app-context', () => ({
useAppContext: () => ({
isCurrentWorkspaceEditor: true,
}),
}))
// Mock provider context
const mockOnPlanInfoChanged = jest.fn()
jest.mock('@/context/provider-context', () => ({
const mockOnPlanInfoChanged = vi.fn()
vi.mock('@/context/provider-context', () => ({
useProviderContext: () => ({
onPlanInfoChanged: mockOnPlanInfoChanged,
}),
@@ -44,7 +45,7 @@ jest.mock('@/context/provider-context', () => ({
// Mock global public store - allow dynamic configuration
let mockWebappAuthEnabled = false
jest.mock('@/context/global-public-context', () => ({
vi.mock('@/context/global-public-context', () => ({
useGlobalPublicStore: (selector: (s: any) => any) => selector({
systemFeatures: {
webapp_auth: { enabled: mockWebappAuthEnabled },
@@ -56,23 +57,24 @@ jest.mock('@/context/global-public-context', () => ({
// Mock API services - import for direct manipulation
import * as appsService from '@/service/apps'
import * as workflowService from '@/service/workflow'
import * as exploreService from '@/service/explore'
jest.mock('@/service/apps', () => ({
deleteApp: jest.fn(() => Promise.resolve()),
updateAppInfo: jest.fn(() => Promise.resolve()),
copyApp: jest.fn(() => Promise.resolve({ id: 'new-app-id' })),
exportAppConfig: jest.fn(() => Promise.resolve({ data: 'yaml: content' })),
vi.mock('@/service/apps', () => ({
deleteApp: vi.fn(() => Promise.resolve()),
updateAppInfo: vi.fn(() => Promise.resolve()),
copyApp: vi.fn(() => Promise.resolve({ id: 'new-app-id' })),
exportAppConfig: vi.fn(() => Promise.resolve({ data: 'yaml: content' })),
}))
jest.mock('@/service/workflow', () => ({
fetchWorkflowDraft: jest.fn(() => Promise.resolve({ environment_variables: [] })),
vi.mock('@/service/workflow', () => ({
fetchWorkflowDraft: vi.fn(() => Promise.resolve({ environment_variables: [] })),
}))
jest.mock('@/service/explore', () => ({
fetchInstalledAppList: jest.fn(() => Promise.resolve({ installed_apps: [{ id: 'installed-1' }] })),
vi.mock('@/service/explore', () => ({
fetchInstalledAppList: vi.fn(() => Promise.resolve({ installed_apps: [{ id: 'installed-1' }] })),
}))
jest.mock('@/service/access-control', () => ({
vi.mock('@/service/access-control', () => ({
useGetUserCanAccessApp: () => ({
data: { result: true },
isLoading: false,
@@ -80,108 +82,114 @@ jest.mock('@/service/access-control', () => ({
}))
// Mock hooks
const mockOpenAsyncWindow = jest.fn()
jest.mock('@/hooks/use-async-window-open', () => ({
const mockOpenAsyncWindow = vi.fn()
vi.mock('@/hooks/use-async-window-open', () => ({
useAsyncWindowOpen: () => mockOpenAsyncWindow,
}))
// Mock utils
jest.mock('@/utils/app-redirection', () => ({
getRedirection: jest.fn(),
const { mockGetRedirection } = vi.hoisted(() => ({
mockGetRedirection: vi.fn(),
}))
jest.mock('@/utils/var', () => ({
vi.mock('@/utils/app-redirection', () => ({
getRedirection: mockGetRedirection,
}))
vi.mock('@/utils/var', () => ({
basePath: '',
}))
jest.mock('@/utils/time', () => ({
vi.mock('@/utils/time', () => ({
formatTime: () => 'Jan 1, 2024',
}))
// Mock dynamic imports
jest.mock('next/dynamic', () => {
vi.mock('next/dynamic', () => {
const React = require('react')
return (importFn: () => Promise<any>) => {
const fnString = importFn.toString()
return {
default: (importFn: () => Promise<any>) => {
const fnString = importFn.toString()
if (fnString.includes('create-app-modal') || fnString.includes('explore/create-app-modal')) {
return function MockEditAppModal({ show, onHide, onConfirm }: any) {
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', {
'onClick': () => onConfirm?.({
name: 'Updated App',
icon_type: 'emoji',
icon: '🎯',
icon_background: '#FFEAD5',
description: 'Updated description',
use_icon_as_answer_icon: false,
max_active_requests: null,
}),
'data-testid': 'confirm-edit-modal',
}, 'Confirm'),
)
if (fnString.includes('create-app-modal') || fnString.includes('explore/create-app-modal')) {
return function MockEditAppModal({ show, onHide, onConfirm }: any) {
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', {
'onClick': () => onConfirm?.({
name: 'Updated App',
icon_type: 'emoji',
icon: '🎯',
icon_background: '#FFEAD5',
description: 'Updated description',
use_icon_as_answer_icon: false,
max_active_requests: null,
}),
'data-testid': 'confirm-edit-modal',
}, 'Confirm'),
)
}
}
}
if (fnString.includes('duplicate-modal')) {
return function MockDuplicateAppModal({ show, onHide, onConfirm }: any) {
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', {
'onClick': () => onConfirm?.({
name: 'Copied App',
icon_type: 'emoji',
icon: '📋',
icon_background: '#E4FBCC',
}),
'data-testid': 'confirm-duplicate-modal',
}, 'Confirm'),
)
if (fnString.includes('duplicate-modal')) {
return function MockDuplicateAppModal({ show, onHide, onConfirm }: any) {
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', {
'onClick': () => onConfirm?.({
name: 'Copied App',
icon_type: 'emoji',
icon: '📋',
icon_background: '#E4FBCC',
}),
'data-testid': 'confirm-duplicate-modal',
}, 'Confirm'),
)
}
}
}
if (fnString.includes('switch-app-modal')) {
return function MockSwitchAppModal({ show, onClose, onSuccess }: any) {
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('switch-app-modal')) {
return function MockSwitchAppModal({ show, onClose, onSuccess }: any) {
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) {
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('base/confirm')) {
return function MockConfirm({ isShow, onCancel, onConfirm }: any) {
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 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('dsl-export-confirm-modal')) {
return function MockDSLExportModal({ onClose, onConfirm }: any) {
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 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'),
)
if (fnString.includes('app-access-control')) {
return function MockAccessControl({ onClose, onConfirm }: any) {
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'),
)
}
}
}
return () => null
return () => null
},
}
})
// Popover uses @headlessui/react portals - mock for controlled interaction testing
jest.mock('@/app/components/base/popover', () => {
vi.mock('@/app/components/base/popover', () => {
const MockPopover = ({ htmlContent, btnElement, btnClassName }: any) => {
const [isOpen, setIsOpen] = React.useState(false)
const computedClassName = typeof btnClassName === 'function' ? btnClassName(isOpen) : ''
@@ -202,13 +210,13 @@ jest.mock('@/app/components/base/popover', () => {
})
// Tooltip uses portals - minimal mock preserving popup content as title attribute
jest.mock('@/app/components/base/tooltip', () => ({
vi.mock('@/app/components/base/tooltip', () => ({
__esModule: true,
default: ({ children, popupContent }: any) => React.createElement('div', { title: popupContent }, children),
}))
// TagSelector has API dependency (service/tag) - mock for isolated testing
jest.mock('@/app/components/base/tag-management/selector', () => ({
vi.mock('@/app/components/base/tag-management/selector', () => ({
__esModule: true,
default: ({ tags }: any) => {
const React = require('react')
@@ -219,7 +227,7 @@ jest.mock('@/app/components/base/tag-management/selector', () => ({
}))
// AppTypeIcon has complex icon mapping - mock for focused component testing
jest.mock('@/app/components/app/type-selector', () => ({
vi.mock('@/app/components/app/type-selector', () => ({
AppTypeIcon: () => React.createElement('div', { 'data-testid': 'app-type-icon' }),
}))
@@ -265,10 +273,10 @@ const createMockApp = (overrides: Record<string, any> = {}) => ({
describe('AppCard', () => {
const mockApp = createMockApp()
const mockOnRefresh = jest.fn()
const mockOnRefresh = vi.fn()
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
mockOpenAsyncWindow.mockReset()
mockWebappAuthEnabled = false
})
@@ -375,11 +383,10 @@ describe('AppCard', () => {
})
it('should call getRedirection on card click', () => {
const { getRedirection } = require('@/utils/app-redirection')
render(<AppCard app={mockApp} />)
const card = screen.getByTitle('Test App').closest('[class*="cursor-pointer"]')!
fireEvent.click(card)
expect(getRedirection).toHaveBeenCalledWith(true, mockApp, mockPush)
expect(mockGetRedirection).toHaveBeenCalledWith(true, mockApp, mockPush)
})
})
@@ -627,7 +634,7 @@ describe('AppCard', () => {
})
it('should handle delete failure', async () => {
(appsService.deleteApp as jest.Mock).mockRejectedValueOnce(new Error('Delete failed'))
(appsService.deleteApp as Mock).mockRejectedValueOnce(new Error('Delete failed'))
render(<AppCard app={mockApp} onRefresh={mockOnRefresh} />)
@@ -706,7 +713,7 @@ describe('AppCard', () => {
})
it('should handle copy failure', async () => {
(appsService.copyApp as jest.Mock).mockRejectedValueOnce(new Error('Copy failed'))
(appsService.copyApp as Mock).mockRejectedValueOnce(new Error('Copy failed'))
render(<AppCard app={mockApp} onRefresh={mockOnRefresh} />)
@@ -741,7 +748,7 @@ describe('AppCard', () => {
})
it('should handle export failure', async () => {
(appsService.exportAppConfig as jest.Mock).mockRejectedValueOnce(new Error('Export failed'))
(appsService.exportAppConfig as Mock).mockRejectedValueOnce(new Error('Export failed'))
render(<AppCard app={mockApp} />)
@@ -855,7 +862,7 @@ describe('AppCard', () => {
})
it('should show DSL export modal when workflow has secret variables', async () => {
(workflowService.fetchWorkflowDraft as jest.Mock).mockResolvedValueOnce({
(workflowService.fetchWorkflowDraft as Mock).mockResolvedValueOnce({
environment_variables: [{ value_type: 'secret', name: 'API_KEY' }],
})
@@ -887,7 +894,7 @@ describe('AppCard', () => {
})
it('should close DSL export modal when onClose is called', async () => {
(workflowService.fetchWorkflowDraft as jest.Mock).mockResolvedValueOnce({
(workflowService.fetchWorkflowDraft as Mock).mockResolvedValueOnce({
environment_variables: [{ value_type: 'secret', name: 'API_KEY' }],
})
@@ -981,7 +988,7 @@ describe('AppCard', () => {
})
it('should handle edit failure', async () => {
(appsService.updateAppInfo as jest.Mock).mockRejectedValueOnce(new Error('Edit failed'))
(appsService.updateAppInfo as Mock).mockRejectedValueOnce(new Error('Edit failed'))
render(<AppCard app={mockApp} onRefresh={mockOnRefresh} />)
@@ -1039,7 +1046,7 @@ describe('AppCard', () => {
})
it('should handle workflow draft fetch failure during export', async () => {
(workflowService.fetchWorkflowDraft as jest.Mock).mockRejectedValueOnce(new Error('Fetch failed'))
(workflowService.fetchWorkflowDraft as Mock).mockRejectedValueOnce(new Error('Fetch failed'))
const workflowApp = { ...mockApp, mode: AppModeEnum.WORKFLOW }
render(<AppCard app={workflowApp} />)
@@ -1186,15 +1193,13 @@ describe('AppCard', () => {
fireEvent.click(openInExploreBtn)
})
const { fetchInstalledAppList } = require('@/service/explore')
await waitFor(() => {
expect(fetchInstalledAppList).toHaveBeenCalledWith(mockApp.id)
expect(exploreService.fetchInstalledAppList).toHaveBeenCalledWith(mockApp.id)
})
})
it('should handle open in explore API failure', async () => {
const { fetchInstalledAppList } = require('@/service/explore')
fetchInstalledAppList.mockRejectedValueOnce(new Error('API Error'))
(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) => {
@@ -1215,7 +1220,7 @@ describe('AppCard', () => {
})
await waitFor(() => {
expect(fetchInstalledAppList).toHaveBeenCalled()
expect(exploreService.fetchInstalledAppList).toHaveBeenCalled()
})
})
})
@@ -1236,8 +1241,7 @@ describe('AppCard', () => {
describe('Open in Explore - No App Found', () => {
it('should handle case when installed_apps is empty array', async () => {
const { fetchInstalledAppList } = require('@/service/explore')
fetchInstalledAppList.mockResolvedValueOnce({ installed_apps: [] })
(exploreService.fetchInstalledAppList as Mock).mockResolvedValueOnce({ installed_apps: [] })
// Configure mockOpenAsyncWindow to call the callback and trigger error
mockOpenAsyncWindow.mockImplementationOnce(async (callback: () => Promise<string>, options: any) => {
@@ -1258,13 +1262,12 @@ describe('AppCard', () => {
})
await waitFor(() => {
expect(fetchInstalledAppList).toHaveBeenCalled()
expect(exploreService.fetchInstalledAppList).toHaveBeenCalled()
})
})
it('should handle case when API throws in callback', async () => {
const { fetchInstalledAppList } = require('@/service/explore')
fetchInstalledAppList.mockRejectedValueOnce(new Error('Network error'))
(exploreService.fetchInstalledAppList as Mock).mockRejectedValueOnce(new Error('Network error'))
// Configure mockOpenAsyncWindow to call the callback without catching
mockOpenAsyncWindow.mockImplementationOnce(async (callback: () => Promise<string>) => {
@@ -1280,7 +1283,7 @@ describe('AppCard', () => {
})
await waitFor(() => {
expect(fetchInstalledAppList).toHaveBeenCalled()
expect(exploreService.fetchInstalledAppList).toHaveBeenCalled()
})
})
})

View File

@@ -4,7 +4,7 @@ import Empty from './empty'
describe('Empty', () => {
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
})
describe('Rendering', () => {

View File

@@ -4,7 +4,7 @@ import Footer from './footer'
describe('Footer', () => {
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
})
describe('Rendering', () => {

View File

@@ -12,16 +12,16 @@
import { act, renderHook } from '@testing-library/react'
// Mock Next.js navigation hooks
const mockPush = jest.fn()
const mockPush = vi.fn()
const mockPathname = '/apps'
let mockSearchParams = new URLSearchParams()
jest.mock('next/navigation', () => ({
usePathname: jest.fn(() => mockPathname),
useRouter: jest.fn(() => ({
vi.mock('next/navigation', () => ({
usePathname: vi.fn(() => mockPathname),
useRouter: vi.fn(() => ({
push: mockPush,
})),
useSearchParams: jest.fn(() => mockSearchParams),
useSearchParams: vi.fn(() => mockSearchParams),
}))
// Import the hook after mocks are set up
@@ -29,7 +29,7 @@ import useAppsQueryState from './use-apps-query-state'
describe('useAppsQueryState', () => {
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
mockSearchParams = new URLSearchParams()
})

View File

@@ -7,18 +7,19 @@
* - 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'
describe('useDSLDragDrop', () => {
let container: HTMLDivElement
let mockOnDSLFileDropped: jest.Mock
let mockOnDSLFileDropped: Mock
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
container = document.createElement('div')
document.body.appendChild(container)
mockOnDSLFileDropped = jest.fn()
mockOnDSLFileDropped = vi.fn()
})
afterEach(() => {
@@ -38,11 +39,11 @@ describe('useDSLDragDrop', () => {
writable: false,
})
Object.defineProperty(event, 'preventDefault', {
value: jest.fn(),
value: vi.fn(),
writable: false,
})
Object.defineProperty(event, 'stopPropagation', {
value: jest.fn(),
value: vi.fn(),
writable: false,
})
@@ -320,11 +321,11 @@ describe('useDSLDragDrop', () => {
writable: false,
})
Object.defineProperty(event, 'preventDefault', {
value: jest.fn(),
value: vi.fn(),
writable: false,
})
Object.defineProperty(event, 'stopPropagation', {
value: jest.fn(),
value: vi.fn(),
writable: false,
})
@@ -442,7 +443,7 @@ describe('useDSLDragDrop', () => {
describe('Cleanup', () => {
it('should remove event listeners on unmount', () => {
const containerRef = { current: container }
const removeEventListenerSpy = jest.spyOn(container, 'removeEventListener')
const removeEventListenerSpy = vi.spyOn(container, 'removeEventListener')
const { unmount } = renderHook(() =>
useDSLDragDrop({

View File

@@ -6,7 +6,7 @@ let documentTitleCalls: string[] = []
let educationInitCalls: number = 0
// Mock useDocumentTitle hook
jest.mock('@/hooks/use-document-title', () => ({
vi.mock('@/hooks/use-document-title', () => ({
__esModule: true,
default: (title: string) => {
documentTitleCalls.push(title)
@@ -14,14 +14,14 @@ jest.mock('@/hooks/use-document-title', () => ({
}))
// Mock useEducationInit hook
jest.mock('@/app/education-apply/hooks', () => ({
vi.mock('@/app/education-apply/hooks', () => ({
useEducationInit: () => {
educationInitCalls++
},
}))
// Mock List component
jest.mock('./list', () => ({
vi.mock('./list', () => ({
__esModule: true,
default: () => {
const React = require('react')
@@ -34,7 +34,7 @@ import Apps from './index'
describe('Apps', () => {
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
documentTitleCalls = []
educationInitCalls = 0
})

View File

@@ -3,16 +3,16 @@ import { act, fireEvent, render, screen } from '@testing-library/react'
import { AppModeEnum } from '@/types/app'
// Mock next/navigation
const mockReplace = jest.fn()
const mockReplace = vi.fn()
const mockRouter = { replace: mockReplace }
jest.mock('next/navigation', () => ({
vi.mock('next/navigation', () => ({
useRouter: () => mockRouter,
}))
// Mock app context
const mockIsCurrentWorkspaceEditor = jest.fn(() => true)
const mockIsCurrentWorkspaceDatasetOperator = jest.fn(() => false)
jest.mock('@/context/app-context', () => ({
const mockIsCurrentWorkspaceEditor = vi.fn(() => true)
const mockIsCurrentWorkspaceDatasetOperator = vi.fn(() => false)
vi.mock('@/context/app-context', () => ({
useAppContext: () => ({
isCurrentWorkspaceEditor: mockIsCurrentWorkspaceEditor(),
isCurrentWorkspaceDatasetOperator: mockIsCurrentWorkspaceDatasetOperator(),
@@ -20,7 +20,7 @@ jest.mock('@/context/app-context', () => ({
}))
// Mock global public store
jest.mock('@/context/global-public-context', () => ({
vi.mock('@/context/global-public-context', () => ({
useGlobalPublicStore: () => ({
systemFeatures: {
branding: { enabled: false },
@@ -29,13 +29,13 @@ jest.mock('@/context/global-public-context', () => ({
}))
// Mock custom hooks - allow dynamic query state
const mockSetQuery = jest.fn()
const mockSetQuery = vi.fn()
const mockQueryState = {
tagIDs: [] as string[],
keywords: '',
isCreatedByMe: false,
}
jest.mock('./hooks/use-apps-query-state', () => ({
vi.mock('./hooks/use-apps-query-state', () => ({
__esModule: true,
default: () => ({
query: mockQueryState,
@@ -46,21 +46,21 @@ jest.mock('./hooks/use-apps-query-state', () => ({
// Store callback for testing DSL file drop
let mockOnDSLFileDropped: ((file: File) => void) | null = null
let mockDragging = false
jest.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 = jest.fn()
jest.mock('@/hooks/use-tab-searchparams', () => ({
const mockSetActiveTab = vi.fn()
vi.mock('@/hooks/use-tab-searchparams', () => ({
useTabSearchParams: () => ['all', mockSetActiveTab],
}))
// Mock service hooks - use object for mutable state (jest.mock is hoisted)
const mockRefetch = jest.fn()
const mockFetchNextPage = jest.fn()
// Mock service hooks - use object for mutable state (vi.mock is hoisted)
const mockRefetch = vi.fn()
const mockFetchNextPage = vi.fn()
const mockServiceState = {
error: null as Error | null,
@@ -103,7 +103,7 @@ const defaultAppData = {
}],
}
jest.mock('@/service/use-apps', () => ({
vi.mock('@/service/use-apps', () => ({
useInfiniteAppList: () => ({
data: defaultAppData,
isLoading: mockServiceState.isLoading,
@@ -116,26 +116,26 @@ jest.mock('@/service/use-apps', () => ({
}))
// Mock tag store
jest.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) => {
const state = {
tagList: [{ id: 'tag-1', name: 'Test Tag', type: 'app' }],
setTagList: jest.fn(),
setTagList: vi.fn(),
showTagManagementModal: false,
setShowTagManagementModal: jest.fn(),
setShowTagManagementModal: vi.fn(),
}
return selector(state)
},
}))
// Mock tag service to avoid API calls in TagFilter
jest.mock('@/service/tag', () => ({
fetchTagList: jest.fn().mockResolvedValue([{ id: 'tag-1', name: 'Test Tag', type: 'app' }]),
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
jest.mock('@/app/components/base/tag-management/filter', () => ({
vi.mock('@/app/components/base/tag-management/filter', () => ({
__esModule: true,
default: ({ onChange }: { onChange: (value: string[]) => void }) => {
const React = require('react')
@@ -145,17 +145,17 @@ jest.mock('@/app/components/base/tag-management/filter', () => ({
}))
// Mock config
jest.mock('@/config', () => ({
vi.mock('@/config', () => ({
NEED_REFRESH_APP_LIST_KEY: 'needRefreshAppList',
}))
// Mock pay hook
jest.mock('@/hooks/use-pay', () => ({
vi.mock('@/hooks/use-pay', () => ({
CheckModal: () => null,
}))
// Mock ahooks - useMount only executes once on mount, not on fn change
jest.mock('ahooks', () => ({
vi.mock('ahooks', () => ({
useDebounceFn: (fn: () => void) => ({ run: fn }),
useMount: (fn: () => void) => {
const React = require('react')
@@ -168,26 +168,28 @@ jest.mock('ahooks', () => ({
}))
// Mock dynamic imports
jest.mock('next/dynamic', () => {
vi.mock('next/dynamic', () => {
const React = require('react')
return (importFn: () => Promise<any>) => {
const fnString = importFn.toString()
return {
default: (importFn: () => Promise<any>) => {
const fnString = importFn.toString()
if (fnString.includes('tag-management')) {
return function MockTagManagement() {
return React.createElement('div', { 'data-testid': 'tag-management-modal' })
if (fnString.includes('tag-management')) {
return function MockTagManagement() {
return React.createElement('div', { 'data-testid': 'tag-management-modal' })
}
}
}
if (fnString.includes('create-from-dsl-modal')) {
return function MockCreateFromDSLModal({ show, onClose, onSuccess }: any) {
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'),
)
if (fnString.includes('create-from-dsl-modal')) {
return function MockCreateFromDSLModal({ show, onClose, onSuccess }: any) {
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 () => null
return () => null
},
}
})
@@ -196,7 +198,7 @@ jest.mock('next/dynamic', () => {
* These mocks isolate the List component's behavior from its children.
* Each child component (AppCard, NewAppCard, Empty, Footer) has its own dedicated tests.
*/
jest.mock('./app-card', () => ({
vi.mock('./app-card', () => ({
__esModule: true,
default: ({ app }: any) => {
const React = require('react')
@@ -204,14 +206,16 @@ jest.mock('./app-card', () => ({
},
}))
jest.mock('./new-app-card', () => {
vi.mock('./new-app-card', () => {
const React = require('react')
return React.forwardRef((_props: any, _ref: any) => {
return React.createElement('div', { 'data-testid': 'new-app-card', 'role': 'button' }, 'New App Card')
})
return {
default: React.forwardRef((_props: any, _ref: any) => {
return React.createElement('div', { 'data-testid': 'new-app-card', 'role': 'button' }, 'New App Card')
}),
}
})
jest.mock('./empty', () => ({
vi.mock('./empty', () => ({
__esModule: true,
default: () => {
const React = require('react')
@@ -219,7 +223,7 @@ jest.mock('./empty', () => ({
},
}))
jest.mock('./footer', () => ({
vi.mock('./footer', () => ({
__esModule: true,
default: () => {
const React = require('react')
@@ -232,8 +236,8 @@ import List from './list'
// Store IntersectionObserver callback
let intersectionCallback: IntersectionObserverCallback | null = null
const mockObserve = jest.fn()
const mockDisconnect = jest.fn()
const mockObserve = vi.fn()
const mockDisconnect = vi.fn()
// Mock IntersectionObserver
beforeAll(() => {
@@ -244,7 +248,7 @@ beforeAll(() => {
observe = mockObserve
disconnect = mockDisconnect
unobserve = jest.fn()
unobserve = vi.fn()
root = null
rootMargin = ''
thresholds = []
@@ -254,7 +258,7 @@ beforeAll(() => {
describe('List', () => {
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
mockIsCurrentWorkspaceEditor.mockReturnValue(true)
mockIsCurrentWorkspaceDatasetOperator.mockReturnValue(false)
mockDragging = false
@@ -649,7 +653,7 @@ describe('List', () => {
describe('Tag Filter Change', () => {
it('should handle tag filter value change', () => {
jest.useFakeTimers()
vi.useFakeTimers()
render(<List />)
// TagFilter component is rendered
@@ -663,17 +667,17 @@ describe('List', () => {
// Advance timers to trigger debounced setTagIDs
act(() => {
jest.advanceTimersByTime(500)
vi.advanceTimersByTime(500)
})
// setQuery should have been called with updated tagIDs
expect(mockSetQuery).toHaveBeenCalled()
jest.useRealTimers()
vi.useRealTimers()
})
it('should handle empty tag filter selection', () => {
jest.useFakeTimers()
vi.useFakeTimers()
render(<List />)
// Trigger tag filter change with empty array
@@ -684,12 +688,12 @@ describe('List', () => {
// Advance timers
act(() => {
jest.advanceTimersByTime(500)
vi.advanceTimersByTime(500)
})
expect(mockSetQuery).toHaveBeenCalled()
jest.useRealTimers()
vi.useRealTimers()
})
})

View File

@@ -2,8 +2,8 @@ import React from 'react'
import { fireEvent, render, screen } from '@testing-library/react'
// Mock next/navigation
const mockReplace = jest.fn()
jest.mock('next/navigation', () => ({
const mockReplace = vi.fn()
vi.mock('next/navigation', () => ({
useRouter: () => ({
replace: mockReplace,
}),
@@ -11,54 +11,56 @@ jest.mock('next/navigation', () => ({
}))
// Mock provider context
const mockOnPlanInfoChanged = jest.fn()
jest.mock('@/context/provider-context', () => ({
const mockOnPlanInfoChanged = vi.fn()
vi.mock('@/context/provider-context', () => ({
useProviderContext: () => ({
onPlanInfoChanged: mockOnPlanInfoChanged,
}),
}))
// Mock next/dynamic to immediately resolve components
jest.mock('next/dynamic', () => {
vi.mock('next/dynamic', () => {
const React = require('react')
return (importFn: () => Promise<any>) => {
const fnString = importFn.toString()
return {
default: (importFn: () => Promise<any>) => {
const fnString = importFn.toString()
if (fnString.includes('create-app-modal') && !fnString.includes('create-from-dsl-modal')) {
return function MockCreateAppModal({ show, onClose, onSuccess, onCreateFromTemplate }: any) {
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'),
)
if (fnString.includes('create-app-modal') && !fnString.includes('create-from-dsl-modal')) {
return function MockCreateAppModal({ show, onClose, onSuccess, onCreateFromTemplate }: any) {
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'),
)
}
}
}
if (fnString.includes('create-app-dialog')) {
return function MockCreateAppTemplateDialog({ show, onClose, onSuccess, onCreateFromBlank }: any) {
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'),
)
if (fnString.includes('create-app-dialog')) {
return function MockCreateAppTemplateDialog({ show, onClose, onSuccess, onCreateFromBlank }: any) {
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'),
)
}
}
}
if (fnString.includes('create-from-dsl-modal')) {
return function MockCreateFromDSLModal({ show, onClose, onSuccess }: any) {
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'),
)
if (fnString.includes('create-from-dsl-modal')) {
return function MockCreateFromDSLModal({ show, onClose, onSuccess }: any) {
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 () => null
return () => null
},
}
})
// Mock CreateFromDSLModalTab enum
jest.mock('@/app/components/app/create-from-dsl-modal', () => ({
vi.mock('@/app/components/app/create-from-dsl-modal', () => ({
CreateFromDSLModalTab: {
FROM_URL: 'from-url',
},
@@ -71,7 +73,7 @@ describe('CreateAppCard', () => {
const defaultRef = { current: null } as React.RefObject<HTMLDivElement | null>
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
})
describe('Rendering', () => {
@@ -135,7 +137,7 @@ describe('CreateAppCard', () => {
})
it('should call onSuccess and onPlanInfoChanged on create app success', () => {
const mockOnSuccess = jest.fn()
const mockOnSuccess = vi.fn()
render(<CreateAppCard ref={defaultRef} onSuccess={mockOnSuccess} />)
fireEvent.click(screen.getByText('app.newApp.startFromBlank'))
@@ -178,7 +180,7 @@ describe('CreateAppCard', () => {
})
it('should call onSuccess and onPlanInfoChanged on template success', () => {
const mockOnSuccess = jest.fn()
const mockOnSuccess = vi.fn()
render(<CreateAppCard ref={defaultRef} onSuccess={mockOnSuccess} />)
fireEvent.click(screen.getByText('app.newApp.startFromTemplate'))
@@ -221,7 +223,7 @@ describe('CreateAppCard', () => {
})
it('should call onSuccess and onPlanInfoChanged on DSL import success', () => {
const mockOnSuccess = jest.fn()
const mockOnSuccess = vi.fn()
render(<CreateAppCard ref={defaultRef} onSuccess={mockOnSuccess} />)
fireEvent.click(screen.getByText('app.importDSL'))