mirror of https://github.com/langgenius/dify.git
chore: some tests (#30078)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
parent
f439e081b5
commit
dcde854c5e
|
|
@ -0,0 +1,176 @@
|
|||
import type { App, AppIconType } from '@/types/app'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { PageType } from '@/app/components/base/features/new-feature-panel/annotation-reply/type'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import LogAnnotation from './index'
|
||||
|
||||
const mockRouterPush = vi.fn()
|
||||
vi.mock('next/navigation', () => ({
|
||||
useRouter: () => ({
|
||||
push: mockRouterPush,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/app/annotation', () => ({
|
||||
__esModule: true,
|
||||
default: ({ appDetail }: { appDetail: App }) => (
|
||||
<div data-testid="annotation" data-app-id={appDetail.id} />
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/app/log', () => ({
|
||||
__esModule: true,
|
||||
default: ({ appDetail }: { appDetail: App }) => (
|
||||
<div data-testid="log" data-app-id={appDetail.id} />
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/app/workflow-log', () => ({
|
||||
__esModule: true,
|
||||
default: ({ appDetail }: { appDetail: App }) => (
|
||||
<div data-testid="workflow-log" data-app-id={appDetail.id} />
|
||||
),
|
||||
}))
|
||||
|
||||
const createMockApp = (overrides: Partial<App> = {}): App => ({
|
||||
id: 'app-123',
|
||||
name: 'Test App',
|
||||
description: 'Test app description',
|
||||
author_name: 'Test Author',
|
||||
icon_type: 'emoji' as AppIconType,
|
||||
icon: ':icon:',
|
||||
icon_background: '#FFEAD5',
|
||||
icon_url: null,
|
||||
use_icon_as_answer_icon: false,
|
||||
mode: AppModeEnum.CHAT,
|
||||
enable_site: true,
|
||||
enable_api: true,
|
||||
api_rpm: 60,
|
||||
api_rph: 3600,
|
||||
is_demo: false,
|
||||
model_config: {} as App['model_config'],
|
||||
app_model_config: {} as App['app_model_config'],
|
||||
created_at: Date.now(),
|
||||
updated_at: Date.now(),
|
||||
site: {
|
||||
access_token: 'token',
|
||||
app_base_url: 'https://example.com',
|
||||
} as App['site'],
|
||||
api_base_url: 'https://api.example.com',
|
||||
tags: [],
|
||||
access_mode: 'public_access' as App['access_mode'],
|
||||
...overrides,
|
||||
})
|
||||
|
||||
describe('LogAnnotation', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
useAppStore.setState({ appDetail: createMockApp() })
|
||||
})
|
||||
|
||||
// Rendering behavior
|
||||
describe('Rendering', () => {
|
||||
it('should render loading state when app detail is missing', () => {
|
||||
// Arrange
|
||||
useAppStore.setState({ appDetail: undefined })
|
||||
|
||||
// Act
|
||||
render(<LogAnnotation pageType={PageType.log} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByRole('status')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render log and annotation tabs for non-completion apps', () => {
|
||||
// Arrange
|
||||
useAppStore.setState({ appDetail: createMockApp({ mode: AppModeEnum.CHAT }) })
|
||||
|
||||
// Act
|
||||
render(<LogAnnotation pageType={PageType.log} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('appLog.title')).toBeInTheDocument()
|
||||
expect(screen.getByText('appAnnotation.title')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render only log tab for completion apps', () => {
|
||||
// Arrange
|
||||
useAppStore.setState({ appDetail: createMockApp({ mode: AppModeEnum.COMPLETION }) })
|
||||
|
||||
// Act
|
||||
render(<LogAnnotation pageType={PageType.log} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('appLog.title')).toBeInTheDocument()
|
||||
expect(screen.queryByText('appAnnotation.title')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should hide tabs and render workflow log in workflow mode', () => {
|
||||
// Arrange
|
||||
useAppStore.setState({ appDetail: createMockApp({ mode: AppModeEnum.WORKFLOW }) })
|
||||
|
||||
// Act
|
||||
render(<LogAnnotation pageType={PageType.log} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByText('appLog.title')).not.toBeInTheDocument()
|
||||
expect(screen.getByTestId('workflow-log')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Prop-driven behavior
|
||||
describe('Props', () => {
|
||||
it('should render log content when page type is log', () => {
|
||||
// Arrange
|
||||
useAppStore.setState({ appDetail: createMockApp({ mode: AppModeEnum.CHAT }) })
|
||||
|
||||
// Act
|
||||
render(<LogAnnotation pageType={PageType.log} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('log')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('annotation')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render annotation content when page type is annotation', () => {
|
||||
// Arrange
|
||||
useAppStore.setState({ appDetail: createMockApp({ mode: AppModeEnum.CHAT }) })
|
||||
|
||||
// Act
|
||||
render(<LogAnnotation pageType={PageType.annotation} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('annotation')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('log')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// User interaction behavior
|
||||
describe('User Interactions', () => {
|
||||
it('should navigate to annotations when switching from log tab', async () => {
|
||||
// Arrange
|
||||
const user = userEvent.setup()
|
||||
|
||||
// Act
|
||||
render(<LogAnnotation pageType={PageType.log} />)
|
||||
await user.click(screen.getByText('appAnnotation.title'))
|
||||
|
||||
// Assert
|
||||
expect(mockRouterPush).toHaveBeenCalledWith('/app/app-123/annotations')
|
||||
})
|
||||
|
||||
it('should navigate to logs when switching from annotation tab', async () => {
|
||||
// Arrange
|
||||
const user = userEvent.setup()
|
||||
|
||||
// Act
|
||||
render(<LogAnnotation pageType={PageType.annotation} />)
|
||||
await user.click(screen.getByText('appLog.title'))
|
||||
|
||||
// Assert
|
||||
expect(mockRouterPush).toHaveBeenCalledWith('/app/app-123/logs')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -110,7 +110,11 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(({
|
|||
{...props}
|
||||
/>
|
||||
{showClearIcon && value && !disabled && !destructive && (
|
||||
<div className={cn('group absolute right-2 top-1/2 -translate-y-1/2 cursor-pointer p-[1px]')} onClick={onClear}>
|
||||
<div
|
||||
className={cn('group absolute right-2 top-1/2 -translate-y-1/2 cursor-pointer p-[1px]')}
|
||||
onClick={onClear}
|
||||
data-testid="input-clear"
|
||||
>
|
||||
<RiCloseCircleFill className="h-3.5 w-3.5 cursor-pointer text-text-quaternary group-hover:text-text-tertiary" />
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,125 @@
|
|||
import type { Mock } from 'vitest'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { createMockPlan } from '@/__mocks__/provider-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { Plan } from '../type'
|
||||
import PriorityLabel from './index'
|
||||
|
||||
vi.mock('@/context/provider-context', () => ({
|
||||
useProviderContext: vi.fn(),
|
||||
}))
|
||||
|
||||
const useProviderContextMock = useProviderContext as Mock
|
||||
|
||||
const setupPlan = (planType: Plan) => {
|
||||
useProviderContextMock.mockReturnValue(createMockPlan(planType))
|
||||
}
|
||||
|
||||
describe('PriorityLabel', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// Rendering: basic label output for sandbox plan.
|
||||
describe('Rendering', () => {
|
||||
it('should render the standard priority label when plan is sandbox', () => {
|
||||
// Arrange
|
||||
setupPlan(Plan.sandbox)
|
||||
|
||||
// Act
|
||||
render(<PriorityLabel />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('billing.plansCommon.priority.standard')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Props: custom class name applied to the label container.
|
||||
describe('Props', () => {
|
||||
it('should apply custom className to the label container', () => {
|
||||
// Arrange
|
||||
setupPlan(Plan.sandbox)
|
||||
|
||||
// Act
|
||||
render(<PriorityLabel className="custom-class" />)
|
||||
|
||||
// Assert
|
||||
const label = screen.getByText('billing.plansCommon.priority.standard').closest('div')
|
||||
expect(label).toHaveClass('custom-class')
|
||||
})
|
||||
})
|
||||
|
||||
// Plan types: label text and icon visibility for different plans.
|
||||
describe('Plan Types', () => {
|
||||
it('should render priority label and icon when plan is professional', () => {
|
||||
// Arrange
|
||||
setupPlan(Plan.professional)
|
||||
|
||||
// Act
|
||||
const { container } = render(<PriorityLabel />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('billing.plansCommon.priority.priority')).toBeInTheDocument()
|
||||
expect(container.querySelector('svg')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render top priority label and icon when plan is team', () => {
|
||||
// Arrange
|
||||
setupPlan(Plan.team)
|
||||
|
||||
// Act
|
||||
const { container } = render(<PriorityLabel />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('billing.plansCommon.priority.top-priority')).toBeInTheDocument()
|
||||
expect(container.querySelector('svg')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render standard label without icon when plan is sandbox', () => {
|
||||
// Arrange
|
||||
setupPlan(Plan.sandbox)
|
||||
|
||||
// Act
|
||||
const { container } = render(<PriorityLabel />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('billing.plansCommon.priority.standard')).toBeInTheDocument()
|
||||
expect(container.querySelector('svg')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Edge cases: tooltip content varies by priority level.
|
||||
describe('Edge Cases', () => {
|
||||
it('should show the tip text when priority is not top priority', async () => {
|
||||
// Arrange
|
||||
setupPlan(Plan.sandbox)
|
||||
|
||||
// Act
|
||||
render(<PriorityLabel />)
|
||||
const label = screen.getByText('billing.plansCommon.priority.standard').closest('div')
|
||||
fireEvent.mouseEnter(label as HTMLElement)
|
||||
|
||||
// Assert
|
||||
expect(await screen.findByText(
|
||||
'billing.plansCommon.documentProcessingPriority: billing.plansCommon.priority.standard',
|
||||
)).toBeInTheDocument()
|
||||
expect(screen.getByText('billing.plansCommon.documentProcessingPriorityTip')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should hide the tip text when priority is top priority', async () => {
|
||||
// Arrange
|
||||
setupPlan(Plan.enterprise)
|
||||
|
||||
// Act
|
||||
render(<PriorityLabel />)
|
||||
const label = screen.getByText('billing.plansCommon.priority.top-priority').closest('div')
|
||||
fireEvent.mouseEnter(label as HTMLElement)
|
||||
|
||||
// Assert
|
||||
expect(await screen.findByText(
|
||||
'billing.plansCommon.documentProcessingPriority: billing.plansCommon.priority.top-priority',
|
||||
)).toBeInTheDocument()
|
||||
expect(screen.queryByText('billing.plansCommon.documentProcessingPriorityTip')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,271 @@
|
|||
import type { Mock } from 'vitest'
|
||||
import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal'
|
||||
import type { App } from '@/models/explore'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import ExploreContext from '@/context/explore-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 mockSWRData: { categories: string[], allList: App[] } = { categories: [], allList: [] }
|
||||
const mockHandleImportDSL = vi.fn()
|
||||
const mockHandleImportDSLConfirm = vi.fn()
|
||||
|
||||
vi.mock('@/hooks/use-tab-searchparams', () => ({
|
||||
useTabSearchParams: () => [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('swr', () => ({
|
||||
__esModule: true,
|
||||
default: () => ({ data: mockSWRData }),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/explore', () => ({
|
||||
fetchAppDetail: vi.fn(),
|
||||
fetchAppList: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-import-dsl', () => ({
|
||||
useImportDSL: () => ({
|
||||
handleImportDSL: mockHandleImportDSL,
|
||||
handleImportDSLConfirm: mockHandleImportDSLConfirm,
|
||||
versions: ['v1'],
|
||||
isFetching: false,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/explore/create-app-modal', () => ({
|
||||
__esModule: true,
|
||||
default: (props: CreateAppModalProps) => {
|
||||
if (!props.show)
|
||||
return null
|
||||
return (
|
||||
<div data-testid="create-app-modal">
|
||||
<button
|
||||
data-testid="confirm-create"
|
||||
onClick={() => props.onConfirm({
|
||||
name: 'New App',
|
||||
icon_type: 'emoji',
|
||||
icon: '🤖',
|
||||
icon_background: '#fff',
|
||||
description: 'desc',
|
||||
})}
|
||||
>
|
||||
confirm
|
||||
</button>
|
||||
<button data-testid="hide-create" onClick={props.onHide}>hide</button>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/app/create-from-dsl-modal/dsl-confirm-modal', () => ({
|
||||
__esModule: true,
|
||||
default: ({ onConfirm, onCancel }: { onConfirm: () => void, onCancel: () => void }) => (
|
||||
<div data-testid="dsl-confirm-modal">
|
||||
<button data-testid="dsl-confirm" onClick={onConfirm}>confirm</button>
|
||||
<button data-testid="dsl-cancel" onClick={onCancel}>cancel</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
const createApp = (overrides: Partial<App> = {}): App => ({
|
||||
app: {
|
||||
id: overrides.app?.id ?? 'app-basic-id',
|
||||
mode: overrides.app?.mode ?? AppModeEnum.CHAT,
|
||||
icon_type: overrides.app?.icon_type ?? 'emoji',
|
||||
icon: overrides.app?.icon ?? '😀',
|
||||
icon_background: overrides.app?.icon_background ?? '#fff',
|
||||
icon_url: overrides.app?.icon_url ?? '',
|
||||
name: overrides.app?.name ?? 'Alpha',
|
||||
description: overrides.app?.description ?? 'Alpha description',
|
||||
use_icon_as_answer_icon: overrides.app?.use_icon_as_answer_icon ?? false,
|
||||
},
|
||||
app_id: overrides.app_id ?? 'app-1',
|
||||
description: overrides.description ?? 'Alpha description',
|
||||
copyright: overrides.copyright ?? '',
|
||||
privacy_policy: overrides.privacy_policy ?? null,
|
||||
custom_disclaimer: overrides.custom_disclaimer ?? null,
|
||||
category: overrides.category ?? 'Writing',
|
||||
position: overrides.position ?? 1,
|
||||
is_listed: overrides.is_listed ?? true,
|
||||
install_count: overrides.install_count ?? 0,
|
||||
installed: overrides.installed ?? false,
|
||||
editable: overrides.editable ?? false,
|
||||
is_agent: overrides.is_agent ?? false,
|
||||
})
|
||||
|
||||
const renderWithContext = (hasEditPermission = false, onSuccess?: () => void) => {
|
||||
return render(
|
||||
<ExploreContext.Provider
|
||||
value={{
|
||||
controlUpdateInstalledApps: 0,
|
||||
setControlUpdateInstalledApps: vi.fn(),
|
||||
hasEditPermission,
|
||||
installedApps: [],
|
||||
setInstalledApps: vi.fn(),
|
||||
isFetchingInstalledApps: false,
|
||||
setIsFetchingInstalledApps: vi.fn(),
|
||||
}}
|
||||
>
|
||||
<AppList onSuccess={onSuccess} />
|
||||
</ExploreContext.Provider>,
|
||||
)
|
||||
}
|
||||
|
||||
describe('AppList', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockTabValue = allCategoriesEn
|
||||
mockSWRData = { categories: [], allList: [] }
|
||||
})
|
||||
|
||||
// Rendering: show loading when categories are not ready.
|
||||
describe('Rendering', () => {
|
||||
it('should render loading when categories are empty', () => {
|
||||
// Arrange
|
||||
mockSWRData = { categories: [], allList: [] }
|
||||
|
||||
// Act
|
||||
renderWithContext()
|
||||
|
||||
// Assert
|
||||
expect(screen.getByRole('status')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render app cards when data is available', () => {
|
||||
// Arrange
|
||||
mockSWRData = {
|
||||
categories: ['Writing', 'Translate'],
|
||||
allList: [createApp(), createApp({ app_id: 'app-2', app: { ...createApp().app, name: 'Beta' }, category: 'Translate' })],
|
||||
}
|
||||
|
||||
// Act
|
||||
renderWithContext()
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Alpha')).toBeInTheDocument()
|
||||
expect(screen.getByText('Beta')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Props: category selection filters the list.
|
||||
describe('Props', () => {
|
||||
it('should filter apps by selected category', () => {
|
||||
// Arrange
|
||||
mockTabValue = 'Writing'
|
||||
mockSWRData = {
|
||||
categories: ['Writing', 'Translate'],
|
||||
allList: [createApp(), createApp({ app_id: 'app-2', app: { ...createApp().app, name: 'Beta' }, category: 'Translate' })],
|
||||
}
|
||||
|
||||
// Act
|
||||
renderWithContext()
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Alpha')).toBeInTheDocument()
|
||||
expect(screen.queryByText('Beta')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// User interactions: search and create flow.
|
||||
describe('User Interactions', () => {
|
||||
it('should filter apps by search keywords', async () => {
|
||||
// Arrange
|
||||
mockSWRData = {
|
||||
categories: ['Writing'],
|
||||
allList: [createApp(), createApp({ app_id: 'app-2', app: { ...createApp().app, name: 'Gamma' } })],
|
||||
}
|
||||
renderWithContext()
|
||||
|
||||
// Act
|
||||
const input = screen.getByPlaceholderText('common.operation.search')
|
||||
fireEvent.change(input, { target: { value: 'gam' } })
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Alpha')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('Gamma')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle create flow and confirm DSL when pending', async () => {
|
||||
// Arrange
|
||||
const onSuccess = vi.fn()
|
||||
mockSWRData = {
|
||||
categories: ['Writing'],
|
||||
allList: [createApp()],
|
||||
};
|
||||
(fetchAppDetail as unknown as Mock).mockResolvedValue({ export_data: 'yaml-content' })
|
||||
mockHandleImportDSL.mockImplementation(async (_payload: unknown, options: { onSuccess?: () => void, onPending?: () => void }) => {
|
||||
options.onPending?.()
|
||||
})
|
||||
mockHandleImportDSLConfirm.mockImplementation(async (options: { onSuccess?: () => void }) => {
|
||||
options.onSuccess?.()
|
||||
})
|
||||
|
||||
// Act
|
||||
renderWithContext(true, onSuccess)
|
||||
fireEvent.click(screen.getByText('explore.appCard.addToWorkspace'))
|
||||
fireEvent.click(await screen.findByTestId('confirm-create'))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(fetchAppDetail).toHaveBeenCalledWith('app-basic-id')
|
||||
})
|
||||
expect(mockHandleImportDSL).toHaveBeenCalledTimes(1)
|
||||
expect(await screen.findByTestId('dsl-confirm-modal')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByTestId('dsl-confirm'))
|
||||
await waitFor(() => {
|
||||
expect(mockHandleImportDSLConfirm).toHaveBeenCalledTimes(1)
|
||||
expect(onSuccess).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Edge cases: handle clearing search keywords.
|
||||
describe('Edge Cases', () => {
|
||||
it('should reset search results when clear icon is clicked', async () => {
|
||||
// Arrange
|
||||
mockSWRData = {
|
||||
categories: ['Writing'],
|
||||
allList: [createApp(), createApp({ app_id: 'app-2', app: { ...createApp().app, name: 'Gamma' } })],
|
||||
}
|
||||
renderWithContext()
|
||||
|
||||
// Act
|
||||
const input = screen.getByPlaceholderText('common.operation.search')
|
||||
fireEvent.change(input, { target: { value: 'gam' } })
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Alpha')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByTestId('input-clear'))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Alpha')).toBeInTheDocument()
|
||||
expect(screen.getByText('Gamma')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
import type { AppCategory } from '@/models/explore'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import Category from './category'
|
||||
|
||||
describe('Category', () => {
|
||||
const allCategoriesEn = 'Recommended'
|
||||
|
||||
const renderComponent = (overrides: Partial<React.ComponentProps<typeof Category>> = {}) => {
|
||||
const props: React.ComponentProps<typeof Category> = {
|
||||
list: ['Writing', 'Recommended'] as AppCategory[],
|
||||
value: allCategoriesEn,
|
||||
onChange: vi.fn(),
|
||||
allCategoriesEn,
|
||||
...overrides,
|
||||
}
|
||||
return {
|
||||
props,
|
||||
...render(<Category {...props} />),
|
||||
}
|
||||
}
|
||||
|
||||
// Rendering: basic categories and all-categories button.
|
||||
describe('Rendering', () => {
|
||||
it('should render all categories item and translated categories', () => {
|
||||
// Arrange
|
||||
renderComponent()
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('explore.apps.allCategories')).toBeInTheDocument()
|
||||
expect(screen.getByText('explore.category.Writing')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render allCategoriesEn again inside the category list', () => {
|
||||
// Arrange
|
||||
renderComponent()
|
||||
|
||||
// Assert
|
||||
const recommendedItems = screen.getAllByText('explore.apps.allCategories')
|
||||
expect(recommendedItems).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
// Props: clicking items triggers onChange.
|
||||
describe('Props', () => {
|
||||
it('should call onChange with category value when category item is clicked', () => {
|
||||
// Arrange
|
||||
const { props } = renderComponent()
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByText('explore.category.Writing'))
|
||||
|
||||
// Assert
|
||||
expect(props.onChange).toHaveBeenCalledWith('Writing')
|
||||
})
|
||||
|
||||
it('should call onChange with allCategoriesEn when all categories is clicked', () => {
|
||||
// Arrange
|
||||
const { props } = renderComponent({ value: 'Writing' })
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByText('explore.apps.allCategories'))
|
||||
|
||||
// Assert
|
||||
expect(props.onChange).toHaveBeenCalledWith(allCategoriesEn)
|
||||
})
|
||||
})
|
||||
|
||||
// Edge cases: handle values not in the list.
|
||||
describe('Edge Cases', () => {
|
||||
it('should treat unknown value as all categories selection', () => {
|
||||
// Arrange
|
||||
renderComponent({ value: 'Unknown' })
|
||||
|
||||
// Assert
|
||||
const allCategoriesItem = screen.getByText('explore.apps.allCategories')
|
||||
expect(allCategoriesItem.className).toContain('bg-components-main-nav-nav-button-bg-active')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
import type { Mock } from 'vitest'
|
||||
import { 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'
|
||||
import { MediaType } from '@/hooks/use-breakpoints'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import { useMembers } from '@/service/use-common'
|
||||
import Explore from './index'
|
||||
|
||||
const mockReplace = vi.fn()
|
||||
const mockPush = vi.fn()
|
||||
const mockInstalledAppsData = { installed_apps: [] as const }
|
||||
|
||||
vi.mock('next/navigation', () => ({
|
||||
useRouter: () => ({
|
||||
replace: mockReplace,
|
||||
push: mockPush,
|
||||
}),
|
||||
useSelectedLayoutSegments: () => ['apps'],
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-breakpoints', () => ({
|
||||
__esModule: true,
|
||||
default: () => MediaType.pc,
|
||||
MediaType: {
|
||||
mobile: 'mobile',
|
||||
tablet: 'tablet',
|
||||
pc: 'pc',
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-explore', () => ({
|
||||
useGetInstalledApps: () => ({
|
||||
isFetching: false,
|
||||
data: mockInstalledAppsData,
|
||||
refetch: vi.fn(),
|
||||
}),
|
||||
useUninstallApp: () => ({
|
||||
mutateAsync: vi.fn(),
|
||||
}),
|
||||
useUpdateAppPinStatus: () => ({
|
||||
mutateAsync: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context', () => ({
|
||||
useAppContext: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-common', () => ({
|
||||
useMembers: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-document-title', () => ({
|
||||
__esModule: true,
|
||||
default: vi.fn(),
|
||||
}))
|
||||
|
||||
const ContextReader = () => {
|
||||
const { hasEditPermission } = useContext(ExploreContext)
|
||||
return <div>{hasEditPermission ? 'edit-yes' : 'edit-no'}</div>
|
||||
}
|
||||
|
||||
describe('Explore', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// Rendering: provides ExploreContext and children.
|
||||
describe('Rendering', () => {
|
||||
it('should render children and provide edit permission from members role', async () => {
|
||||
// Arrange
|
||||
; (useAppContext as Mock).mockReturnValue({
|
||||
userProfile: { id: 'user-1' },
|
||||
isCurrentWorkspaceDatasetOperator: false,
|
||||
});
|
||||
(useMembers as Mock).mockReturnValue({
|
||||
data: {
|
||||
accounts: [{ id: 'user-1', role: 'admin' }],
|
||||
},
|
||||
})
|
||||
|
||||
// Act
|
||||
render((
|
||||
<Explore>
|
||||
<ContextReader />
|
||||
</Explore>
|
||||
))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('edit-yes')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Effects: set document title and redirect dataset operators.
|
||||
describe('Effects', () => {
|
||||
it('should set document title on render', () => {
|
||||
// Arrange
|
||||
; (useAppContext as Mock).mockReturnValue({
|
||||
userProfile: { id: 'user-1' },
|
||||
isCurrentWorkspaceDatasetOperator: false,
|
||||
});
|
||||
(useMembers as Mock).mockReturnValue({ data: { accounts: [] } })
|
||||
|
||||
// Act
|
||||
render((
|
||||
<Explore>
|
||||
<div>child</div>
|
||||
</Explore>
|
||||
))
|
||||
|
||||
// Assert
|
||||
expect(useDocumentTitle).toHaveBeenCalledWith('common.menus.explore')
|
||||
})
|
||||
|
||||
it('should redirect dataset operators to /datasets', async () => {
|
||||
// Arrange
|
||||
; (useAppContext as Mock).mockReturnValue({
|
||||
userProfile: { id: 'user-1' },
|
||||
isCurrentWorkspaceDatasetOperator: true,
|
||||
});
|
||||
(useMembers as Mock).mockReturnValue({ data: { accounts: [] } })
|
||||
|
||||
// Act
|
||||
render((
|
||||
<Explore>
|
||||
<div>child</div>
|
||||
</Explore>
|
||||
))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockReplace).toHaveBeenCalledWith('/datasets')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import ItemOperation from './index'
|
||||
|
||||
describe('ItemOperation', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
const renderComponent = (overrides: Partial<React.ComponentProps<typeof ItemOperation>> = {}) => {
|
||||
const props: React.ComponentProps<typeof ItemOperation> = {
|
||||
isPinned: false,
|
||||
isShowDelete: true,
|
||||
togglePin: vi.fn(),
|
||||
onDelete: vi.fn(),
|
||||
...overrides,
|
||||
}
|
||||
return {
|
||||
props,
|
||||
...render(<ItemOperation {...props} />),
|
||||
}
|
||||
}
|
||||
|
||||
// Rendering: menu items show after opening.
|
||||
describe('Rendering', () => {
|
||||
it('should render pin and delete actions when menu is open', async () => {
|
||||
// Arrange
|
||||
renderComponent()
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByTestId('item-operation-trigger'))
|
||||
|
||||
// Assert
|
||||
expect(await screen.findByText('explore.sidebar.action.pin')).toBeInTheDocument()
|
||||
expect(screen.getByText('explore.sidebar.action.delete')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Props: render optional rename action and pinned label text.
|
||||
describe('Props', () => {
|
||||
it('should render rename action when isShowRenameConversation is true', async () => {
|
||||
// Arrange
|
||||
renderComponent({ isShowRenameConversation: true })
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByTestId('item-operation-trigger'))
|
||||
|
||||
// Assert
|
||||
expect(await screen.findByText('explore.sidebar.action.rename')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render unpin label when isPinned is true', async () => {
|
||||
// Arrange
|
||||
renderComponent({ isPinned: true })
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByTestId('item-operation-trigger'))
|
||||
|
||||
// Assert
|
||||
expect(await screen.findByText('explore.sidebar.action.unpin')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// User interactions: clicking action items triggers callbacks.
|
||||
describe('User Interactions', () => {
|
||||
it('should call togglePin when clicking pin action', async () => {
|
||||
// Arrange
|
||||
const { props } = renderComponent()
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByTestId('item-operation-trigger'))
|
||||
fireEvent.click(await screen.findByText('explore.sidebar.action.pin'))
|
||||
|
||||
// Assert
|
||||
expect(props.togglePin).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should call onDelete when clicking delete action', async () => {
|
||||
// Arrange
|
||||
const { props } = renderComponent()
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByTestId('item-operation-trigger'))
|
||||
fireEvent.click(await screen.findByText('explore.sidebar.action.delete'))
|
||||
|
||||
// Assert
|
||||
expect(props.onDelete).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
// Edge cases: menu closes after mouse leave when no hovering state remains.
|
||||
describe('Edge Cases', () => {
|
||||
it('should close the menu when mouse leaves the panel and item is not hovering', async () => {
|
||||
// Arrange
|
||||
renderComponent()
|
||||
fireEvent.click(screen.getByTestId('item-operation-trigger'))
|
||||
const pinText = await screen.findByText('explore.sidebar.action.pin')
|
||||
const menu = pinText.closest('div')?.parentElement as HTMLElement
|
||||
|
||||
// Act
|
||||
fireEvent.mouseEnter(menu)
|
||||
fireEvent.mouseLeave(menu)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('explore.sidebar.action.pin')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -53,7 +53,11 @@ const ItemOperation: FC<IItemOperationProps> = ({
|
|||
<PortalToFollowElemTrigger
|
||||
onClick={() => setOpen(v => !v)}
|
||||
>
|
||||
<div className={cn(className, s.btn, 'h-6 w-6 rounded-md border-none py-1', (isItemHovering || open) && `${s.open} !bg-components-actionbar-bg !shadow-none`)}></div>
|
||||
<div
|
||||
className={cn(className, s.btn, 'h-6 w-6 rounded-md border-none py-1', (isItemHovering || open) && `${s.open} !bg-components-actionbar-bg !shadow-none`)}
|
||||
data-testid="item-operation-trigger"
|
||||
>
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent
|
||||
className="z-50"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,99 @@
|
|||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import AppNavItem from './index'
|
||||
|
||||
const mockPush = vi.fn()
|
||||
|
||||
vi.mock('next/navigation', () => ({
|
||||
useRouter: () => ({
|
||||
push: mockPush,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('ahooks', async () => {
|
||||
const actual = await vi.importActual<typeof import('ahooks')>('ahooks')
|
||||
return {
|
||||
...actual,
|
||||
useHover: () => false,
|
||||
}
|
||||
})
|
||||
|
||||
const baseProps = {
|
||||
isMobile: false,
|
||||
name: 'My App',
|
||||
id: 'app-123',
|
||||
icon_type: 'emoji' as const,
|
||||
icon: '🤖',
|
||||
icon_background: '#fff',
|
||||
icon_url: '',
|
||||
isSelected: false,
|
||||
isPinned: false,
|
||||
togglePin: vi.fn(),
|
||||
uninstallable: false,
|
||||
onDelete: vi.fn(),
|
||||
}
|
||||
|
||||
describe('AppNavItem', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// Rendering: display app name for desktop and hide for mobile.
|
||||
describe('Rendering', () => {
|
||||
it('should render name and item operation on desktop', () => {
|
||||
// Arrange
|
||||
render(<AppNavItem {...baseProps} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('My App')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('item-operation-trigger')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should hide name on mobile', () => {
|
||||
// Arrange
|
||||
render(<AppNavItem {...baseProps} isMobile />)
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByText('My App')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// User interactions: navigation and delete flow.
|
||||
describe('User Interactions', () => {
|
||||
it('should navigate to installed app when item is clicked', () => {
|
||||
// Arrange
|
||||
render(<AppNavItem {...baseProps} />)
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByText('My App'))
|
||||
|
||||
// Assert
|
||||
expect(mockPush).toHaveBeenCalledWith('/explore/installed/app-123')
|
||||
})
|
||||
|
||||
it('should call onDelete with app id when delete action is clicked', async () => {
|
||||
// Arrange
|
||||
render(<AppNavItem {...baseProps} />)
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByTestId('item-operation-trigger'))
|
||||
fireEvent.click(await screen.findByText('explore.sidebar.action.delete'))
|
||||
|
||||
// Assert
|
||||
expect(baseProps.onDelete).toHaveBeenCalledWith('app-123')
|
||||
})
|
||||
})
|
||||
|
||||
// Edge cases: hide delete when uninstallable or selected.
|
||||
describe('Edge Cases', () => {
|
||||
it('should not render delete action when app is uninstallable', () => {
|
||||
// Arrange
|
||||
render(<AppNavItem {...baseProps} uninstallable />)
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByTestId('item-operation-trigger'))
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByText('explore.sidebar.action.delete')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
import type { InstalledApp } from '@/models/explore'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import ExploreContext from '@/context/explore-context'
|
||||
import { MediaType } from '@/hooks/use-breakpoints'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import SideBar from './index'
|
||||
|
||||
const mockSegments = ['apps']
|
||||
const mockPush = vi.fn()
|
||||
const mockRefetch = vi.fn()
|
||||
const mockUninstall = vi.fn()
|
||||
const mockUpdatePinStatus = vi.fn()
|
||||
let mockIsFetching = false
|
||||
let mockInstalledApps: InstalledApp[] = []
|
||||
|
||||
vi.mock('next/navigation', () => ({
|
||||
useSelectedLayoutSegments: () => mockSegments,
|
||||
useRouter: () => ({
|
||||
push: mockPush,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-breakpoints', () => ({
|
||||
__esModule: true,
|
||||
default: () => MediaType.pc,
|
||||
MediaType: {
|
||||
mobile: 'mobile',
|
||||
tablet: 'tablet',
|
||||
pc: 'pc',
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-explore', () => ({
|
||||
useGetInstalledApps: () => ({
|
||||
isFetching: mockIsFetching,
|
||||
data: { installed_apps: mockInstalledApps },
|
||||
refetch: mockRefetch,
|
||||
}),
|
||||
useUninstallApp: () => ({
|
||||
mutateAsync: mockUninstall,
|
||||
}),
|
||||
useUpdateAppPinStatus: () => ({
|
||||
mutateAsync: mockUpdatePinStatus,
|
||||
}),
|
||||
}))
|
||||
|
||||
const createInstalledApp = (overrides: Partial<InstalledApp> = {}): InstalledApp => ({
|
||||
id: overrides.id ?? 'app-123',
|
||||
uninstallable: overrides.uninstallable ?? false,
|
||||
is_pinned: overrides.is_pinned ?? false,
|
||||
app: {
|
||||
id: overrides.app?.id ?? 'app-basic-id',
|
||||
mode: overrides.app?.mode ?? AppModeEnum.CHAT,
|
||||
icon_type: overrides.app?.icon_type ?? 'emoji',
|
||||
icon: overrides.app?.icon ?? '🤖',
|
||||
icon_background: overrides.app?.icon_background ?? '#fff',
|
||||
icon_url: overrides.app?.icon_url ?? '',
|
||||
name: overrides.app?.name ?? 'My App',
|
||||
description: overrides.app?.description ?? 'desc',
|
||||
use_icon_as_answer_icon: overrides.app?.use_icon_as_answer_icon ?? false,
|
||||
},
|
||||
})
|
||||
|
||||
const renderWithContext = (installedApps: InstalledApp[] = []) => {
|
||||
return render(
|
||||
<ExploreContext.Provider
|
||||
value={{
|
||||
controlUpdateInstalledApps: 0,
|
||||
setControlUpdateInstalledApps: vi.fn(),
|
||||
hasEditPermission: true,
|
||||
installedApps,
|
||||
setInstalledApps: vi.fn(),
|
||||
isFetchingInstalledApps: false,
|
||||
setIsFetchingInstalledApps: vi.fn(),
|
||||
}}
|
||||
>
|
||||
<SideBar controlUpdateInstalledApps={0} />
|
||||
</ExploreContext.Provider>,
|
||||
)
|
||||
}
|
||||
|
||||
describe('SideBar', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockIsFetching = false
|
||||
mockInstalledApps = []
|
||||
vi.spyOn(Toast, 'notify').mockImplementation(() => ({ clear: vi.fn() }))
|
||||
})
|
||||
|
||||
// Rendering: show discovery and workspace section.
|
||||
describe('Rendering', () => {
|
||||
it('should render workspace items when installed apps exist', () => {
|
||||
// Arrange
|
||||
mockInstalledApps = [createInstalledApp()]
|
||||
|
||||
// Act
|
||||
renderWithContext(mockInstalledApps)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('explore.sidebar.discovery')).toBeInTheDocument()
|
||||
expect(screen.getByText('explore.sidebar.workspace')).toBeInTheDocument()
|
||||
expect(screen.getByText('My App')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Effects: refresh and sync installed apps state.
|
||||
describe('Effects', () => {
|
||||
it('should refetch installed apps on mount', () => {
|
||||
// Arrange
|
||||
mockInstalledApps = [createInstalledApp()]
|
||||
|
||||
// Act
|
||||
renderWithContext(mockInstalledApps)
|
||||
|
||||
// Assert
|
||||
expect(mockRefetch).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
// User interactions: delete and pin flows.
|
||||
describe('User Interactions', () => {
|
||||
it('should uninstall app and show toast when delete is confirmed', async () => {
|
||||
// Arrange
|
||||
mockInstalledApps = [createInstalledApp()]
|
||||
mockUninstall.mockResolvedValue(undefined)
|
||||
renderWithContext(mockInstalledApps)
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByTestId('item-operation-trigger'))
|
||||
fireEvent.click(await screen.findByText('explore.sidebar.action.delete'))
|
||||
fireEvent.click(await screen.findByText('common.operation.confirm'))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockUninstall).toHaveBeenCalledWith('app-123')
|
||||
expect(Toast.notify).toHaveBeenCalledWith(expect.objectContaining({
|
||||
type: 'success',
|
||||
message: 'common.api.remove',
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
it('should update pin status and show toast when pin is clicked', async () => {
|
||||
// Arrange
|
||||
mockInstalledApps = [createInstalledApp({ is_pinned: false })]
|
||||
mockUpdatePinStatus.mockResolvedValue(undefined)
|
||||
renderWithContext(mockInstalledApps)
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByTestId('item-operation-trigger'))
|
||||
fireEvent.click(await screen.findByText('explore.sidebar.action.pin'))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockUpdatePinStatus).toHaveBeenCalledWith({ appId: 'app-123', isPinned: true })
|
||||
expect(Toast.notify).toHaveBeenCalledWith(expect.objectContaining({
|
||||
type: 'success',
|
||||
message: 'common.api.success',
|
||||
}))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
Reference in New Issue