fix(web): update tests for AlertDialog migration and component API changes

- Replace deprecated Confirm mock with real AlertDialog role-based queries
- Add useInvalidateCheckInstalled mock for QueryClient dependency
- Wrap model-list-item renders in QueryClientProvider
- Migrate PluginVersionPicker from PortalToFollowElem to Popover
- Migrate UpdatePluginModal from Modal to Dialog
- Update version picker offset props (sideOffset/alignOffset)
This commit is contained in:
yyh
2026-03-04 22:52:21 +08:00
parent 22a4100dd7
commit 62631658e9
7 changed files with 157 additions and 208 deletions

View File

@ -1,9 +1,17 @@
import type { ModelItem, ModelProvider } from '../declarations'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { disableModel, enableModel } from '@/service/common'
import { ModelStatusEnum } from '../declarations'
import ModelListItem from './model-list-item'
function createWrapper() {
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } })
return ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
)
}
let mockModelLoadBalancingEnabled = false
vi.mock('@/context/app-context', () => ({
@ -69,6 +77,7 @@ describe('ModelListItem', () => {
provider={mockProvider}
isConfigurable={false}
/>,
{ wrapper: createWrapper() },
)
expect(screen.getByTestId('model-icon')).toBeInTheDocument()
expect(screen.getByTestId('model-name')).toBeInTheDocument()
@ -83,6 +92,7 @@ describe('ModelListItem', () => {
isConfigurable={false}
onChange={onChange}
/>,
{ wrapper: createWrapper() },
)
fireEvent.click(screen.getByRole('switch'))
@ -102,6 +112,7 @@ describe('ModelListItem', () => {
isConfigurable={false}
onChange={onChange}
/>,
{ wrapper: createWrapper() },
)
fireEvent.click(screen.getByRole('switch'))
@ -122,6 +133,7 @@ describe('ModelListItem', () => {
isConfigurable={false}
onModifyLoadBalancing={onModifyLoadBalancing}
/>,
{ wrapper: createWrapper() },
)
fireEvent.click(screen.getByRole('button', { name: 'modify load balancing' }))

View File

@ -81,11 +81,10 @@ const ProviderCardActions: FC<Props> = ({ detail, onUpdate }) => {
pluginID={detail.plugin_id}
currentVersion={version}
onSelect={handleVersionSelect}
offset={{ mainAxis: 4, crossAxis: 0 }}
sideOffset={4}
alignOffset={0}
trigger={(
<button
type="button"
disabled={!isFromMarketplace}
<span
className={cn(
'relative inline-flex min-w-5 items-center justify-center gap-[3px] rounded-md border border-divider-deep bg-state-base-hover px-[5px] py-[2px] text-text-tertiary system-xs-medium-uppercase',
isFromMarketplace && 'cursor-pointer hover:bg-state-base-hover-alt',
@ -96,7 +95,7 @@ const ProviderCardActions: FC<Props> = ({ detail, onUpdate }) => {
{hasNewVersion && (
<span className="absolute -right-0.5 -top-0.5 h-1.5 w-1.5 rounded-full bg-state-destructive-solid" />
)}
</button>
</span>
)}
/>
)}

View File

@ -79,6 +79,10 @@ vi.mock('@/service/plugins', () => ({
uninstallPlugin: mockUninstallPlugin,
}))
vi.mock('@/service/use-plugins', () => ({
useInvalidateCheckInstalled: () => vi.fn(),
}))
vi.mock('@/service/use-tools', () => ({
useAllToolProviders: () => ({ data: [] }),
useInvalidateAllToolProviders: () => mockInvalidateAllToolProviders,
@ -218,23 +222,6 @@ vi.mock('../../plugin-auth', () => ({
PluginAuth: () => <div data-testid="plugin-auth" />,
}))
// Mock Confirm component
vi.mock('@/app/components/base/confirm', () => ({
default: ({ isShow, onCancel, onConfirm, isLoading }: {
isShow: boolean
onCancel: () => void
onConfirm: () => void
isLoading: boolean
}) => isShow
? (
<div data-testid="delete-confirm">
<button data-testid="confirm-cancel" onClick={onCancel}>Cancel</button>
<button data-testid="confirm-ok" onClick={onConfirm} disabled={isLoading}>Confirm</button>
</div>
)
: null,
}))
const createPluginDetail = (overrides: Partial<PluginDetail> = {}): PluginDetail => ({
id: 'test-id',
created_at: '2024-01-01',
@ -801,7 +788,7 @@ describe('DetailHeader', () => {
fireEvent.click(screen.getByTestId('remove-btn'))
await waitFor(() => {
expect(screen.getByTestId('delete-confirm')).toBeInTheDocument()
expect(screen.getByRole('alertdialog')).toBeInTheDocument()
})
})
@ -810,13 +797,13 @@ describe('DetailHeader', () => {
fireEvent.click(screen.getByTestId('remove-btn'))
await waitFor(() => {
expect(screen.getByTestId('delete-confirm')).toBeInTheDocument()
expect(screen.getByRole('alertdialog')).toBeInTheDocument()
})
fireEvent.click(screen.getByTestId('confirm-cancel'))
fireEvent.click(screen.getByRole('button', { name: 'common.operation.cancel' }))
await waitFor(() => {
expect(screen.queryByTestId('delete-confirm')).not.toBeInTheDocument()
expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()
})
})
@ -825,10 +812,10 @@ describe('DetailHeader', () => {
fireEvent.click(screen.getByTestId('remove-btn'))
await waitFor(() => {
expect(screen.getByTestId('delete-confirm')).toBeInTheDocument()
expect(screen.getByRole('alertdialog')).toBeInTheDocument()
})
fireEvent.click(screen.getByTestId('confirm-ok'))
fireEvent.click(screen.getByRole('button', { name: 'common.operation.confirm' }))
await waitFor(() => {
expect(mockUninstallPlugin).toHaveBeenCalledWith('test-id')
@ -840,10 +827,10 @@ describe('DetailHeader', () => {
fireEvent.click(screen.getByTestId('remove-btn'))
await waitFor(() => {
expect(screen.getByTestId('delete-confirm')).toBeInTheDocument()
expect(screen.getByRole('alertdialog')).toBeInTheDocument()
})
fireEvent.click(screen.getByTestId('confirm-ok'))
fireEvent.click(screen.getByRole('button', { name: 'common.operation.confirm' }))
await waitFor(() => {
expect(mockOnUpdate).toHaveBeenCalledWith(true)
@ -861,10 +848,10 @@ describe('DetailHeader', () => {
fireEvent.click(screen.getByTestId('remove-btn'))
await waitFor(() => {
expect(screen.getByTestId('delete-confirm')).toBeInTheDocument()
expect(screen.getByRole('alertdialog')).toBeInTheDocument()
})
fireEvent.click(screen.getByTestId('confirm-ok'))
fireEvent.click(screen.getByRole('button', { name: 'common.operation.confirm' }))
await waitFor(() => {
expect(mockRefreshModelProviders).toHaveBeenCalled()
@ -876,10 +863,10 @@ describe('DetailHeader', () => {
fireEvent.click(screen.getByTestId('remove-btn'))
await waitFor(() => {
expect(screen.getByTestId('delete-confirm')).toBeInTheDocument()
expect(screen.getByRole('alertdialog')).toBeInTheDocument()
})
fireEvent.click(screen.getByTestId('confirm-ok'))
fireEvent.click(screen.getByRole('button', { name: 'common.operation.confirm' }))
await waitFor(() => {
expect(mockInvalidateAllToolProviders).toHaveBeenCalled()
@ -891,10 +878,10 @@ describe('DetailHeader', () => {
fireEvent.click(screen.getByTestId('remove-btn'))
await waitFor(() => {
expect(screen.getByTestId('delete-confirm')).toBeInTheDocument()
expect(screen.getByRole('alertdialog')).toBeInTheDocument()
})
fireEvent.click(screen.getByTestId('confirm-ok'))
fireEvent.click(screen.getByRole('button', { name: 'common.operation.confirm' }))
await waitFor(() => {
expect(amplitude.trackEvent).toHaveBeenCalledWith('plugin_uninstalled', expect.any(Object))

View File

@ -9,24 +9,6 @@ vi.mock('@/context/i18n', () => ({
useGetLanguage: () => 'en_US',
}))
vi.mock('@/app/components/base/confirm', () => ({
default: ({ isShow, title, onCancel, onConfirm, isLoading }: {
isShow: boolean
title: string
onCancel: () => void
onConfirm: () => void
isLoading: boolean
}) => isShow
? (
<div data-testid="delete-confirm">
<div data-testid="delete-title">{title}</div>
<button data-testid="confirm-cancel" onClick={onCancel}>Cancel</button>
<button data-testid="confirm-ok" onClick={onConfirm} disabled={isLoading}>Confirm</button>
</div>
)
: null,
}))
vi.mock('@/app/components/plugins/plugin-page/plugin-info', () => ({
default: ({ repository, release, packageName, onHide }: {
repository: string
@ -230,7 +212,7 @@ describe('HeaderModals', () => {
/>,
)
expect(screen.queryByTestId('delete-confirm')).not.toBeInTheDocument()
expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()
})
it('should render delete confirm when isShowDeleteConfirm is true', () => {
@ -247,7 +229,7 @@ describe('HeaderModals', () => {
/>,
)
expect(screen.getByTestId('delete-confirm')).toBeInTheDocument()
expect(screen.getByRole('alertdialog')).toBeInTheDocument()
})
it('should show correct delete title', () => {
@ -264,7 +246,7 @@ describe('HeaderModals', () => {
/>,
)
expect(screen.getByTestId('delete-title')).toHaveTextContent('plugin.action.delete')
expect(screen.getByRole('alertdialog')).toHaveTextContent('plugin.action.delete')
})
it('should call hideDeleteConfirm when cancel is clicked', () => {
@ -281,7 +263,7 @@ describe('HeaderModals', () => {
/>,
)
fireEvent.click(screen.getByTestId('confirm-cancel'))
fireEvent.click(screen.getByRole('button', { name: 'common.operation.cancel' }))
expect(modalStates.hideDeleteConfirm).toHaveBeenCalled()
})
@ -300,7 +282,7 @@ describe('HeaderModals', () => {
/>,
)
fireEvent.click(screen.getByTestId('confirm-ok'))
fireEvent.click(screen.getByRole('button', { name: 'common.operation.confirm' }))
expect(mockOnDelete).toHaveBeenCalled()
})
@ -319,7 +301,7 @@ describe('HeaderModals', () => {
/>,
)
expect(screen.getByTestId('confirm-ok')).toBeDisabled()
expect(screen.getByRole('button', { name: /common\.operation\.confirm/ })).toBeDisabled()
})
})
@ -485,7 +467,7 @@ describe('HeaderModals', () => {
)
expect(screen.getByTestId('plugin-info')).toBeInTheDocument()
expect(screen.getByTestId('delete-confirm')).toBeInTheDocument()
expect(screen.getByRole('alertdialog')).toBeInTheDocument()
expect(screen.getByTestId('update-modal')).toBeInTheDocument()
})
})

View File

@ -104,36 +104,6 @@ vi.mock('../../install-plugin/install-from-github', () => ({
),
}))
// Mock Portal components for PluginVersionPicker
let mockPortalOpen = false
vi.mock('@/app/components/base/portal-to-follow-elem', () => ({
PortalToFollowElem: ({ children, open, onOpenChange: _onOpenChange }: {
children: React.ReactNode
open: boolean
onOpenChange: (open: boolean) => void
}) => {
mockPortalOpen = open
return <div data-testid="portal-elem" data-open={open}>{children}</div>
},
PortalToFollowElemTrigger: ({ children, onClick, className }: {
children: React.ReactNode
onClick: () => void
className?: string
}) => (
<div data-testid="portal-trigger" onClick={onClick} className={className}>
{children}
</div>
),
PortalToFollowElemContent: ({ children, className }: {
children: React.ReactNode
className?: string
}) => {
if (!mockPortalOpen)
return null
return <div data-testid="portal-content" className={className}>{children}</div>
},
}))
// Mock semver
vi.mock('semver', () => ({
lt: (v1: string, v2: string) => {
@ -247,7 +217,6 @@ const renderWithQueryClient = (ui: React.ReactElement) => {
describe('update-plugin', () => {
beforeEach(() => {
vi.clearAllMocks()
mockPortalOpen = false
mockCheck.mockResolvedValue({ status: TaskStatus.success })
})
@ -964,7 +933,7 @@ describe('update-plugin', () => {
render(<PluginVersionPicker {...defaultProps} isShow={false} />)
// Assert
expect(screen.queryByTestId('portal-content')).not.toBeInTheDocument()
expect(screen.queryByText('plugin.detailPanel.switchVersion')).not.toBeInTheDocument()
})
it('should render version list when isShow is true', () => {
@ -972,7 +941,6 @@ describe('update-plugin', () => {
render(<PluginVersionPicker {...defaultProps} isShow={true} />)
// Assert
expect(screen.getByTestId('portal-content')).toBeInTheDocument()
expect(screen.getByText('plugin.detailPanel.switchVersion')).toBeInTheDocument()
})
@ -1002,7 +970,7 @@ describe('update-plugin', () => {
// Act
render(<PluginVersionPicker {...defaultProps} onShowChange={onShowChange} />)
fireEvent.click(screen.getByTestId('portal-trigger'))
fireEvent.click(screen.getByText('Select Version'))
// Assert
expect(onShowChange).toHaveBeenCalledWith(true)
@ -1014,7 +982,7 @@ describe('update-plugin', () => {
// Act
render(<PluginVersionPicker {...defaultProps} disabled={true} onShowChange={onShowChange} />)
fireEvent.click(screen.getByTestId('portal-trigger'))
fireEvent.click(screen.getByText('Select Version'))
// Assert
expect(onShowChange).not.toHaveBeenCalled()
@ -1116,7 +1084,7 @@ describe('update-plugin', () => {
)
// Assert
expect(screen.getByTestId('portal-elem')).toBeInTheDocument()
expect(screen.getByText('plugin.detailPanel.switchVersion')).toBeInTheDocument()
})
it('should support custom offset', () => {
@ -1125,12 +1093,13 @@ describe('update-plugin', () => {
<PluginVersionPicker
{...defaultProps}
isShow={true}
offset={{ mainAxis: 10, crossAxis: 20 }}
sideOffset={10}
alignOffset={20}
/>,
)
// Assert
expect(screen.getByTestId('portal-elem')).toBeInTheDocument()
expect(screen.getByText('plugin.detailPanel.switchVersion')).toBeInTheDocument()
})
})

View File

@ -6,7 +6,12 @@ import { useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Badge, { BadgeState } from '@/app/components/base/badge/index'
import Button from '@/app/components/base/button'
import Modal from '@/app/components/base/modal'
import {
Dialog,
DialogCloseButton,
DialogContent,
DialogTitle,
} from '@/app/components/base/ui/dialog'
import Card from '@/app/components/plugins/card'
import checkTaskStatus from '@/app/components/plugins/install-plugin/base/check-task-status'
import { pluginManifestToCardPluginProps } from '@/app/components/plugins/install-plugin/utils'
@ -125,63 +130,65 @@ const UpdatePluginModal: FC<Props> = ({
const doShowDowngradeWarningModal = isShowDowngradeWarningModal && uploadStep === UploadStep.notStarted
return (
<Modal
isShow={true}
onClose={onCancel}
className={cn('min-w-[560px]', doShowDowngradeWarningModal && 'min-w-[640px]')}
closable
title={!doShowDowngradeWarningModal && t(`${i18nPrefix}.${uploadStep === UploadStep.installed ? 'successfulTitle' : 'title'}`, { ns: 'plugin' })}
>
{doShowDowngradeWarningModal && (
<DowngradeWarningModal
onCancel={onCancel}
onJustDowngrade={handleConfirm}
onExcludeAndDowngrade={handleExcludeAndDownload}
/>
)}
{!doShowDowngradeWarningModal && (
<>
<div className="system-md-regular mb-2 mt-3 text-text-secondary">
{t(`${i18nPrefix}.description`, { ns: 'plugin' })}
</div>
<div className="flex flex-wrap content-start items-start gap-1 self-stretch rounded-2xl bg-background-section-burn p-2">
<Card
installed={uploadStep === UploadStep.installed}
payload={pluginManifestToCardPluginProps({
...originalPackageInfo.payload,
icon: icon!,
})}
className="w-full"
titleLeft={(
<>
<Badge className="mx-1" size="s" state={BadgeState.Warning}>
{`${originalPackageInfo.payload.version} -> ${targetPackageInfo.version}`}
</Badge>
</>
<Dialog open onOpenChange={() => onCancel()}>
<DialogContent
backdropProps={{ forceRender: true }}
className={cn('min-w-[560px]', doShowDowngradeWarningModal && 'min-w-[640px]')}
>
<DialogCloseButton />
{doShowDowngradeWarningModal && (
<DowngradeWarningModal
onCancel={onCancel}
onJustDowngrade={handleConfirm}
onExcludeAndDowngrade={handleExcludeAndDownload}
/>
)}
{!doShowDowngradeWarningModal && (
<>
<DialogTitle className="text-text-primary title-2xl-semi-bold">
{t(`${i18nPrefix}.${uploadStep === UploadStep.installed ? 'successfulTitle' : 'title'}`, { ns: 'plugin' })}
</DialogTitle>
<div className="mb-2 mt-3 text-text-secondary system-md-regular">
{t(`${i18nPrefix}.description`, { ns: 'plugin' })}
</div>
<div className="flex flex-wrap content-start items-start gap-1 self-stretch rounded-2xl bg-background-section-burn p-2">
<Card
installed={uploadStep === UploadStep.installed}
payload={pluginManifestToCardPluginProps({
...originalPackageInfo.payload,
icon: icon!,
})}
className="w-full"
titleLeft={(
<>
<Badge className="mx-1" size="s" state={BadgeState.Warning}>
{`${originalPackageInfo.payload.version} -> ${targetPackageInfo.version}`}
</Badge>
</>
)}
/>
</div>
<div className="flex items-center justify-end gap-2 self-stretch pt-5">
{uploadStep === UploadStep.notStarted && (
<Button
onClick={handleCancel}
>
{t('operation.cancel', { ns: 'common' })}
</Button>
)}
/>
</div>
<div className="flex items-center justify-end gap-2 self-stretch pt-5">
{uploadStep === UploadStep.notStarted && (
<Button
onClick={handleCancel}
variant="primary"
loading={uploadStep === UploadStep.upgrading}
onClick={handleConfirm}
disabled={uploadStep === UploadStep.upgrading}
>
{t('operation.cancel', { ns: 'common' })}
{configBtnText}
</Button>
)}
<Button
variant="primary"
loading={uploadStep === UploadStep.upgrading}
onClick={handleConfirm}
disabled={uploadStep === UploadStep.upgrading}
>
{configBtnText}
</Button>
</div>
</>
)}
</Modal>
</div>
</>
)}
</DialogContent>
</Dialog>
)
}
export default React.memo(UpdatePluginModal)

View File

@ -1,19 +1,16 @@
'use client'
import type {
OffsetOptions,
Placement,
} from '@floating-ui/react'
import type { FC } from 'react'
import type { Placement } from '@/app/components/base/ui/placement'
import * as React from 'react'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { lt } from 'semver'
import Badge from '@/app/components/base/badge'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
Popover,
PopoverContent,
PopoverTrigger,
} from '@/app/components/base/ui/popover'
import useTimestamp from '@/hooks/use-timestamp'
import { useVersionListOfPlugin } from '@/service/use-plugins'
import { cn } from '@/utils/classnames'
@ -26,7 +23,8 @@ type Props = {
currentVersion: string
trigger: React.ReactNode
placement?: Placement
offset?: OffsetOptions
sideOffset?: number
alignOffset?: number
onSelect: ({
version,
unique_identifier,
@ -46,22 +44,14 @@ const PluginVersionPicker: FC<Props> = ({
currentVersion,
trigger,
placement = 'bottom-start',
offset = {
mainAxis: 4,
crossAxis: -16,
},
sideOffset = 4,
alignOffset = -16,
onSelect,
}) => {
const { t } = useTranslation()
const format = t('dateTimeFormat', { ns: 'appLog' }).split(' ')[0]
const { formatDate } = useTimestamp()
const handleTriggerClick = () => {
if (disabled)
return
onShowChange(!isShow)
}
const { data: res } = useVersionListOfPlugin(pluginID)
const handleSelect = useCallback(({ version, unique_identifier, isDowngrade }: {
@ -76,49 +66,52 @@ const PluginVersionPicker: FC<Props> = ({
}, [currentVersion, onSelect, onShowChange])
return (
<PortalToFollowElem
placement={placement}
offset={offset}
<Popover
open={isShow}
onOpenChange={onShowChange}
onOpenChange={(open) => {
if (!disabled)
onShowChange(open)
}}
>
<PortalToFollowElemTrigger
<PopoverTrigger
className={cn('inline-flex cursor-pointer items-center', disabled && 'cursor-default')}
onClick={handleTriggerClick}
>
{trigger}
</PortalToFollowElemTrigger>
</PopoverTrigger>
<PortalToFollowElemContent className="z-[1000]">
<div className="relative w-[209px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg backdrop-blur-sm">
<div className="system-xs-medium-uppercase px-3 pb-0.5 pt-1 text-text-tertiary">
{t('detailPanel.switchVersion', { ns: 'plugin' })}
</div>
<div className="relative max-h-[224px] overflow-y-auto">
{res?.data.versions.map(version => (
<div
key={version.unique_identifier}
className={cn(
'flex h-7 cursor-pointer items-center gap-1 rounded-lg px-3 py-1 hover:bg-state-base-hover',
currentVersion === version.version && 'cursor-default opacity-30 hover:bg-transparent',
)}
onClick={() => handleSelect({
version: version.version,
unique_identifier: version.unique_identifier,
isDowngrade: lt(version.version, currentVersion),
})}
>
<div className="flex grow items-center">
<div className="system-sm-medium text-text-secondary">{version.version}</div>
{currentVersion === version.version && <Badge className="ml-1" text="CURRENT" />}
</div>
<div className="system-xs-regular shrink-0 text-text-tertiary">{formatDate(version.created_at, format)}</div>
</div>
))}
</div>
<PopoverContent
placement={placement}
sideOffset={sideOffset}
alignOffset={alignOffset}
popupClassName="relative w-[209px] bg-components-panel-bg-blur p-1 backdrop-blur-sm"
>
<div className="px-3 pb-0.5 pt-1 text-text-tertiary system-xs-medium-uppercase">
{t('detailPanel.switchVersion', { ns: 'plugin' })}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
<div className="relative max-h-[224px] overflow-y-auto">
{res?.data.versions.map(version => (
<div
key={version.unique_identifier}
className={cn(
'flex h-7 cursor-pointer items-center gap-1 rounded-lg px-3 py-1 hover:bg-state-base-hover',
currentVersion === version.version && 'cursor-default opacity-30 hover:bg-transparent',
)}
onClick={() => handleSelect({
version: version.version,
unique_identifier: version.unique_identifier,
isDowngrade: lt(version.version, currentVersion),
})}
>
<div className="flex grow items-center">
<div className="text-text-secondary system-sm-medium">{version.version}</div>
{currentVersion === version.version && <Badge className="ml-1" text="CURRENT" />}
</div>
<div className="shrink-0 text-text-tertiary system-xs-regular">{formatDate(version.created_at, format)}</div>
</div>
))}
</div>
</PopoverContent>
</Popover>
)
}