chore: some tests (#30078)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Joel
2025-12-24 14:45:33 +08:00
committed by GitHub
parent f439e081b5
commit dcde854c5e
10 changed files with 1173 additions and 2 deletions

View File

@ -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()
})
})
})
})

View File

@ -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')
})
})
})

View File

@ -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')
})
})
})
})

View File

@ -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()
})
})
})
})

View File

@ -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"

View File

@ -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()
})
})
})

View File

@ -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',
}))
})
})
})
})