test(web): add and enhance frontend automated tests across multiple modules (#32268)

Co-authored-by: CodingOnStar <hanxujiang@dify.com>
This commit is contained in:
Coding On Star
2026-02-13 10:27:48 +08:00
committed by GitHub
parent 16df9851a2
commit b6d506828b
75 changed files with 5652 additions and 4081 deletions

View File

@ -60,5 +60,11 @@ describe('Category', () => {
const allCategoriesItem = screen.getByText('explore.apps.allCategories')
expect(allCategoriesItem.className).toContain('bg-components-main-nav-nav-button-bg-active')
})
it('should render raw category name when i18n key does not exist', () => {
renderComponent({ list: ['CustomCategory', 'Recommended'] as AppCategory[] })
expect(screen.getByText('CustomCategory')).toBeInTheDocument()
})
})
})

View File

@ -1,5 +1,6 @@
import type { Mock } from 'vitest'
import { render, screen, waitFor } from '@testing-library/react'
import type { CurrentTryAppParams } from '@/context/explore-context'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { useContext } from 'use-context-selector'
import { useAppContext } from '@/context/app-context'
import ExploreContext from '@/context/explore-context'
@ -55,9 +56,21 @@ vi.mock('@/hooks/use-document-title', () => ({
default: vi.fn(),
}))
const ContextReader = () => {
const { hasEditPermission } = useContext(ExploreContext)
return <div>{hasEditPermission ? 'edit-yes' : 'edit-no'}</div>
const ContextReader = ({ triggerTryPanel }: { triggerTryPanel?: boolean }) => {
const { hasEditPermission, setShowTryAppPanel, isShowTryAppPanel, currentApp } = useContext(ExploreContext)
return (
<div>
{hasEditPermission ? 'edit-yes' : 'edit-no'}
{isShowTryAppPanel && <span data-testid="try-panel-open">open</span>}
{currentApp && <span data-testid="current-app">{currentApp.appId}</span>}
{triggerTryPanel && (
<>
<button data-testid="show-try" onClick={() => setShowTryAppPanel(true, { appId: 'test-app' } as CurrentTryAppParams)}>show</button>
<button data-testid="hide-try" onClick={() => setShowTryAppPanel(false)}>hide</button>
</>
)}
</div>
)
}
describe('Explore', () => {
@ -123,5 +136,69 @@ describe('Explore', () => {
expect(mockReplace).toHaveBeenCalledWith('/datasets')
})
})
it('should skip permission check when membersData has no accounts', () => {
; (useAppContext as Mock).mockReturnValue({
userProfile: { id: 'user-1' },
isCurrentWorkspaceDatasetOperator: false,
});
(useMembers as Mock).mockReturnValue({ data: undefined })
render((
<Explore>
<ContextReader />
</Explore>
))
expect(screen.getByText('edit-no')).toBeInTheDocument()
})
})
describe('Context: setShowTryAppPanel', () => {
it('should set currentApp params when showing try panel', async () => {
; (useAppContext as Mock).mockReturnValue({
userProfile: { id: 'user-1' },
isCurrentWorkspaceDatasetOperator: false,
});
(useMembers as Mock).mockReturnValue({ data: { accounts: [] } })
render((
<Explore>
<ContextReader triggerTryPanel />
</Explore>
))
fireEvent.click(screen.getByTestId('show-try'))
await waitFor(() => {
expect(screen.getByTestId('try-panel-open')).toBeInTheDocument()
expect(screen.getByTestId('current-app')).toHaveTextContent('test-app')
})
})
it('should clear currentApp params when hiding try panel', async () => {
; (useAppContext as Mock).mockReturnValue({
userProfile: { id: 'user-1' },
isCurrentWorkspaceDatasetOperator: false,
});
(useMembers as Mock).mockReturnValue({ data: { accounts: [] } })
render((
<Explore>
<ContextReader triggerTryPanel />
</Explore>
))
fireEvent.click(screen.getByTestId('show-try'))
await waitFor(() => {
expect(screen.getByTestId('try-panel-open')).toBeInTheDocument()
})
fireEvent.click(screen.getByTestId('hide-try'))
await waitFor(() => {
expect(screen.queryByTestId('try-panel-open')).not.toBeInTheDocument()
expect(screen.queryByTestId('current-app')).not.toBeInTheDocument()
})
})
})
})

View File

