mirror of https://github.com/langgenius/dify.git
1003 lines
33 KiB
TypeScript
1003 lines
33 KiB
TypeScript
import type { PluginDetail } from '../types'
|
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import * as amplitude from '@/app/components/base/amplitude'
|
|
import Toast from '@/app/components/base/toast'
|
|
import { PluginSource } from '../types'
|
|
import DetailHeader from './detail-header'
|
|
|
|
// Use vi.hoisted for mock functions used in vi.mock factories
|
|
const {
|
|
mockSetShowUpdatePluginModal,
|
|
mockRefreshModelProviders,
|
|
mockInvalidateAllToolProviders,
|
|
mockUninstallPlugin,
|
|
mockFetchReleases,
|
|
mockCheckForUpdates,
|
|
} = vi.hoisted(() => {
|
|
return {
|
|
mockSetShowUpdatePluginModal: vi.fn(),
|
|
mockRefreshModelProviders: vi.fn(),
|
|
mockInvalidateAllToolProviders: vi.fn(),
|
|
mockUninstallPlugin: vi.fn(() => Promise.resolve({ success: true })),
|
|
mockFetchReleases: vi.fn(() => Promise.resolve([{ tag_name: 'v2.0.0' }])),
|
|
mockCheckForUpdates: vi.fn(() => ({ needUpdate: true, toastProps: { type: 'success', message: 'Update available' } })),
|
|
}
|
|
})
|
|
|
|
vi.mock('react-i18next', () => ({
|
|
useTranslation: () => ({
|
|
t: (key: string) => key,
|
|
}),
|
|
}))
|
|
|
|
vi.mock('ahooks', async () => {
|
|
const React = await import('react')
|
|
return {
|
|
useBoolean: (initial: boolean) => {
|
|
const [value, setValue] = React.useState(initial)
|
|
return [
|
|
value,
|
|
{
|
|
setTrue: () => setValue(true),
|
|
setFalse: () => setValue(false),
|
|
},
|
|
]
|
|
},
|
|
}
|
|
})
|
|
|
|
vi.mock('@/context/app-context', () => ({
|
|
useAppContext: () => ({
|
|
userProfile: { timezone: 'UTC' },
|
|
}),
|
|
}))
|
|
|
|
vi.mock('@/hooks/use-theme', () => ({
|
|
default: () => ({ theme: 'light' }),
|
|
}))
|
|
|
|
vi.mock('@/context/i18n', () => ({
|
|
useGetLanguage: () => 'en_US',
|
|
useLocale: () => 'en-US',
|
|
}))
|
|
|
|
// Global mock state for enable_marketplace
|
|
let mockEnableMarketplace = true
|
|
|
|
vi.mock('@/context/global-public-context', () => ({
|
|
useGlobalPublicStore: (selector: (state: { systemFeatures: { enable_marketplace: boolean } }) => unknown) =>
|
|
selector({ systemFeatures: { enable_marketplace: mockEnableMarketplace } }),
|
|
}))
|
|
|
|
vi.mock('@/context/modal-context', () => ({
|
|
useModalContext: () => ({
|
|
setShowUpdatePluginModal: mockSetShowUpdatePluginModal,
|
|
}),
|
|
}))
|
|
|
|
vi.mock('@/context/provider-context', () => ({
|
|
useProviderContext: () => ({
|
|
refreshModelProviders: mockRefreshModelProviders,
|
|
}),
|
|
}))
|
|
|
|
vi.mock('@/service/plugins', () => ({
|
|
uninstallPlugin: mockUninstallPlugin,
|
|
}))
|
|
|
|
vi.mock('@/service/use-tools', () => ({
|
|
useAllToolProviders: () => ({ data: [] }),
|
|
useInvalidateAllToolProviders: () => mockInvalidateAllToolProviders,
|
|
}))
|
|
|
|
vi.mock('../install-plugin/hooks', () => ({
|
|
useGitHubReleases: () => ({
|
|
checkForUpdates: mockCheckForUpdates,
|
|
fetchReleases: mockFetchReleases,
|
|
}),
|
|
}))
|
|
|
|
// Auto upgrade settings mock
|
|
let mockAutoUpgradeInfo: {
|
|
strategy_setting: string
|
|
upgrade_mode: string
|
|
include_plugins: string[]
|
|
exclude_plugins: string[]
|
|
upgrade_time_of_day: number
|
|
} | null = null
|
|
|
|
vi.mock('../plugin-page/use-reference-setting', () => ({
|
|
default: () => ({
|
|
referenceSetting: mockAutoUpgradeInfo ? { auto_upgrade: mockAutoUpgradeInfo } : null,
|
|
}),
|
|
}))
|
|
|
|
vi.mock('../reference-setting-modal/auto-update-setting/types', () => ({
|
|
AUTO_UPDATE_MODE: {
|
|
update_all: 'update_all',
|
|
partial: 'partial',
|
|
exclude: 'exclude',
|
|
},
|
|
}))
|
|
|
|
vi.mock('../reference-setting-modal/auto-update-setting/utils', () => ({
|
|
convertUTCDaySecondsToLocalSeconds: (seconds: number) => seconds,
|
|
timeOfDayToDayjs: () => ({ format: () => '10:00 AM' }),
|
|
}))
|
|
|
|
vi.mock('@/hooks/use-i18n', () => ({
|
|
useRenderI18nObject: () => (obj: Record<string, string>) => obj?.en_US || '',
|
|
}))
|
|
|
|
vi.mock('@/utils/classnames', () => ({
|
|
cn: (...args: (string | undefined | false | null)[]) => args.filter(Boolean).join(' '),
|
|
}))
|
|
|
|
vi.mock('@/utils/var', () => ({
|
|
getMarketplaceUrl: (path: string) => `https://marketplace.example.com${path}`,
|
|
}))
|
|
|
|
vi.mock('../card/base/card-icon', () => ({
|
|
default: ({ src }: { src: string }) => <div data-testid="card-icon" data-src={src} />,
|
|
}))
|
|
|
|
vi.mock('../card/base/description', () => ({
|
|
default: ({ text }: { text: string }) => <div data-testid="description">{text}</div>,
|
|
}))
|
|
|
|
vi.mock('../card/base/org-info', () => ({
|
|
default: ({ orgName }: { orgName: string }) => <div data-testid="org-info">{orgName}</div>,
|
|
}))
|
|
|
|
vi.mock('../card/base/title', () => ({
|
|
default: ({ title }: { title: string }) => <div data-testid="title">{title}</div>,
|
|
}))
|
|
|
|
vi.mock('../base/badges/verified', () => ({
|
|
default: () => <span data-testid="verified-badge" />,
|
|
}))
|
|
|
|
vi.mock('../base/deprecation-notice', () => ({
|
|
default: () => <div data-testid="deprecation-notice" />,
|
|
}))
|
|
|
|
// Enhanced operation-dropdown mock
|
|
vi.mock('./operation-dropdown', () => ({
|
|
default: ({ onInfo, onCheckVersion, onRemove }: { onInfo: () => void, onCheckVersion: () => void, onRemove: () => void }) => (
|
|
<div data-testid="operation-dropdown">
|
|
<button data-testid="info-btn" onClick={onInfo}>Info</button>
|
|
<button data-testid="check-version-btn" onClick={onCheckVersion}>Check Version</button>
|
|
<button data-testid="remove-btn" onClick={onRemove}>Remove</button>
|
|
</div>
|
|
),
|
|
}))
|
|
|
|
// Enhanced update modal mock
|
|
vi.mock('../update-plugin/from-market-place', () => ({
|
|
default: ({ onSave, onCancel }: { onSave: () => void, onCancel: () => void }) => {
|
|
return (
|
|
<div data-testid="update-modal">
|
|
<button data-testid="update-modal-save" onClick={onSave}>Save</button>
|
|
<button data-testid="update-modal-cancel" onClick={onCancel}>Cancel</button>
|
|
</div>
|
|
)
|
|
},
|
|
}))
|
|
|
|
// Enhanced version picker mock
|
|
vi.mock('../update-plugin/plugin-version-picker', () => ({
|
|
default: ({ trigger, onSelect, onShowChange }: { trigger: React.ReactNode, onSelect: (state: { version: string, unique_identifier: string, isDowngrade?: boolean }) => void, onShowChange: (show: boolean) => void }) => (
|
|
<div data-testid="version-picker">
|
|
{trigger}
|
|
<button
|
|
data-testid="select-version-btn"
|
|
onClick={() => {
|
|
onShowChange(true)
|
|
onSelect({ version: '2.0.0', unique_identifier: 'new-uid', isDowngrade: false })
|
|
}}
|
|
>
|
|
Select Version
|
|
</button>
|
|
<button
|
|
data-testid="select-downgrade-btn"
|
|
onClick={() => {
|
|
onShowChange(true)
|
|
onSelect({ version: '0.5.0', unique_identifier: 'old-uid', isDowngrade: true })
|
|
}}
|
|
>
|
|
Downgrade
|
|
</button>
|
|
</div>
|
|
),
|
|
}))
|
|
|
|
vi.mock('../plugin-page/plugin-info', () => ({
|
|
default: ({ onHide }: { onHide: () => void }) => (
|
|
<div data-testid="plugin-info">
|
|
<button data-testid="plugin-info-close" onClick={onHide}>Close</button>
|
|
</div>
|
|
),
|
|
}))
|
|
|
|
vi.mock('../plugin-auth', () => ({
|
|
AuthCategory: { tool: 'tool' },
|
|
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',
|
|
updated_at: '2024-01-02',
|
|
name: 'Test Plugin',
|
|
plugin_id: 'test-plugin',
|
|
plugin_unique_identifier: 'test-uid',
|
|
declaration: {
|
|
author: 'test-author',
|
|
name: 'test-plugin-name',
|
|
category: 'tool',
|
|
label: { en_US: 'Test Plugin Label' },
|
|
description: { en_US: 'Test description' },
|
|
icon: 'icon.png',
|
|
verified: true,
|
|
tool: {
|
|
identity: {
|
|
name: 'test-tool',
|
|
author: 'author',
|
|
description: { en_US: 'Tool desc' },
|
|
icon: 'icon.png',
|
|
label: { en_US: 'Tool' },
|
|
tags: [],
|
|
},
|
|
credentials_schema: [],
|
|
},
|
|
} as unknown as PluginDetail['declaration'],
|
|
installation_id: 'install-1',
|
|
tenant_id: 'tenant-1',
|
|
endpoints_setups: 0,
|
|
endpoints_active: 0,
|
|
version: '1.0.0',
|
|
latest_version: '1.0.0',
|
|
latest_unique_identifier: 'test-uid',
|
|
source: PluginSource.marketplace,
|
|
meta: undefined,
|
|
status: 'active',
|
|
deprecated_reason: '',
|
|
alternative_plugin_id: '',
|
|
...overrides,
|
|
})
|
|
|
|
describe('DetailHeader', () => {
|
|
const mockOnUpdate = vi.fn()
|
|
const mockOnHide = vi.fn()
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
mockAutoUpgradeInfo = null
|
|
mockEnableMarketplace = true
|
|
vi.spyOn(Toast, 'notify').mockImplementation(() => ({ clear: vi.fn() }))
|
|
vi.spyOn(amplitude, 'trackEvent').mockImplementation(() => {})
|
|
})
|
|
|
|
describe('Rendering', () => {
|
|
it('should render plugin title', () => {
|
|
render(<DetailHeader detail={createPluginDetail()} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
expect(screen.getByTestId('title')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render plugin icon with correct src', () => {
|
|
render(<DetailHeader detail={createPluginDetail()} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
expect(screen.getByTestId('card-icon')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render icon with http url directly', () => {
|
|
const detail = createPluginDetail({
|
|
declaration: {
|
|
...createPluginDetail().declaration,
|
|
icon: 'https://example.com/icon.png',
|
|
} as unknown as PluginDetail['declaration'],
|
|
})
|
|
render(<DetailHeader detail={detail} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
expect(screen.getByTestId('card-icon')).toHaveAttribute('data-src', 'https://example.com/icon.png')
|
|
})
|
|
|
|
it('should render description when not in readme view', () => {
|
|
render(<DetailHeader detail={createPluginDetail()} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
expect(screen.getByTestId('description')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should not render description in readme view', () => {
|
|
render(<DetailHeader detail={createPluginDetail()} isReadmeView onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
expect(screen.queryByTestId('description')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('should render verified badge when verified', () => {
|
|
render(<DetailHeader detail={createPluginDetail()} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
expect(screen.getByTestId('verified-badge')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('Version Display', () => {
|
|
it('should show new version indicator when hasNewVersion is true', () => {
|
|
const detail = createPluginDetail({
|
|
version: '1.0.0',
|
|
latest_version: '2.0.0',
|
|
})
|
|
render(<DetailHeader detail={detail} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
// Badge component should render with the version
|
|
expect(screen.getByText('1.0.0')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should not show new version indicator when versions match', () => {
|
|
const detail = createPluginDetail({
|
|
version: '1.0.0',
|
|
latest_version: '1.0.0',
|
|
})
|
|
render(<DetailHeader detail={detail} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
// Badge component should render with the version
|
|
expect(screen.getByText('1.0.0')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should show update button when new version is available', () => {
|
|
const detail = createPluginDetail({
|
|
version: '1.0.0',
|
|
latest_version: '2.0.0',
|
|
})
|
|
render(<DetailHeader detail={detail} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
expect(screen.getByText('detailPanel.operation.update')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should show update button for GitHub source', () => {
|
|
const detail = createPluginDetail({
|
|
source: PluginSource.github,
|
|
meta: { repo: 'owner/repo', version: 'v1.0.0', package: 'pkg' },
|
|
})
|
|
render(<DetailHeader detail={detail} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
expect(screen.getByText('detailPanel.operation.update')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('Auto Upgrade Feature', () => {
|
|
it('should render component when marketplace is disabled', () => {
|
|
mockAutoUpgradeInfo = {
|
|
strategy_setting: 'enabled',
|
|
upgrade_mode: 'update_all',
|
|
include_plugins: [],
|
|
exclude_plugins: [],
|
|
upgrade_time_of_day: 36000,
|
|
}
|
|
|
|
render(<DetailHeader detail={createPluginDetail()} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
expect(screen.getByTestId('title')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render component when strategy is disabled', () => {
|
|
mockAutoUpgradeInfo = {
|
|
strategy_setting: 'disabled',
|
|
upgrade_mode: 'update_all',
|
|
include_plugins: [],
|
|
exclude_plugins: [],
|
|
upgrade_time_of_day: 36000,
|
|
}
|
|
|
|
render(<DetailHeader detail={createPluginDetail()} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
expect(screen.getByTestId('title')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should enable auto upgrade for update_all mode', () => {
|
|
mockAutoUpgradeInfo = {
|
|
strategy_setting: 'enabled',
|
|
upgrade_mode: 'update_all',
|
|
include_plugins: [],
|
|
exclude_plugins: [],
|
|
upgrade_time_of_day: 36000,
|
|
}
|
|
|
|
render(<DetailHeader detail={createPluginDetail()} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
// Auto upgrade badge should be rendered
|
|
expect(screen.getByTestId('title')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should enable auto upgrade for partial mode when plugin is included', () => {
|
|
mockAutoUpgradeInfo = {
|
|
strategy_setting: 'enabled',
|
|
upgrade_mode: 'partial',
|
|
include_plugins: ['test-plugin'],
|
|
exclude_plugins: [],
|
|
upgrade_time_of_day: 36000,
|
|
}
|
|
|
|
render(<DetailHeader detail={createPluginDetail()} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
expect(screen.getByTestId('title')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should not enable auto upgrade for partial mode when plugin is not included', () => {
|
|
mockAutoUpgradeInfo = {
|
|
strategy_setting: 'enabled',
|
|
upgrade_mode: 'partial',
|
|
include_plugins: ['other-plugin'],
|
|
exclude_plugins: [],
|
|
upgrade_time_of_day: 36000,
|
|
}
|
|
|
|
render(<DetailHeader detail={createPluginDetail()} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
expect(screen.getByTestId('title')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should enable auto upgrade for exclude mode when plugin is not excluded', () => {
|
|
mockAutoUpgradeInfo = {
|
|
strategy_setting: 'enabled',
|
|
upgrade_mode: 'exclude',
|
|
include_plugins: [],
|
|
exclude_plugins: ['other-plugin'],
|
|
upgrade_time_of_day: 36000,
|
|
}
|
|
|
|
render(<DetailHeader detail={createPluginDetail()} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
expect(screen.getByTestId('title')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should not enable auto upgrade for exclude mode when plugin is excluded', () => {
|
|
mockAutoUpgradeInfo = {
|
|
strategy_setting: 'enabled',
|
|
upgrade_mode: 'exclude',
|
|
include_plugins: [],
|
|
exclude_plugins: ['test-plugin'],
|
|
upgrade_time_of_day: 36000,
|
|
}
|
|
|
|
render(<DetailHeader detail={createPluginDetail()} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
expect(screen.getByTestId('title')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should not enable auto upgrade for non-marketplace plugins', () => {
|
|
mockAutoUpgradeInfo = {
|
|
strategy_setting: 'enabled',
|
|
upgrade_mode: 'update_all',
|
|
include_plugins: [],
|
|
exclude_plugins: [],
|
|
upgrade_time_of_day: 36000,
|
|
}
|
|
|
|
const detail = createPluginDetail({
|
|
source: PluginSource.github,
|
|
meta: { repo: 'owner/repo', version: 'v1.0.0', package: 'pkg' },
|
|
})
|
|
render(<DetailHeader detail={detail} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
expect(screen.getByTestId('title')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should not enable auto upgrade when marketplace feature is disabled', () => {
|
|
mockEnableMarketplace = false
|
|
mockAutoUpgradeInfo = {
|
|
strategy_setting: 'enabled',
|
|
upgrade_mode: 'update_all',
|
|
include_plugins: [],
|
|
exclude_plugins: [],
|
|
upgrade_time_of_day: 36000,
|
|
}
|
|
|
|
render(<DetailHeader detail={createPluginDetail()} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
// Component should still render but auto upgrade should be disabled
|
|
expect(screen.getByTestId('title')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('User Interactions', () => {
|
|
it('should call onHide when close button clicked', () => {
|
|
render(<DetailHeader detail={createPluginDetail()} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
// Find the close button (ActionButton with action-btn class)
|
|
const actionButtons = screen.getAllByRole('button').filter(btn => btn.classList.contains('action-btn'))
|
|
fireEvent.click(actionButtons[actionButtons.length - 1])
|
|
|
|
expect(mockOnHide).toHaveBeenCalled()
|
|
})
|
|
|
|
it('should have info button available', () => {
|
|
render(<DetailHeader detail={createPluginDetail()} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
const infoBtn = screen.getByTestId('info-btn')
|
|
fireEvent.click(infoBtn)
|
|
|
|
expect(infoBtn).toBeInTheDocument()
|
|
})
|
|
|
|
it('should have check version button available', () => {
|
|
render(<DetailHeader detail={createPluginDetail()} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
const checkBtn = screen.getByTestId('check-version-btn')
|
|
fireEvent.click(checkBtn)
|
|
|
|
expect(checkBtn).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('Update Flow - Marketplace', () => {
|
|
it('should have update button for new version', () => {
|
|
const detail = createPluginDetail({
|
|
version: '1.0.0',
|
|
latest_version: '2.0.0',
|
|
})
|
|
render(<DetailHeader detail={detail} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
const updateBtn = screen.getByText('detailPanel.operation.update')
|
|
fireEvent.click(updateBtn)
|
|
|
|
expect(updateBtn).toBeInTheDocument()
|
|
})
|
|
|
|
it('should have version picker select button', () => {
|
|
render(<DetailHeader detail={createPluginDetail()} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
const selectBtn = screen.getByTestId('select-version-btn')
|
|
fireEvent.click(selectBtn)
|
|
|
|
expect(selectBtn).toBeInTheDocument()
|
|
})
|
|
|
|
it('should have downgrade button', () => {
|
|
render(<DetailHeader detail={createPluginDetail()} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
const downgradeBtn = screen.getByTestId('select-downgrade-btn')
|
|
fireEvent.click(downgradeBtn)
|
|
|
|
expect(downgradeBtn).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('Update Flow - GitHub', () => {
|
|
it('should check for updates from GitHub when update clicked', async () => {
|
|
const detail = createPluginDetail({
|
|
source: PluginSource.github,
|
|
meta: { repo: 'owner/repo', version: 'v1.0.0', package: 'pkg' },
|
|
})
|
|
render(<DetailHeader detail={detail} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
fireEvent.click(screen.getByText('detailPanel.operation.update'))
|
|
|
|
await waitFor(() => {
|
|
expect(mockFetchReleases).toHaveBeenCalledWith('owner', 'repo')
|
|
})
|
|
})
|
|
|
|
it('should show toast when no releases found', async () => {
|
|
mockFetchReleases.mockResolvedValueOnce([])
|
|
|
|
const detail = createPluginDetail({
|
|
source: PluginSource.github,
|
|
meta: { repo: 'owner/repo', version: 'v1.0.0', package: 'pkg' },
|
|
})
|
|
render(<DetailHeader detail={detail} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
fireEvent.click(screen.getByText('detailPanel.operation.update'))
|
|
|
|
await waitFor(() => {
|
|
expect(mockFetchReleases).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
it('should show update plugin modal when update is needed', async () => {
|
|
const detail = createPluginDetail({
|
|
source: PluginSource.github,
|
|
meta: { repo: 'owner/repo', version: 'v1.0.0', package: 'pkg' },
|
|
})
|
|
render(<DetailHeader detail={detail} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
fireEvent.click(screen.getByText('detailPanel.operation.update'))
|
|
|
|
await waitFor(() => {
|
|
expect(mockSetShowUpdatePluginModal).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
it('should call onUpdate via onSaveCallback when GitHub update completes', async () => {
|
|
mockSetShowUpdatePluginModal.mockImplementation(({ onSaveCallback }) => {
|
|
// Simulate the modal completing and calling onSaveCallback
|
|
onSaveCallback()
|
|
})
|
|
|
|
const detail = createPluginDetail({
|
|
source: PluginSource.github,
|
|
meta: { repo: 'owner/repo', version: 'v1.0.0', package: 'pkg' },
|
|
})
|
|
render(<DetailHeader detail={detail} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
fireEvent.click(screen.getByText('detailPanel.operation.update'))
|
|
|
|
await waitFor(() => {
|
|
expect(mockOnUpdate).toHaveBeenCalled()
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Delete Flow', () => {
|
|
it('should have remove button available', () => {
|
|
render(<DetailHeader detail={createPluginDetail()} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
const removeBtn = screen.getByTestId('remove-btn')
|
|
fireEvent.click(removeBtn)
|
|
|
|
expect(removeBtn).toBeInTheDocument()
|
|
})
|
|
|
|
it('should have uninstallPlugin mock defined', () => {
|
|
render(<DetailHeader detail={createPluginDetail()} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
fireEvent.click(screen.getByTestId('remove-btn'))
|
|
|
|
expect(mockUninstallPlugin).toBeDefined()
|
|
})
|
|
|
|
it('should render correctly for model plugin delete', () => {
|
|
const detail = createPluginDetail({
|
|
declaration: {
|
|
...createPluginDetail().declaration,
|
|
category: 'model',
|
|
} as unknown as PluginDetail['declaration'],
|
|
})
|
|
render(<DetailHeader detail={detail} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
expect(screen.getByTestId('remove-btn')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render correctly for tool plugin delete', () => {
|
|
render(<DetailHeader detail={createPluginDetail()} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
expect(screen.getByTestId('remove-btn')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('Plugin Sources', () => {
|
|
it('should render github source icon', () => {
|
|
const detail = createPluginDetail({
|
|
source: PluginSource.github,
|
|
meta: { repo: 'owner/repo', version: 'v1.0.0', package: 'pkg' },
|
|
})
|
|
render(<DetailHeader detail={detail} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
expect(screen.getByTestId('title')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render local source icon', () => {
|
|
const detail = createPluginDetail({ source: PluginSource.local })
|
|
render(<DetailHeader detail={detail} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
expect(screen.getByTestId('title')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render debugging source icon', () => {
|
|
const detail = createPluginDetail({ source: PluginSource.debugging })
|
|
render(<DetailHeader detail={detail} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
expect(screen.getByTestId('title')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should not render deprecation notice for non-marketplace source', () => {
|
|
const detail = createPluginDetail({ source: PluginSource.github, meta: { repo: 'owner/repo', version: 'v1.0.0', package: 'pkg' } })
|
|
render(<DetailHeader detail={detail} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
expect(screen.queryByTestId('deprecation-notice')).not.toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('Detail URL Generation', () => {
|
|
it('should render GitHub source correctly', () => {
|
|
const detail = createPluginDetail({
|
|
source: PluginSource.github,
|
|
meta: { repo: 'owner/repo', version: 'v1.0.0', package: 'pkg' },
|
|
})
|
|
render(<DetailHeader detail={detail} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
expect(screen.getByTestId('operation-dropdown')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render marketplace source correctly', () => {
|
|
render(<DetailHeader detail={createPluginDetail()} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
expect(screen.getByTestId('operation-dropdown')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render local source correctly', () => {
|
|
const detail = createPluginDetail({ source: PluginSource.local })
|
|
render(<DetailHeader detail={detail} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
expect(screen.getByTestId('operation-dropdown')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('Plugin Auth', () => {
|
|
it('should render plugin auth for tool category', () => {
|
|
render(<DetailHeader detail={createPluginDetail()} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
expect(screen.getByTestId('plugin-auth')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should not render plugin auth for non-tool category', () => {
|
|
const detail = createPluginDetail({
|
|
declaration: {
|
|
...createPluginDetail().declaration,
|
|
category: 'model',
|
|
} as unknown as PluginDetail['declaration'],
|
|
})
|
|
render(<DetailHeader detail={detail} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
expect(screen.queryByTestId('plugin-auth')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('should not render plugin auth in readme view', () => {
|
|
render(<DetailHeader detail={createPluginDetail()} isReadmeView onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
expect(screen.queryByTestId('plugin-auth')).not.toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('Edge Cases', () => {
|
|
it('should handle plugin without version', () => {
|
|
const detail = createPluginDetail({ version: '' })
|
|
render(<DetailHeader detail={detail} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
expect(screen.getByTestId('title')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should handle plugin with name containing slash', () => {
|
|
const detail = createPluginDetail({
|
|
declaration: {
|
|
...createPluginDetail().declaration,
|
|
name: 'org/plugin-name',
|
|
} as unknown as PluginDetail['declaration'],
|
|
})
|
|
render(<DetailHeader detail={detail} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
expect(screen.getByTestId('org-info')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should handle empty icon', () => {
|
|
const detail = createPluginDetail({
|
|
declaration: {
|
|
...createPluginDetail().declaration,
|
|
icon: '',
|
|
} as unknown as PluginDetail['declaration'],
|
|
})
|
|
render(<DetailHeader detail={detail} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
expect(screen.getByTestId('card-icon')).toHaveAttribute('data-src', '')
|
|
})
|
|
})
|
|
|
|
describe('Delete Confirmation Flow', () => {
|
|
it('should show delete confirm when remove button is clicked', async () => {
|
|
render(<DetailHeader detail={createPluginDetail()} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
fireEvent.click(screen.getByTestId('remove-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('delete-confirm')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('should hide delete confirm when cancel is clicked', async () => {
|
|
render(<DetailHeader detail={createPluginDetail()} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
fireEvent.click(screen.getByTestId('remove-btn'))
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('delete-confirm')).toBeInTheDocument()
|
|
})
|
|
|
|
fireEvent.click(screen.getByTestId('confirm-cancel'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.queryByTestId('delete-confirm')).not.toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('should call uninstallPlugin when confirm delete is clicked', async () => {
|
|
render(<DetailHeader detail={createPluginDetail()} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
fireEvent.click(screen.getByTestId('remove-btn'))
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('delete-confirm')).toBeInTheDocument()
|
|
})
|
|
|
|
fireEvent.click(screen.getByTestId('confirm-ok'))
|
|
|
|
await waitFor(() => {
|
|
expect(mockUninstallPlugin).toHaveBeenCalledWith('test-id')
|
|
})
|
|
})
|
|
|
|
it('should call onUpdate with true after successful delete', async () => {
|
|
render(<DetailHeader detail={createPluginDetail()} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
fireEvent.click(screen.getByTestId('remove-btn'))
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('delete-confirm')).toBeInTheDocument()
|
|
})
|
|
|
|
fireEvent.click(screen.getByTestId('confirm-ok'))
|
|
|
|
await waitFor(() => {
|
|
expect(mockOnUpdate).toHaveBeenCalledWith(true)
|
|
})
|
|
})
|
|
|
|
it('should refresh model providers when deleting model plugin', async () => {
|
|
const detail = createPluginDetail({
|
|
declaration: {
|
|
...createPluginDetail().declaration,
|
|
category: 'model',
|
|
} as unknown as PluginDetail['declaration'],
|
|
})
|
|
render(<DetailHeader detail={detail} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
fireEvent.click(screen.getByTestId('remove-btn'))
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('delete-confirm')).toBeInTheDocument()
|
|
})
|
|
|
|
fireEvent.click(screen.getByTestId('confirm-ok'))
|
|
|
|
await waitFor(() => {
|
|
expect(mockRefreshModelProviders).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
it('should invalidate tool providers when deleting tool plugin', async () => {
|
|
render(<DetailHeader detail={createPluginDetail()} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
fireEvent.click(screen.getByTestId('remove-btn'))
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('delete-confirm')).toBeInTheDocument()
|
|
})
|
|
|
|
fireEvent.click(screen.getByTestId('confirm-ok'))
|
|
|
|
await waitFor(() => {
|
|
expect(mockInvalidateAllToolProviders).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
it('should track plugin uninstalled event after successful delete', async () => {
|
|
render(<DetailHeader detail={createPluginDetail()} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
fireEvent.click(screen.getByTestId('remove-btn'))
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('delete-confirm')).toBeInTheDocument()
|
|
})
|
|
|
|
fireEvent.click(screen.getByTestId('confirm-ok'))
|
|
|
|
await waitFor(() => {
|
|
expect(amplitude.trackEvent).toHaveBeenCalledWith('plugin_uninstalled', expect.any(Object))
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Update Modal Flow', () => {
|
|
it('should show update modal when update button clicked for marketplace plugin', async () => {
|
|
const detail = createPluginDetail({
|
|
version: '1.0.0',
|
|
latest_version: '2.0.0',
|
|
})
|
|
render(<DetailHeader detail={detail} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
fireEvent.click(screen.getByText('detailPanel.operation.update'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('update-modal')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('should call onUpdate when save is clicked in update modal', async () => {
|
|
const detail = createPluginDetail({
|
|
version: '1.0.0',
|
|
latest_version: '2.0.0',
|
|
})
|
|
render(<DetailHeader detail={detail} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
fireEvent.click(screen.getByText('detailPanel.operation.update'))
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('update-modal')).toBeInTheDocument()
|
|
})
|
|
|
|
fireEvent.click(screen.getByTestId('update-modal-save'))
|
|
|
|
await waitFor(() => {
|
|
expect(mockOnUpdate).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
it('should hide update modal when cancel is clicked', async () => {
|
|
const detail = createPluginDetail({
|
|
version: '1.0.0',
|
|
latest_version: '2.0.0',
|
|
})
|
|
render(<DetailHeader detail={detail} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
fireEvent.click(screen.getByText('detailPanel.operation.update'))
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('update-modal')).toBeInTheDocument()
|
|
})
|
|
|
|
fireEvent.click(screen.getByTestId('update-modal-cancel'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.queryByTestId('update-modal')).not.toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Plugin Info Modal', () => {
|
|
it('should show plugin info modal when info button is clicked', async () => {
|
|
render(<DetailHeader detail={createPluginDetail()} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
fireEvent.click(screen.getByTestId('info-btn'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('plugin-info')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('should hide plugin info modal when close button is clicked', async () => {
|
|
render(<DetailHeader detail={createPluginDetail()} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
fireEvent.click(screen.getByTestId('info-btn'))
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('plugin-info')).toBeInTheDocument()
|
|
})
|
|
|
|
fireEvent.click(screen.getByTestId('plugin-info-close'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.queryByTestId('plugin-info')).not.toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('should render plugin info with GitHub meta data', () => {
|
|
const detail = createPluginDetail({
|
|
source: PluginSource.github,
|
|
meta: { repo: 'owner/repo', version: 'v1.0.0', package: 'test-pkg' },
|
|
})
|
|
render(<DetailHeader detail={detail} onUpdate={mockOnUpdate} onHide={mockOnHide} />)
|
|
|
|
expect(screen.getByTestId('info-btn')).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|