fix(web): refresh model provider version after update (#41212)

This commit is contained in:
Jingyi
2026-08-25 01:12:01 +00:00
committed by GitHub
parent 8d6128957f
commit b56d30b366
2 changed files with 112 additions and 16 deletions
@@ -1,7 +1,7 @@
import type { ReactElement } from 'react'
import type { ModelProviderPluginSummary } from '../../index'
import type { PluginDetail } from '@/app/components/plugins/types'
import { fireEvent, screen, waitFor } from '@testing-library/react'
import { act, fireEvent, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { PluginSource } from '@/app/components/plugins/types'
import { consoleQuery } from '@/service/client'
@@ -45,6 +45,7 @@ let mockHeaderState = {
isFromMarketplace: true,
isFromGitHub: false,
}
let handleMarketplaceUpdateComplete: (() => Promise<void>) | undefined
const render = (ui: ReactElement) =>
renderWithConsoleQuery(ui, { systemFeatures: { enable_marketplace: true } })
@@ -55,9 +56,12 @@ const openActionsMenu = () => {
vi.mock('@/app/components/plugins/plugin-detail-panel/detail-header/hooks', () => ({
useDetailHeaderState: () => mockHeaderState,
usePluginOperations: () => ({
usePluginOperations: ({ onUpdate }: { onUpdate?: () => void | Promise<void> }) => ({
handleUpdate: mockHandleUpdate,
handleUpdatedFromMarketplace: mockHandleUpdatedFromMarketplace,
handleUpdatedFromMarketplace: async () => {
mockHandleUpdatedFromMarketplace()
await onUpdate?.()
},
handleDelete: mockHandleDelete,
}),
}))
@@ -67,18 +71,23 @@ vi.mock('@/app/components/plugins/plugin-detail-panel/detail-header/components',
targetVersion,
isDowngrade,
isAutoUpgradeEnabled,
onUpdatedFromMarketplace,
}: {
targetVersion?: { version: string; unique_identifier: string }
isDowngrade: boolean
isAutoUpgradeEnabled: boolean
}) => (
<div
data-testid="header-modals"
data-target-version={targetVersion?.version ?? ''}
data-is-downgrade={String(isDowngrade)}
data-auto-upgrade={String(isAutoUpgradeEnabled)}
/>
),
onUpdatedFromMarketplace: () => Promise<void>
}) => {
handleMarketplaceUpdateComplete = onUpdatedFromMarketplace
return (
<div
data-testid="header-modals"
data-target-version={targetVersion?.version ?? ''}
data-is-downgrade={String(isDowngrade)}
data-auto-upgrade={String(isAutoUpgradeEnabled)}
/>
)
},
}))
vi.mock('@/app/components/plugins/plugin-page/use-reference-setting', () => ({
@@ -137,6 +146,7 @@ const createSummary = (
describe('ProviderCardActions', () => {
beforeEach(() => {
vi.clearAllMocks()
handleMarketplaceUpdateComplete = undefined
mockHeaderState = {
modalStates: {
showPluginInfo: mockShowPluginInfo,
@@ -300,6 +310,87 @@ describe('ProviderCardActions', () => {
})
})
it.each([
['upgrade', '1.0.0', '2.0.0'],
['downgrade', '2.0.0', '1.0.0'],
])(
'should show the refreshed summary version after a marketplace %s',
async (_, current, next) => {
const user = userEvent.setup()
let resolveRefresh: () => void = () => {}
const refreshPromise = new Promise<void>((resolve) => {
resolveRefresh = resolve
})
const onUpdate = vi.fn(() => refreshPromise)
mockNormalizeInstalledPluginDetail.mockReturnValue(createDetail({ version: current }))
const rendered = render(
<ProviderCardActions
summary={createSummary({ source: 'marketplace', version: current })}
providerLabel="Provider Plugin"
onUpdate={onUpdate}
/>,
)
vi.spyOn(rendered.queryClient, 'fetchQuery').mockResolvedValue({ plugins: [{}] })
await user.click(screen.getByRole('button', { name: current }))
let updateComplete: Promise<void> | undefined
await act(async () => {
updateComplete = handleMarketplaceUpdateComplete?.()
})
await waitFor(() => expect(onUpdate).toHaveBeenCalledOnce())
rendered.rerender(
<ProviderCardActions
summary={createSummary({ source: 'marketplace', version: next })}
providerLabel="Provider Plugin"
onUpdate={onUpdate}
/>,
)
expect(screen.getByText(current)).toBeInTheDocument()
expect(screen.getByTestId('header-modals')).toBeInTheDocument()
await act(async () => {
resolveRefresh()
await updateComplete
})
expect(await screen.findByText(next)).toBeInTheDocument()
expect(screen.queryByText(current)).not.toBeInTheDocument()
expect(screen.queryByTestId('header-modals')).not.toBeInTheDocument()
},
)
it('should keep the loaded detail when refreshing the summary fails', async () => {
const user = userEvent.setup()
const refreshError = new Error('refresh failed')
const onUpdate = vi.fn().mockRejectedValue(refreshError)
mockNormalizeInstalledPluginDetail.mockReturnValue(createDetail({ version: '1.0.0' }))
const rendered = render(
<ProviderCardActions
summary={createSummary({ source: 'marketplace', version: '1.0.0' })}
providerLabel="Provider Plugin"
onUpdate={onUpdate}
/>,
)
vi.spyOn(rendered.queryClient, 'fetchQuery').mockResolvedValue({ plugins: [{}] })
await user.click(screen.getByRole('button', { name: '1.0.0' }))
let thrownError: unknown
await act(async () => {
try {
await handleMarketplaceUpdateComplete?.()
} catch (error) {
thrownError = error
}
})
expect(thrownError).toBe(refreshError)
expect(screen.getByText('1.0.0')).toBeInTheDocument()
expect(screen.getByTestId('header-modals')).toBeInTheDocument()
})
it('should render version controls for marketplace plugins and handle manual version selection', async () => {
const user = userEvent.setup()
mockHeaderState = {
@@ -41,12 +41,12 @@ type Props =
| Readonly<{
summary: ModelProviderPluginSummary
providerLabel: string
onUpdate?: () => void
onUpdate?: () => void | Promise<void>
detail?: never
}>
| Readonly<{
detail: PluginDetail
onUpdate?: () => void
onUpdate?: () => void | Promise<void>
summary?: never
providerLabel?: never
}>
@@ -79,7 +79,7 @@ const ProviderCardActions: FC<Props> = (props) => {
queryClient.invalidateQueries({ queryKey: ['marketplacePlugins'] }),
queryClient.invalidateQueries({ queryKey: ['marketplaceCollectionPlugins'] }),
])
onUpdate?.()
await onUpdate?.()
}, [onUpdate, queryClient])
if (props.detail) {
@@ -156,13 +156,18 @@ function SummaryProviderCardActions({ summary, providerLabel, onUpdate }: Summar
}
}
const handleLoadedDetailUpdated = useCallback(async () => {
await onUpdate?.()
setDetail(undefined)
}, [onUpdate])
if (detail) {
return (
<LoadedProviderCardActions
detail={detail}
initialAction={detailAction}
onInitialActionHandled={() => setDetailAction(undefined)}
onUpdate={onUpdate}
onUpdate={handleLoadedDetailUpdated}
/>
)
}
@@ -278,7 +283,7 @@ type LoadedProps = Readonly<{
detail: PluginDetail
initialAction?: DetailAction
onInitialActionHandled: () => void
onUpdate?: () => void
onUpdate?: () => void | Promise<void>
}>
function LoadedProviderCardActions({