@ -2,6 +2,7 @@ import type { AppCardProps } from '../index'
import type { App } from '@/models/explore'
import { fireEvent, render, screen } from '@testing-library/react'
import * as React from 'react'
import ExploreContext from '@/context/explore-context'
import { AppModeEnum } from '@/types/app'
import AppCard from '../index'
@ -136,5 +137,32 @@ describe('AppCard', () => {
expect(screen.getByText('Sample App')).toBeInTheDocument()
})
it('should call setShowTryAppPanel when try button is clicked', () => {
const mockSetShowTryAppPanel = vi.fn()
const app = createApp()
render(
<ExploreContext.Provider
value={{
controlUpdateInstalledApps: 0,
setControlUpdateInstalledApps: vi.fn(),
hasEditPermission: false,
installedApps: [],
setInstalledApps: vi.fn(),
isFetchingInstalledApps: false,
setIsFetchingInstalledApps: vi.fn(),
isShowTryAppPanel: false,
setShowTryAppPanel: mockSetShowTryAppPanel,
}}
>
<AppCard app={app} canCreate={true} onCreate={vi.fn()} isExplore={true} />
</ExploreContext.Provider>,
)
fireEvent.click(screen.getByText('explore.appCard.try'))
expect(mockSetShowTryAppPanel).toHaveBeenCalledWith(true, { appId: 'app-id', app })
})
})
})

View File

