fix(plugin-tasks): handle error actions by source and clear item after marketplace install

This commit is contained in:
CodingOnStar
2026-03-11 15:59:37 +08:00
parent c2def7a840
commit 3f27c8a9d2
7 changed files with 73 additions and 42 deletions

View File

@ -1,7 +1,7 @@
import type { PluginStatus } from '@/app/components/plugins/types'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { TaskStatus } from '@/app/components/plugins/types'
import { PluginSource, TaskStatus } from '@/app/components/plugins/types'
// Import mocked modules
import { useMutationClearTaskPlugin, usePluginTaskList } from '@/service/use-plugins'
import PluginTaskList from '../components/plugin-task-list'
@ -30,6 +30,7 @@ vi.mock('@/context/i18n', () => ({
const createMockPlugin = (overrides: Partial<PluginStatus> = {}): PluginStatus => ({
plugin_unique_identifier: `plugin-${Math.random().toString(36).substr(2, 9)}`,
plugin_id: 'test-plugin',
source: PluginSource.marketplace,
status: TaskStatus.running,
message: '',
icon: 'test-icon.png',

View File

@ -1,6 +1,6 @@
import type { PluginInfoFromMarketPlace, PluginStatus } from '@/app/components/plugins/types'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { PluginCategoryEnum, TaskStatus } from '@/app/components/plugins/types'
import { PluginCategoryEnum, PluginSource, TaskStatus } from '@/app/components/plugins/types'
import { fetchPluginInfoFromMarketPlace } from '@/service/plugins'
import ErrorPluginItem from '../error-plugin-item'
@ -43,6 +43,7 @@ function createMarketplaceResponse(identifier: string, version: string) {
const createPlugin = (overrides: Partial<PluginStatus> = {}): PluginStatus => ({
plugin_unique_identifier: 'org/plugin:1.0.0',
plugin_id: 'org/plugin',
source: PluginSource.marketplace,
status: TaskStatus.failed,
message: '',
icon: 'icon.png',
@ -102,7 +103,7 @@ describe('ErrorPluginItem', () => {
it('should show marketplace error message for marketplace plugins', () => {
render(
<ErrorPluginItem
plugin={createPlugin({ plugin_id: 'org/my-plugin' })}
plugin={createPlugin({ source: PluginSource.marketplace, plugin_id: 'org/my-plugin' })}
getIconUrl={mockGetIconUrl}
language="en_US"
onClear={vi.fn()}
@ -115,7 +116,7 @@ describe('ErrorPluginItem', () => {
it('should show github error message for github plugins', () => {
render(
<ErrorPluginItem
plugin={createPlugin({ plugin_id: 'https://github.com/user/repo' })}
plugin={createPlugin({ source: PluginSource.github, plugin_id: 'https://github.com/user/repo' })}
getIconUrl={mockGetIconUrl}
language="en_US"
onClear={vi.fn()}
@ -128,7 +129,7 @@ describe('ErrorPluginItem', () => {
it('should show unknown error message for unknown source plugins', () => {
render(
<ErrorPluginItem
plugin={createPlugin({ plugin_id: 'local-only-plugin' })}
plugin={createPlugin({ source: PluginSource.local, plugin_id: 'local-only-plugin' })}
getIconUrl={mockGetIconUrl}
language="en_US"
onClear={vi.fn()}
@ -156,7 +157,7 @@ describe('ErrorPluginItem', () => {
it('should show "Install from Marketplace" button for marketplace plugins', () => {
render(
<ErrorPluginItem
plugin={createPlugin({ plugin_id: 'org/my-plugin' })}
plugin={createPlugin({ source: PluginSource.marketplace, plugin_id: 'org/my-plugin' })}
getIconUrl={mockGetIconUrl}
language="en_US"
onClear={vi.fn()}
@ -169,7 +170,7 @@ describe('ErrorPluginItem', () => {
it('should show "Install from GitHub" button for github plugins', () => {
render(
<ErrorPluginItem
plugin={createPlugin({ plugin_id: 'https://github.com/user/repo' })}
plugin={createPlugin({ source: PluginSource.github, plugin_id: 'https://github.com/user/repo' })}
getIconUrl={mockGetIconUrl}
language="en_US"
onClear={vi.fn()}
@ -182,7 +183,7 @@ describe('ErrorPluginItem', () => {
it('should not show action button for unknown source plugins', () => {
render(
<ErrorPluginItem
plugin={createPlugin({ plugin_id: 'local-only-plugin' })}
plugin={createPlugin({ source: PluginSource.local, plugin_id: 'local-only-plugin' })}
getIconUrl={mockGetIconUrl}
language="en_US"
onClear={vi.fn()}
@ -191,6 +192,20 @@ describe('ErrorPluginItem', () => {
expect(screen.queryByText(/plugin\.task\.installFrom/)).not.toBeInTheDocument()
})
it('should use source instead of plugin_id heuristics when deciding button text', () => {
render(
<ErrorPluginItem
plugin={createPlugin({ source: PluginSource.github, plugin_id: 'org/my-plugin' })}
getIconUrl={mockGetIconUrl}
language="en_US"
onClear={vi.fn()}
/>,
)
expect(screen.getByText(/plugin\.task\.installFromGithub/)).toBeInTheDocument()
expect(screen.queryByText(/plugin\.task\.installFromMarketplace/)).not.toBeInTheDocument()
})
})
describe('User Interactions', () => {
@ -218,7 +233,7 @@ describe('ErrorPluginItem', () => {
render(
<ErrorPluginItem
plugin={createPlugin({ plugin_id: 'org/my-plugin' })}
plugin={createPlugin({ source: PluginSource.marketplace, plugin_id: 'org/my-plugin' })}
getIconUrl={mockGetIconUrl}
language="en_US"
onClear={vi.fn()}
@ -240,7 +255,7 @@ describe('ErrorPluginItem', () => {
render(
<ErrorPluginItem
plugin={createPlugin({ plugin_id: 'org/my-plugin' })}
plugin={createPlugin({ source: PluginSource.marketplace, plugin_id: 'org/my-plugin' })}
getIconUrl={mockGetIconUrl}
language="en_US"
onClear={vi.fn()}
@ -263,7 +278,7 @@ describe('ErrorPluginItem', () => {
render(
<ErrorPluginItem
plugin={createPlugin({ plugin_id: 'org/my-plugin' })}
plugin={createPlugin({ source: PluginSource.marketplace, plugin_id: 'org/my-plugin' })}
getIconUrl={mockGetIconUrl}
language="en_US"
onClear={vi.fn()}
@ -282,7 +297,7 @@ describe('ErrorPluginItem', () => {
it('should not fetch when plugin_id has fewer than 2 parts', async () => {
render(
<ErrorPluginItem
plugin={createPlugin({ plugin_id: 'single-part' })}
plugin={createPlugin({ source: PluginSource.local, plugin_id: 'single-part' })}
getIconUrl={mockGetIconUrl}
language="en_US"
onClear={vi.fn()}
@ -296,10 +311,10 @@ describe('ErrorPluginItem', () => {
})
describe('Edge Cases', () => {
it('should detect github source with github in URL', () => {
it('should render github action when source is github even if plugin_id looks like a URL', () => {
render(
<ErrorPluginItem
plugin={createPlugin({ plugin_id: 'http://github.com/user/repo' })}
plugin={createPlugin({ source: PluginSource.github, plugin_id: 'http://github.com/user/repo' })}
getIconUrl={mockGetIconUrl}
language="en_US"
onClear={vi.fn()}
@ -309,15 +324,16 @@ describe('ErrorPluginItem', () => {
expect(screen.getByText(/plugin\.task\.installFromGithub/)).toBeInTheDocument()
})
it('should close install modal when onSuccess is called', async () => {
it('should close install modal and clear the error item when onSuccess is called', async () => {
mockFetch.mockResolvedValueOnce(createMarketplaceResponse('org/p:1.0.0', '1.0.0'))
const onClear = vi.fn()
render(
<ErrorPluginItem
plugin={createPlugin({ plugin_id: 'org/p' })}
plugin={createPlugin({ source: PluginSource.marketplace, plugin_id: 'org/p' })}
getIconUrl={mockGetIconUrl}
language="en_US"
onClear={vi.fn()}
onClear={onClear}
/>,
)
@ -330,19 +346,35 @@ describe('ErrorPluginItem', () => {
fireEvent.click(screen.getByText('Success'))
expect(screen.queryByTestId('install-modal')).not.toBeInTheDocument()
expect(onClear).toHaveBeenCalledTimes(1)
})
it('should detect github source when id contains github keyword', () => {
it('should show unknown action state for local source even if id contains github keyword', () => {
render(
<ErrorPluginItem
plugin={createPlugin({ plugin_id: 'my-github-plugin' })}
plugin={createPlugin({ source: PluginSource.local, plugin_id: 'my-github-plugin' })}
getIconUrl={mockGetIconUrl}
language="en_US"
onClear={vi.fn()}
/>,
)
expect(screen.getByText(/plugin\.task\.installFromGithub/)).toBeInTheDocument()
expect(screen.queryByText(/plugin\.task\.installFromGithub/)).not.toBeInTheDocument()
expect(screen.getByText(/plugin\.task\.errorMsg\.unknown/)).toBeInTheDocument()
})
it('should show unknown error message for debugging source plugins', () => {
render(
<ErrorPluginItem
plugin={createPlugin({ source: PluginSource.debugging, plugin_id: 'remote-plugin' })}
getIconUrl={mockGetIconUrl}
language="en_US"
onClear={vi.fn()}
/>,
)
expect(screen.getByText(/plugin\.task\.errorMsg\.unknown/)).toBeInTheDocument()
expect(screen.queryByText(/plugin\.task\.installFrom/)).not.toBeInTheDocument()
})
})
})

View File

@ -1,6 +1,6 @@
import type { PluginStatus } from '@/app/components/plugins/types'
import { fireEvent, render, screen } from '@testing-library/react'
import { TaskStatus } from '@/app/components/plugins/types'
import { PluginSource, TaskStatus } from '@/app/components/plugins/types'
import PluginItem from '../plugin-item'
vi.mock('@/app/components/plugins/card/base/card-icon', () => ({
@ -14,6 +14,7 @@ const mockGetIconUrl = vi.fn((icon: string) => `https://example.com/icons/${icon
const createPlugin = (overrides: Partial<PluginStatus> = {}): PluginStatus => ({
plugin_unique_identifier: 'org/plugin:1.0.0',
plugin_id: 'org/plugin',
source: PluginSource.marketplace,
status: TaskStatus.running,
message: '',
icon: 'icon.png',

View File

@ -1,6 +1,6 @@
import type { PluginStatus } from '@/app/components/plugins/types'
import { fireEvent, render, screen } from '@testing-library/react'
import { TaskStatus } from '@/app/components/plugins/types'
import { PluginSource, TaskStatus } from '@/app/components/plugins/types'
import PluginSection from '../plugin-section'
vi.mock('@/app/components/plugins/card/base/card-icon', () => ({
@ -14,6 +14,7 @@ const mockGetIconUrl = vi.fn((icon: string) => `https://icons/${icon}`)
const createPlugin = (id: string, name: string, message = ''): PluginStatus => ({
plugin_unique_identifier: id,
plugin_id: `org/${name.toLowerCase()}`,
source: PluginSource.marketplace,
status: TaskStatus.running,
message,
icon: `${name.toLowerCase()}.png`,

View File

@ -1,6 +1,6 @@
import type { PluginStatus } from '@/app/components/plugins/types'
import { fireEvent, render, screen } from '@testing-library/react'
import { TaskStatus } from '@/app/components/plugins/types'
import { PluginSource, TaskStatus } from '@/app/components/plugins/types'
import PluginTaskList from '../plugin-task-list'
vi.mock('@/app/components/plugins/card/base/card-icon', () => ({
@ -26,6 +26,7 @@ const mockGetIconUrl = vi.fn((icon: string) => `https://icons/${icon}`)
const createPlugin = (id: string, name: string, overrides: Partial<PluginStatus> = {}): PluginStatus => ({
plugin_unique_identifier: id,
plugin_id: `org/${name.toLowerCase()}`,
source: PluginSource.marketplace,
status: TaskStatus.running,
message: '',
icon: `${name.toLowerCase()}.png`,

View File

@ -5,19 +5,10 @@ import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace'
import { PluginSource } from '@/app/components/plugins/types'
import { fetchPluginInfoFromMarketPlace } from '@/service/plugins'
import PluginItem from './plugin-item'
type PluginSource = 'marketplace' | 'github' | 'unknown'
function getPluginSource(pluginId: string): PluginSource {
if (pluginId.includes('/') && !pluginId.startsWith('http'))
return 'marketplace'
if (pluginId.startsWith('http') || pluginId.includes('github'))
return 'github'
return 'unknown'
}
type ErrorPluginItemProps = {
plugin: PluginStatus
getIconUrl: (icon: string) => string
@ -27,7 +18,7 @@ type ErrorPluginItemProps = {
const ErrorPluginItem: FC<ErrorPluginItemProps> = ({ plugin, getIconUrl, language, onClear }) => {
const { t } = useTranslation()
const source = getPluginSource(plugin.plugin_id)
const source = plugin.source
const [showInstallModal, setShowInstallModal] = useState(false)
const [installPayload, setInstallPayload] = useState<{ uniqueIdentifier: string, manifest: Plugin } | null>(null)
const [isFetching, setIsFetching] = useState(false)
@ -75,16 +66,16 @@ const ErrorPluginItem: FC<ErrorPluginItemProps> = ({ plugin, getIconUrl, languag
}
}, [plugin.plugin_id, plugin.labels, plugin.icon])
const errorMsgKey = {
marketplace: 'task.errorMsg.marketplace',
github: 'task.errorMsg.github',
unknown: 'task.errorMsg.unknown',
}[source] as 'task.errorMsg.marketplace'
const errorMsgKey: 'task.errorMsg.marketplace' | 'task.errorMsg.github' | 'task.errorMsg.unknown' = source === PluginSource.marketplace
? 'task.errorMsg.marketplace'
: source === PluginSource.github
? 'task.errorMsg.github'
: 'task.errorMsg.unknown'
const errorMsg = t(errorMsgKey, { ns: 'plugin' })
const renderAction = () => {
if (source === 'marketplace') {
if (source === PluginSource.marketplace) {
return (
<div className="pt-1">
<Button variant="secondary" size="small" loading={isFetching} onClick={handleInstallFromMarketplace}>
@ -93,7 +84,7 @@ const ErrorPluginItem: FC<ErrorPluginItemProps> = ({ plugin, getIconUrl, languag
</div>
)
}
if (source === 'github') {
if (source === PluginSource.github) {
return (
<div className="pt-1">
<Button variant="secondary" size="small">
@ -130,7 +121,10 @@ const ErrorPluginItem: FC<ErrorPluginItemProps> = ({ plugin, getIconUrl, languag
uniqueIdentifier={installPayload.uniqueIdentifier}
manifest={installPayload.manifest}
onClose={() => setShowInstallModal(false)}
onSuccess={() => setShowInstallModal(false)}
onSuccess={() => {
setShowInstallModal(false)
onClear()
}}
/>
)}
</>

View File

@ -432,6 +432,7 @@ export enum TaskStatus {
export type PluginStatus = {
plugin_unique_identifier: string
plugin_id: string
source: PluginSource
status: TaskStatus
message: string
icon: string