mirror of
https://github.com/langgenius/dify.git
synced 2026-05-02 08:28:03 +08:00
refactor(tests): simplify App Card operations flow tests by extracting common menu opening logic
This commit is contained in:
@ -258,6 +258,10 @@ const renderAppCard = (app?: Partial<App>) => {
|
||||
return render(<AppCard app={createMockApp(app)} onRefresh={mockOnRefresh} />)
|
||||
}
|
||||
|
||||
const openOperationsMenu = () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.more' }))
|
||||
}
|
||||
|
||||
describe('App Card Operations Flow', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@ -313,32 +317,19 @@ describe('App Card Operations Flow', () => {
|
||||
it('should show delete confirmation and call API on confirm', async () => {
|
||||
renderAppCard({ id: 'app-to-delete', name: 'Deletable App' })
|
||||
|
||||
// Find and click the more button (popover trigger)
|
||||
const moreIcons = document.querySelectorAll('svg')
|
||||
const moreFill = Array.from(moreIcons).find(svg => svg.closest('[class*="cursor-pointer"]'))
|
||||
openOperationsMenu()
|
||||
fireEvent.click(await screen.findByText('common.operation.delete'))
|
||||
|
||||
if (moreFill) {
|
||||
const btn = moreFill.closest('[class*="cursor-pointer"]')
|
||||
if (btn)
|
||||
fireEvent.click(btn)
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('app.deleteAppConfirmTitle')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
const deleteBtn = screen.queryByText('common.operation.delete')
|
||||
if (deleteBtn)
|
||||
fireEvent.click(deleteBtn)
|
||||
})
|
||||
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'Deletable App' } })
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.confirm' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('app.deleteAppConfirmTitle')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'Deletable App' } })
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.confirm' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockDeleteAppMutation).toHaveBeenCalledWith('app-to-delete')
|
||||
})
|
||||
}
|
||||
await waitFor(() => {
|
||||
expect(mockDeleteAppMutation).toHaveBeenCalledWith('app-to-delete')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -347,34 +338,18 @@ describe('App Card Operations Flow', () => {
|
||||
it('should open edit modal and call updateAppInfo on confirm', async () => {
|
||||
renderAppCard({ id: 'app-edit', name: 'Editable App' })
|
||||
|
||||
const moreIcons = document.querySelectorAll('svg')
|
||||
const moreFill = Array.from(moreIcons).find(svg => svg.closest('[class*="cursor-pointer"]'))
|
||||
openOperationsMenu()
|
||||
fireEvent.click(await screen.findByText('app.editApp'))
|
||||
fireEvent.click(await screen.findByTestId('confirm-edit'))
|
||||
|
||||
if (moreFill) {
|
||||
const btn = moreFill.closest('[class*="cursor-pointer"]')
|
||||
if (btn)
|
||||
fireEvent.click(btn)
|
||||
|
||||
await waitFor(() => {
|
||||
const editBtn = screen.queryByText('app.editApp')
|
||||
if (editBtn)
|
||||
fireEvent.click(editBtn)
|
||||
})
|
||||
|
||||
const confirmEdit = screen.queryByTestId('confirm-edit')
|
||||
if (confirmEdit) {
|
||||
fireEvent.click(confirmEdit)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(updateAppInfo).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
appID: 'app-edit',
|
||||
name: 'Updated App Name',
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
await waitFor(() => {
|
||||
expect(updateAppInfo).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
appID: 'app-edit',
|
||||
name: 'Updated App Name',
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -383,26 +358,14 @@ describe('App Card Operations Flow', () => {
|
||||
it('should call exportAppConfig for completion apps', async () => {
|
||||
renderAppCard({ id: 'app-export', mode: AppModeEnum.COMPLETION, name: 'Export App' })
|
||||
|
||||
const moreIcons = document.querySelectorAll('svg')
|
||||
const moreFill = Array.from(moreIcons).find(svg => svg.closest('[class*="cursor-pointer"]'))
|
||||
openOperationsMenu()
|
||||
fireEvent.click(await screen.findByText('app.export'))
|
||||
|
||||
if (moreFill) {
|
||||
const btn = moreFill.closest('[class*="cursor-pointer"]')
|
||||
if (btn)
|
||||
fireEvent.click(btn)
|
||||
|
||||
await waitFor(() => {
|
||||
const exportBtn = screen.queryByText('app.export')
|
||||
if (exportBtn)
|
||||
fireEvent.click(exportBtn)
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(exportAppConfig).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ appID: 'app-export' }),
|
||||
)
|
||||
})
|
||||
}
|
||||
await waitFor(() => {
|
||||
expect(exportAppConfig).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ appID: 'app-export' }),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -422,35 +385,21 @@ describe('App Card Operations Flow', () => {
|
||||
it('should show switch option for chat mode apps', async () => {
|
||||
renderAppCard({ id: 'app-switch', mode: AppModeEnum.CHAT })
|
||||
|
||||
const moreIcons = document.querySelectorAll('svg')
|
||||
const moreFill = Array.from(moreIcons).find(svg => svg.closest('[class*="cursor-pointer"]'))
|
||||
openOperationsMenu()
|
||||
|
||||
if (moreFill) {
|
||||
const btn = moreFill.closest('[class*="cursor-pointer"]')
|
||||
if (btn)
|
||||
fireEvent.click(btn)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('app.switch')).toBeInTheDocument()
|
||||
})
|
||||
}
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('app.switch')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should not show switch option for workflow apps', async () => {
|
||||
renderAppCard({ id: 'app-wf', mode: AppModeEnum.WORKFLOW, name: 'WF App' })
|
||||
|
||||
const moreIcons = document.querySelectorAll('svg')
|
||||
const moreFill = Array.from(moreIcons).find(svg => svg.closest('[class*="cursor-pointer"]'))
|
||||
openOperationsMenu()
|
||||
|
||||
if (moreFill) {
|
||||
const btn = moreFill.closest('[class*="cursor-pointer"]')
|
||||
if (btn)
|
||||
fireEvent.click(btn)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('app.switch')).not.toBeInTheDocument()
|
||||
})
|
||||
}
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('app.switch')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -176,6 +176,29 @@ const AppCardOperationsMenu: React.FC<AppCardOperationsMenuProps> = ({
|
||||
)
|
||||
}
|
||||
|
||||
type AppCardOperationsMenuContentProps = Omit<AppCardOperationsMenuProps, 'shouldShowOpenInExploreOption'>
|
||||
|
||||
const AppCardOperationsMenuContent: React.FC<AppCardOperationsMenuContentProps> = (props) => {
|
||||
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
|
||||
const { data: userCanAccessApp, isLoading: isGettingUserCanAccessApp } = useGetUserCanAccessApp({
|
||||
appId: props.app.id,
|
||||
enabled: systemFeatures.webapp_auth.enabled,
|
||||
})
|
||||
|
||||
const shouldShowOpenInExploreOption = !props.app.has_draft_trigger
|
||||
&& (
|
||||
!systemFeatures.webapp_auth.enabled
|
||||
|| (!isGettingUserCanAccessApp && Boolean(userCanAccessApp?.result))
|
||||
)
|
||||
|
||||
return (
|
||||
<AppCardOperationsMenu
|
||||
{...props}
|
||||
shouldShowOpenInExploreOption={shouldShowOpenInExploreOption}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
||||
const { t } = useTranslation()
|
||||
const deleteAppNameInputId = useId()
|
||||
@ -192,10 +215,6 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
||||
const [showAccessControl, setShowAccessControl] = useState(false)
|
||||
const [isOperationsMenuOpen, setIsOperationsMenuOpen] = useState(false)
|
||||
const [secretEnvList, setSecretEnvList] = useState<EnvironmentVariable[]>([])
|
||||
const { data: userCanAccessApp, isLoading: isGettingUserCanAccessApp } = useGetUserCanAccessApp({
|
||||
appId: app.id,
|
||||
enabled: isOperationsMenuOpen && systemFeatures.webapp_auth.enabled,
|
||||
})
|
||||
const { mutateAsync: mutateDeleteApp, isPending: isDeleting } = useDeleteAppMutation()
|
||||
|
||||
const onConfirmDelete = useCallback(async () => {
|
||||
@ -365,11 +384,6 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
||||
}, [onRefresh, setShowAccessControl])
|
||||
|
||||
const shouldShowSwitchOption = app.mode === AppModeEnum.COMPLETION || app.mode === AppModeEnum.CHAT
|
||||
const shouldShowOpenInExploreOption = !app.has_draft_trigger
|
||||
&& (
|
||||
!systemFeatures.webapp_auth.enabled
|
||||
|| (!isGettingUserCanAccessApp && Boolean(userCanAccessApp?.result))
|
||||
)
|
||||
const shouldShowAccessControlOption = systemFeatures.webapp_auth.enabled && isCurrentWorkspaceEditor
|
||||
const operationsMenuWidthClassName = shouldShowSwitchOption ? 'w-[256px]' : 'w-[216px]'
|
||||
|
||||
@ -495,24 +509,42 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
||||
<span aria-hidden className="i-ri-more-fill h-4 w-4 text-text-tertiary" />
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
placement="bottom-end"
|
||||
sideOffset={4}
|
||||
popupClassName={operationsMenuWidthClassName}
|
||||
>
|
||||
<AppCardOperationsMenu
|
||||
app={app}
|
||||
shouldShowSwitchOption={shouldShowSwitchOption}
|
||||
shouldShowOpenInExploreOption={shouldShowOpenInExploreOption}
|
||||
shouldShowAccessControlOption={shouldShowAccessControlOption}
|
||||
onEdit={handleShowEditModal}
|
||||
onDuplicate={handleShowDuplicateModal}
|
||||
onExport={exportCheck}
|
||||
onSwitch={handleShowSwitchModal}
|
||||
onDelete={handleShowDeleteConfirm}
|
||||
onAccessControl={handleShowAccessControl}
|
||||
/>
|
||||
</DropdownMenuContent>
|
||||
{isOperationsMenuOpen && (
|
||||
<DropdownMenuContent
|
||||
placement="bottom-end"
|
||||
sideOffset={4}
|
||||
popupClassName={operationsMenuWidthClassName}
|
||||
>
|
||||
{systemFeatures.webapp_auth.enabled
|
||||
? (
|
||||
<AppCardOperationsMenuContent
|
||||
app={app}
|
||||
shouldShowSwitchOption={shouldShowSwitchOption}
|
||||
shouldShowAccessControlOption={shouldShowAccessControlOption}
|
||||
onEdit={handleShowEditModal}
|
||||
onDuplicate={handleShowDuplicateModal}
|
||||
onExport={exportCheck}
|
||||
onSwitch={handleShowSwitchModal}
|
||||
onDelete={handleShowDeleteConfirm}
|
||||
onAccessControl={handleShowAccessControl}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<AppCardOperationsMenu
|
||||
app={app}
|
||||
shouldShowSwitchOption={shouldShowSwitchOption}
|
||||
shouldShowOpenInExploreOption={!app.has_draft_trigger}
|
||||
shouldShowAccessControlOption={shouldShowAccessControlOption}
|
||||
onEdit={handleShowEditModal}
|
||||
onDuplicate={handleShowDuplicateModal}
|
||||
onExport={exportCheck}
|
||||
onSwitch={handleShowSwitchModal}
|
||||
onDelete={handleShowDeleteConfirm}
|
||||
onAccessControl={handleShowAccessControl}
|
||||
/>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
)}
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button } from '@/app/components/base/ui/button'
|
||||
@ -6,7 +7,6 @@ import {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/app/components/base/ui/dropdown-menu'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import Operations from './operations'
|
||||
|
||||
type ActionsProps = {
|
||||
|
||||
@ -370,8 +370,7 @@ describe('DocumentList', () => {
|
||||
})
|
||||
}
|
||||
|
||||
// After clicking rename, the modal should potentially be visible
|
||||
expect(screen.getByRole('table')).toBeInTheDocument()
|
||||
expect(screen.getByRole('dialog', { name: 'datasetDocuments.list.table.rename' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call onUpdate when document is renamed', () => {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import * as React from 'react'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
type OperationItemProps = {
|
||||
iconClassName: string
|
||||
|
||||
Reference in New Issue
Block a user