@ -1,44 +1,21 @@
import type { Mock } from 'vitest'
import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal'
import type { CurrentTryAppParams } from '@/context/explore-context'
import type { App } from '@/models/explore'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { NuqsTestingAdapter } from 'nuqs/adapters/testing'
import ExploreContext from '@/context/explore-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { fetchAppDetail } from '@/service/explore'
import { AppModeEnum } from '@/types/app'
import AppList from '../index'
const allCategoriesEn = 'explore.apps.allCategories:{"lng":"en"}'
let mockTabValue = allCategoriesEn
const mockSetTab = vi.fn()
let mockExploreData: { categories: string[], allList: App[] } | undefined = { categories: [], allList: [] }
let mockIsLoading = false
let mockIsError = false
const mockHandleImportDSL = vi.fn()
const mockHandleImportDSLConfirm = vi.fn()
vi.mock('nuqs', async (importOriginal) => {
const actual = await importOriginal<typeof import('nuqs')>()
return {
...actual,
useQueryState: () => [mockTabValue, mockSetTab],
}
})
vi.mock('ahooks', async () => {
const actual = await vi.importActual<typeof import('ahooks')>('ahooks')
const React = await vi.importActual<typeof import('react')>('react')
return {
...actual,
useDebounceFn: (fn: (...args: unknown[]) => void) => {
const fnRef = React.useRef(fn)
fnRef.current = fn
return {
run: () => setTimeout(() => fnRef.current(), 0),
}
},
}
})
vi.mock('@/service/use-explore', () => ({
useExploreAppList: () => ({
data: mockExploreData,
@ -85,6 +62,19 @@ vi.mock('@/app/components/explore/create-app-modal', () => ({
},
}))
vi.mock('../../try-app', () => ({
default: ({ onCreate, onClose }: { onCreate: () => void, onClose: () => void }) => (
<div data-testid="try-app-panel">
<button data-testid="try-app-create" onClick={onCreate}>create</button>
<button data-testid="try-app-close" onClick={onClose}>close</button>
</div>
),
}))
vi.mock('../../banner/banner', () => ({
default: () => <div data-testid="explore-banner">banner</div>,
}))
vi.mock('@/app/components/app/create-from-dsl-modal/dsl-confirm-modal', () => ({
default: ({ onConfirm, onCancel }: { onConfirm: () => void, onCancel: () => void }) => (
<div data-testid="dsl-confirm-modal">
@ -121,35 +111,41 @@ const createApp = (overrides: Partial<App> = {}): App => ({
is_agent: overrides.is_agent ?? false,
})
const renderWithContext = (hasEditPermission = false, onSuccess?: () => void) => {
const renderWithContext = (hasEditPermission = false, onSuccess?: () => void, searchParams?: Record<string, string>) => {
return render(
<ExploreContext.Provider
value={{
controlUpdateInstalledApps: 0,
setControlUpdateInstalledApps: vi.fn(),
hasEditPermission,
installedApps: [],
setInstalledApps: vi.fn(),
isFetchingInstalledApps: false,
setIsFetchingInstalledApps: vi.fn(),
isShowTryAppPanel: false,
setShowTryAppPanel: vi.fn(),
}}
>
<AppList onSuccess={onSuccess} />
</ExploreContext.Provider>,
<NuqsTestingAdapter searchParams={searchParams}>
<ExploreContext.Provider
value={{
controlUpdateInstalledApps: 0,
setControlUpdateInstalledApps: vi.fn(),
hasEditPermission,
installedApps: [],
setInstalledApps: vi.fn(),
isFetchingInstalledApps: false,
setIsFetchingInstalledApps: vi.fn(),
isShowTryAppPanel: false,
setShowTryAppPanel: vi.fn(),
}}
>
<AppList onSuccess={onSuccess} />
</ExploreContext.Provider>
</NuqsTestingAdapter>,
)
}
describe('AppList', () => {
beforeEach(() => {
vi.useFakeTimers()
vi.clearAllMocks()
mockTabValue = allCategoriesEn
mockExploreData = { categories: [], allList: [] }
mockIsLoading = false
mockIsError = false
})
afterEach(() => {
vi.useRealTimers()
})
describe('Rendering', () => {
it('should render loading when the query is loading', () => {
mockExploreData = undefined
@ -175,13 +171,12 @@ describe('AppList', () => {
describe('Props', () => {
it('should filter apps by selected category', () => {
mockTabValue = 'Writing'
mockExploreData = {
categories: ['Writing', 'Translate'],
allList: [createApp(), createApp({ app_id: 'app-2', app: { ...createApp().app, name: 'Beta' }, category: 'Translate' })],
}
renderWithContext()
renderWithContext(false, undefined, { category: 'Writing' })
expect(screen.getByText('Alpha')).toBeInTheDocument()
expect(screen.queryByText('Beta')).not.toBeInTheDocument()
@ -199,13 +194,16 @@ describe('AppList', () => {
const input = screen.getByPlaceholderText('common.operation.search')
fireEvent.change(input, { target: { value: 'gam' } })
await waitFor(() => {
expect(screen.queryByText('Alpha')).not.toBeInTheDocument()
expect(screen.getByText('Gamma')).toBeInTheDocument()
await act(async () => {
await vi.advanceTimersByTimeAsync(500)
})
expect(screen.queryByText('Alpha')).not.toBeInTheDocument()
expect(screen.getByText('Gamma')).toBeInTheDocument()
})
it('should handle create flow and confirm DSL when pending', async () => {
vi.useRealTimers()
const onSuccess = vi.fn()
mockExploreData = {
categories: ['Writing'],
@ -247,16 +245,241 @@ describe('AppList', () => {
const input = screen.getByPlaceholderText('common.operation.search')
fireEvent.change(input, { target: { value: 'gam' } })
await waitFor(() => {
expect(screen.queryByText('Alpha')).not.toBeInTheDocument()
await act(async () => {
await vi.advanceTimersByTimeAsync(500)
})
expect(screen.queryByText('Alpha')).not.toBeInTheDocument()
fireEvent.click(screen.getByTestId('input-clear'))
await act(async () => {
await vi.advanceTimersByTimeAsync(500)
})
expect(screen.getByText('Alpha')).toBeInTheDocument()
expect(screen.getByText('Gamma')).toBeInTheDocument()
})
it('should render nothing when isError is true', () => {
mockIsError = true
mockExploreData = undefined
const { container } = renderWithContext()
expect(container.innerHTML).toBe('')
})
it('should render nothing when data is undefined', () => {
mockExploreData = undefined
const { container } = renderWithContext()
expect(container.innerHTML).toBe('')
})
it('should reset filter when reset button is clicked', async () => {
mockExploreData = {
categories: ['Writing'],
allList: [createApp(), createApp({ app_id: 'app-2', app: { ...createApp().app, name: 'Gamma' } })],
}
renderWithContext()
const input = screen.getByPlaceholderText('common.operation.search')
fireEvent.change(input, { target: { value: 'gam' } })
await act(async () => {
await vi.advanceTimersByTimeAsync(500)
})
expect(screen.queryByText('Alpha')).not.toBeInTheDocument()
fireEvent.click(screen.getByText('explore.apps.resetFilter'))
expect(screen.getByText('Alpha')).toBeInTheDocument()
expect(screen.getByText('Gamma')).toBeInTheDocument()
})
it('should close create modal via hide button', async () => {
vi.useRealTimers()
mockExploreData = {
categories: ['Writing'],
allList: [createApp()],
};
(fetchAppDetail as unknown as Mock).mockResolvedValue({ export_data: 'yaml' })
renderWithContext(true)
fireEvent.click(screen.getByText('explore.appCard.addToWorkspace'))
expect(await screen.findByTestId('create-app-modal')).toBeInTheDocument()
fireEvent.click(screen.getByTestId('hide-create'))
await waitFor(() => {
expect(screen.queryByTestId('create-app-modal')).not.toBeInTheDocument()
})
})
it('should close create modal on successful DSL import', async () => {
vi.useRealTimers()
mockExploreData = {
categories: ['Writing'],
allList: [createApp()],
};
(fetchAppDetail as unknown as Mock).mockResolvedValue({ export_data: 'yaml' })
mockHandleImportDSL.mockImplementation(async (_payload: unknown, options: { onSuccess?: () => void }) => {
options.onSuccess?.()
})
renderWithContext(true)
fireEvent.click(screen.getByText('explore.appCard.addToWorkspace'))
fireEvent.click(await screen.findByTestId('confirm-create'))
await waitFor(() => {
expect(screen.getByText('Alpha')).toBeInTheDocument()
expect(screen.getByText('Gamma')).toBeInTheDocument()
expect(screen.queryByTestId('create-app-modal')).not.toBeInTheDocument()
})
})
it('should cancel DSL confirm modal', async () => {
vi.useRealTimers()
mockExploreData = {
categories: ['Writing'],
allList: [createApp()],
};
(fetchAppDetail as unknown as Mock).mockResolvedValue({ export_data: 'yaml' })
mockHandleImportDSL.mockImplementation(async (_payload: unknown, options: { onPending?: () => void }) => {
options.onPending?.()
})
renderWithContext(true)
fireEvent.click(screen.getByText('explore.appCard.addToWorkspace'))
fireEvent.click(await screen.findByTestId('confirm-create'))
await waitFor(() => {
expect(screen.getByTestId('dsl-confirm-modal')).toBeInTheDocument()
})
fireEvent.click(screen.getByTestId('dsl-cancel'))
await waitFor(() => {
expect(screen.queryByTestId('dsl-confirm-modal')).not.toBeInTheDocument()
})
})
})
describe('TryApp Panel', () => {
it('should open create modal from try app panel', async () => {
vi.useRealTimers()
const mockSetShowTryAppPanel = vi.fn()
const app = createApp()
mockExploreData = {
categories: ['Writing'],
allList: [app],
}
render(
<NuqsTestingAdapter>
<ExploreContext.Provider
value={{
controlUpdateInstalledApps: 0,
setControlUpdateInstalledApps: vi.fn(),
hasEditPermission: true,
installedApps: [],
setInstalledApps: vi.fn(),
isFetchingInstalledApps: false,
setIsFetchingInstalledApps: vi.fn(),
isShowTryAppPanel: true,
setShowTryAppPanel: mockSetShowTryAppPanel,
currentApp: { appId: 'app-1', app },
}}
>
<AppList />
</ExploreContext.Provider>
</NuqsTestingAdapter>,
)
const createBtn = screen.getByTestId('try-app-create')
fireEvent.click(createBtn)
await waitFor(() => {
expect(screen.getByTestId('create-app-modal')).toBeInTheDocument()
})
})
it('should open create modal with null currApp when appParams has no app', async () => {
vi.useRealTimers()
mockExploreData = {
categories: ['Writing'],
allList: [createApp()],
}
render(
<NuqsTestingAdapter>
<ExploreContext.Provider
value={{
controlUpdateInstalledApps: 0,
setControlUpdateInstalledApps: vi.fn(),
hasEditPermission: true,
installedApps: [],
setInstalledApps: vi.fn(),
isFetchingInstalledApps: false,
setIsFetchingInstalledApps: vi.fn(),
isShowTryAppPanel: true,
setShowTryAppPanel: vi.fn(),
currentApp: { appId: 'app-1' } as CurrentTryAppParams,
}}
>
<AppList />
</ExploreContext.Provider>
</NuqsTestingAdapter>,
)
fireEvent.click(screen.getByTestId('try-app-create'))
await waitFor(() => {
expect(screen.getByTestId('create-app-modal')).toBeInTheDocument()
})
})
it('should render try app panel with empty appId when currentApp is undefined', () => {
mockExploreData = {
categories: ['Writing'],
allList: [createApp()],
}
render(
<NuqsTestingAdapter>
<ExploreContext.Provider
value={{
controlUpdateInstalledApps: 0,
setControlUpdateInstalledApps: vi.fn(),
hasEditPermission: true,
installedApps: [],
setInstalledApps: vi.fn(),
isFetchingInstalledApps: false,
setIsFetchingInstalledApps: vi.fn(),
isShowTryAppPanel: true,
setShowTryAppPanel: vi.fn(),
}}
>
<AppList />
</ExploreContext.Provider>
</NuqsTestingAdapter>,
)
expect(screen.getByTestId('try-app-panel')).toBeInTheDocument()
})
})
describe('Banner', () => {
it('should render banner when enable_explore_banner is true', () => {
useGlobalPublicStore.setState({
systemFeatures: {
...useGlobalPublicStore.getState().systemFeatures,
enable_explore_banner: true,
},
})
mockExploreData = {
categories: ['Writing'],
allList: [createApp()],
}
renderWithContext()
expect(screen.getByTestId('explore-banner')).toBeInTheDocument()
})
})
})

View File

@ -4,6 +4,9 @@ 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 {