dify/web/app/components/plugins/plugin-detail-panel/index.spec.tsx

1145 lines
31 KiB
TypeScript

import type { PluginDeclaration, PluginDetail } from '@/app/components/plugins/types'
import { act, fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { PluginCategoryEnum, PluginSource } from '@/app/components/plugins/types'
import PluginDetailPanel from './index'
// Mock store
const mockSetDetail = vi.fn()
vi.mock('./store', () => ({
usePluginStore: () => ({
setDetail: mockSetDetail,
}),
}))
// Mock DetailHeader
const mockDetailHeaderOnUpdate = vi.fn()
vi.mock('./detail-header', () => ({
default: ({ detail, onUpdate, onHide }: {
detail: PluginDetail
onUpdate: (isDelete?: boolean) => void
onHide: () => void
}) => {
// Capture the onUpdate callback for testing
mockDetailHeaderOnUpdate.mockImplementation(onUpdate)
return (
<div data-testid="detail-header">
<span data-testid="header-title">{detail.name}</span>
<button
data-testid="header-update-btn"
onClick={() => onUpdate()}
>
Update
</button>
<button
data-testid="header-delete-btn"
onClick={() => onUpdate(true)}
>
Delete
</button>
<button
data-testid="header-hide-btn"
onClick={onHide}
>
Hide
</button>
</div>
)
},
}))
// Mock ActionList
vi.mock('./action-list', () => ({
default: ({ detail }: { detail: PluginDetail }) => (
<div data-testid="action-list">
<span data-testid="action-list-plugin-id">{detail.plugin_id}</span>
</div>
),
}))
// Mock AgentStrategyList
vi.mock('./agent-strategy-list', () => ({
default: ({ detail }: { detail: PluginDetail }) => (
<div data-testid="agent-strategy-list">
<span data-testid="strategy-list-plugin-id">{detail.plugin_id}</span>
</div>
),
}))
// Mock EndpointList
vi.mock('./endpoint-list', () => ({
default: ({ detail }: { detail: PluginDetail }) => (
<div data-testid="endpoint-list">
<span data-testid="endpoint-list-plugin-id">{detail.plugin_id}</span>
</div>
),
}))
// Mock ModelList
vi.mock('./model-list', () => ({
default: ({ detail }: { detail: PluginDetail }) => (
<div data-testid="model-list">
<span data-testid="model-list-plugin-id">{detail.plugin_id}</span>
</div>
),
}))
// Mock DatasourceActionList
vi.mock('./datasource-action-list', () => ({
default: ({ detail }: { detail: PluginDetail }) => (
<div data-testid="datasource-action-list">
<span data-testid="datasource-list-plugin-id">{detail.plugin_id}</span>
</div>
),
}))
// Mock SubscriptionList
vi.mock('./subscription-list', () => ({
SubscriptionList: ({ pluginDetail }: { pluginDetail: PluginDetail }) => (
<div data-testid="subscription-list">
<span data-testid="subscription-list-plugin-id">{pluginDetail.plugin_id}</span>
</div>
),
}))
// Mock TriggerEventsList
vi.mock('./trigger/event-list', () => ({
TriggerEventsList: () => (
<div data-testid="trigger-events-list">Events List</div>
),
}))
// Mock ReadmeEntrance
vi.mock('../readme-panel/entrance', () => ({
ReadmeEntrance: ({ pluginDetail, className }: { pluginDetail: PluginDetail, className?: string }) => (
<div data-testid="readme-entrance" className={className}>
<span data-testid="readme-plugin-id">{pluginDetail.plugin_id}</span>
</div>
),
}))
// Mock classnames utility
vi.mock('@/utils/classnames', () => ({
cn: (...args: (string | undefined | false | null)[]) => args.filter(Boolean).join(' '),
}))
// Factory function to create mock PluginDetail
const createPluginDetail = (overrides: Partial<PluginDetail> = {}): PluginDetail => {
const baseDeclaration = {
plugin_unique_identifier: 'test-plugin-uid',
version: '1.0.0',
author: 'test-author',
icon: 'test-icon.png',
name: 'test-plugin',
category: PluginCategoryEnum.tool,
label: { en_US: 'Test Plugin' },
description: { en_US: 'Test plugin description' },
created_at: '2024-01-01T00:00:00Z',
resource: null,
plugins: null,
verified: true,
endpoint: undefined,
tool: {
identity: {
author: 'test-author',
name: 'test-tool',
description: { en_US: 'Test tool' },
icon: 'tool-icon.png',
label: { en_US: 'Test Tool' },
tags: [],
},
credentials_schema: [],
},
model: null,
tags: [],
agent_strategy: null,
meta: { version: '1.0.0' },
trigger: null,
datasource: null,
} as unknown as PluginDeclaration
return {
id: 'test-plugin-id',
created_at: '2024-01-01T00:00:00Z',
updated_at: '2024-01-02T00:00:00Z',
name: 'Test Plugin',
plugin_id: 'test-plugin-id',
plugin_unique_identifier: 'test-plugin-uid',
declaration: baseDeclaration,
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-plugin-uid',
source: PluginSource.marketplace,
meta: undefined,
status: 'active',
deprecated_reason: '',
alternative_plugin_id: '',
...overrides,
}
}
// Factory for trigger plugin
const createTriggerPluginDetail = (overrides: Partial<PluginDetail> = {}): PluginDetail => {
const triggerDeclaration = {
...createPluginDetail().declaration,
category: PluginCategoryEnum.trigger,
tool: undefined,
trigger: {
events: [],
identity: {
author: 'test-author',
name: 'test-trigger',
label: { en_US: 'Test Trigger' },
description: { en_US: 'Test trigger desc' },
icon: 'trigger-icon.png',
tags: [],
},
subscription_constructor: {
credentials_schema: [],
oauth_schema: { client_schema: [], credentials_schema: [] },
parameters: [],
},
subscription_schema: [],
},
} as unknown as PluginDeclaration
return createPluginDetail({
declaration: triggerDeclaration,
...overrides,
})
}
// Factory for model plugin
const createModelPluginDetail = (overrides: Partial<PluginDetail> = {}): PluginDetail => {
return createPluginDetail({
declaration: {
...createPluginDetail().declaration,
category: PluginCategoryEnum.model,
tool: undefined,
model: { provider: 'test-provider' },
},
...overrides,
})
}
// Factory for agent strategy plugin
const createAgentStrategyPluginDetail = (overrides: Partial<PluginDetail> = {}): PluginDetail => {
const strategyDeclaration = {
...createPluginDetail().declaration,
category: PluginCategoryEnum.agent,
tool: undefined,
agent_strategy: {
identity: {
author: 'test-author',
name: 'test-strategy',
label: { en_US: 'Test Strategy' },
description: { en_US: 'Test strategy desc' },
icon: 'strategy-icon.png',
tags: [],
},
},
} as unknown as PluginDeclaration
return createPluginDetail({
declaration: strategyDeclaration,
...overrides,
})
}
// Factory for endpoint plugin
const createEndpointPluginDetail = (overrides: Partial<PluginDetail> = {}): PluginDetail => {
return createPluginDetail({
declaration: {
...createPluginDetail().declaration,
category: PluginCategoryEnum.extension,
tool: undefined,
endpoint: {
settings: [],
endpoints: [{ path: '/test', method: 'GET' }],
},
},
...overrides,
})
}
// Factory for datasource plugin
const createDatasourcePluginDetail = (overrides: Partial<PluginDetail> = {}): PluginDetail => {
const datasourceDeclaration = {
...createPluginDetail().declaration,
category: PluginCategoryEnum.datasource,
tool: undefined,
datasource: {
identity: {
author: 'test-author',
name: 'test-datasource',
description: { en_US: 'Test datasource' },
icon: 'datasource-icon.png',
label: { en_US: 'Test Datasource' },
tags: [],
},
credentials_schema: [],
},
} as unknown as PluginDeclaration
return createPluginDetail({
declaration: datasourceDeclaration,
...overrides,
})
}
describe('PluginDetailPanel', () => {
const mockOnUpdate = vi.fn()
const mockOnHide = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
mockSetDetail.mockClear()
mockOnUpdate.mockClear()
mockOnHide.mockClear()
mockDetailHeaderOnUpdate.mockClear()
})
describe('Rendering', () => {
it('should render nothing when detail is undefined', () => {
const { container } = render(
<PluginDetailPanel
detail={undefined}
onUpdate={mockOnUpdate}
onHide={mockOnHide}
/>,
)
expect(container).toBeEmptyDOMElement()
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
})
it('should render drawer when detail is provided', () => {
const detail = createPluginDetail()
render(
<PluginDetailPanel
detail={detail}
onUpdate={mockOnUpdate}
onHide={mockOnHide}
/>,
)
expect(screen.getByRole('dialog')).toBeInTheDocument()
expect(screen.getByTestId('detail-header')).toBeInTheDocument()
})
it('should render detail header with plugin name', () => {
const detail = createPluginDetail({ name: 'My Custom Plugin' })
render(
<PluginDetailPanel
detail={detail}
onUpdate={mockOnUpdate}
onHide={mockOnHide}
/>,
)
expect(screen.getByTestId('header-title')).toHaveTextContent('My Custom Plugin')
})
it('should render readme entrance with plugin detail', () => {
const detail = createPluginDetail()
render(
<PluginDetailPanel
detail={detail}
onUpdate={mockOnUpdate}
onHide={mockOnHide}
/>,
)
expect(screen.getByTestId('readme-entrance')).toBeInTheDocument()
expect(screen.getByTestId('readme-plugin-id')).toHaveTextContent('test-plugin-id')
})
it('should render drawer with correct styles', () => {
const detail = createPluginDetail()
render(
<PluginDetailPanel
detail={detail}
onUpdate={mockOnUpdate}
onHide={mockOnHide}
/>,
)
const drawer = screen.getByRole('dialog')
expect(drawer).toBeInTheDocument()
})
})
describe('Conditional Rendering by Plugin Category', () => {
it('should render ActionList for tool plugins', () => {
const detail = createPluginDetail()
render(
<PluginDetailPanel
detail={detail}
onUpdate={mockOnUpdate}
onHide={mockOnHide}
/>,
)
expect(screen.getByTestId('action-list')).toBeInTheDocument()
expect(screen.queryByTestId('model-list')).not.toBeInTheDocument()
expect(screen.queryByTestId('endpoint-list')).not.toBeInTheDocument()
expect(screen.queryByTestId('agent-strategy-list')).not.toBeInTheDocument()
expect(screen.queryByTestId('subscription-list')).not.toBeInTheDocument()
})
it('should render ModelList for model plugins', () => {
const detail = createModelPluginDetail()
render(
<PluginDetailPanel
detail={detail}
onUpdate={mockOnUpdate}
onHide={mockOnHide}
/>,
)
expect(screen.getByTestId('model-list')).toBeInTheDocument()
expect(screen.queryByTestId('action-list')).not.toBeInTheDocument()
})
it('should render AgentStrategyList for agent strategy plugins', () => {
const detail = createAgentStrategyPluginDetail()
render(
<PluginDetailPanel
detail={detail}
onUpdate={mockOnUpdate}
onHide={mockOnHide}
/>,
)
expect(screen.getByTestId('agent-strategy-list')).toBeInTheDocument()
expect(screen.queryByTestId('action-list')).not.toBeInTheDocument()
})
it('should render EndpointList for endpoint plugins', () => {
const detail = createEndpointPluginDetail()
render(
<PluginDetailPanel
detail={detail}
onUpdate={mockOnUpdate}
onHide={mockOnHide}
/>,
)
expect(screen.getByTestId('endpoint-list')).toBeInTheDocument()
expect(screen.queryByTestId('action-list')).not.toBeInTheDocument()
})
it('should render DatasourceActionList for datasource plugins', () => {
const detail = createDatasourcePluginDetail()
render(
<PluginDetailPanel
detail={detail}
onUpdate={mockOnUpdate}
onHide={mockOnHide}
/>,
)
expect(screen.getByTestId('datasource-action-list')).toBeInTheDocument()
expect(screen.queryByTestId('action-list')).not.toBeInTheDocument()
})
it('should render SubscriptionList and TriggerEventsList for trigger plugins', () => {
const detail = createTriggerPluginDetail()
render(
<PluginDetailPanel
detail={detail}
onUpdate={mockOnUpdate}
onHide={mockOnHide}
/>,
)
expect(screen.getByTestId('subscription-list')).toBeInTheDocument()
expect(screen.getByTestId('trigger-events-list')).toBeInTheDocument()
expect(screen.queryByTestId('action-list')).not.toBeInTheDocument()
})
it('should render multiple lists when plugin has multiple declarations', () => {
const detail = createPluginDetail({
declaration: {
...createPluginDetail().declaration,
tool: createPluginDetail().declaration.tool,
endpoint: {
settings: [],
endpoints: [{ path: '/api', method: 'POST' }],
},
},
})
render(
<PluginDetailPanel
detail={detail}
onUpdate={mockOnUpdate}
onHide={mockOnHide}
/>,
)
expect(screen.getByTestId('action-list')).toBeInTheDocument()
expect(screen.getByTestId('endpoint-list')).toBeInTheDocument()
})
})
describe('Side Effects and Cleanup', () => {
it('should call setDetail with correct data when detail is provided', () => {
const detail = createPluginDetail({
plugin_id: 'my-plugin-id',
plugin_unique_identifier: 'my-plugin-uid',
name: 'My Plugin',
id: 'detail-id',
})
render(
<PluginDetailPanel
detail={detail}
onUpdate={mockOnUpdate}
onHide={mockOnHide}
/>,
)
expect(mockSetDetail).toHaveBeenCalledTimes(1)
expect(mockSetDetail).toHaveBeenCalledWith(expect.objectContaining({
plugin_id: 'my-plugin-id',
plugin_unique_identifier: 'my-plugin-uid',
name: 'My Plugin',
id: 'detail-id',
provider: 'my-plugin-id/test-plugin',
}))
})
it('should call setDetail with undefined when detail becomes undefined', () => {
const detail = createPluginDetail()
const { rerender } = render(
<PluginDetailPanel
detail={detail}
onUpdate={mockOnUpdate}
onHide={mockOnHide}
/>,
)
expect(mockSetDetail).toHaveBeenCalledTimes(1)
rerender(
<PluginDetailPanel
detail={undefined}
onUpdate={mockOnUpdate}
onHide={mockOnHide}
/>,
)
expect(mockSetDetail).toHaveBeenCalledTimes(2)
expect(mockSetDetail).toHaveBeenLastCalledWith(undefined)
})
it('should update store when detail changes', () => {
const detail1 = createPluginDetail({ plugin_id: 'plugin-1' })
const detail2 = createPluginDetail({ plugin_id: 'plugin-2' })
const { rerender } = render(
<PluginDetailPanel
detail={detail1}
onUpdate={mockOnUpdate}
onHide={mockOnHide}
/>,
)
expect(mockSetDetail).toHaveBeenCalledTimes(1)
expect(mockSetDetail).toHaveBeenLastCalledWith(expect.objectContaining({
plugin_id: 'plugin-1',
}))
rerender(
<PluginDetailPanel
detail={detail2}
onUpdate={mockOnUpdate}
onHide={mockOnHide}
/>,
)
expect(mockSetDetail).toHaveBeenCalledTimes(2)
expect(mockSetDetail).toHaveBeenLastCalledWith(expect.objectContaining({
plugin_id: 'plugin-2',
}))
})
it('should include declaration in setDetail call', () => {
const detail = createPluginDetail()
render(
<PluginDetailPanel
detail={detail}
onUpdate={mockOnUpdate}
onHide={mockOnHide}
/>,
)
expect(mockSetDetail).toHaveBeenCalledWith(expect.objectContaining({
declaration: expect.any(Object),
}))
})
})
describe('Callback Stability and Memoization', () => {
it('should maintain stable callback reference via useCallback', () => {
const detail = createPluginDetail()
const onUpdate = vi.fn()
const onHide = vi.fn()
// Test that the callback is created with useCallback by verifying
// it depends on onHide and onUpdate (tested in other tests)
// This test verifies the basic rendering doesn't change the functionality
const { rerender } = render(
<PluginDetailPanel
detail={detail}
onUpdate={onUpdate}
onHide={onHide}
/>,
)
// Initial click should work
fireEvent.click(screen.getByTestId('header-update-btn'))
expect(onUpdate).toHaveBeenCalledTimes(1)
// Re-render with same props
rerender(
<PluginDetailPanel
detail={detail}
onUpdate={onUpdate}
onHide={onHide}
/>,
)
// Callback should still work after re-render
fireEvent.click(screen.getByTestId('header-update-btn'))
expect(onUpdate).toHaveBeenCalledTimes(2)
})
it('should update handleUpdate when onUpdate prop changes', () => {
const detail = createPluginDetail()
const onUpdate1 = vi.fn()
const onUpdate2 = vi.fn()
const onHide = vi.fn()
const { rerender } = render(
<PluginDetailPanel
detail={detail}
onUpdate={onUpdate1}
onHide={onHide}
/>,
)
fireEvent.click(screen.getByTestId('header-update-btn'))
expect(onUpdate1).toHaveBeenCalledTimes(1)
rerender(
<PluginDetailPanel
detail={detail}
onUpdate={onUpdate2}
onHide={onHide}
/>,
)
fireEvent.click(screen.getByTestId('header-update-btn'))
expect(onUpdate2).toHaveBeenCalledTimes(1)
})
it('should update handleUpdate when onHide prop changes', () => {
const detail = createPluginDetail()
const onUpdate = vi.fn()
const onHide1 = vi.fn()
const onHide2 = vi.fn()
const { rerender } = render(
<PluginDetailPanel
detail={detail}
onUpdate={onUpdate}
onHide={onHide1}
/>,
)
fireEvent.click(screen.getByTestId('header-delete-btn'))
expect(onHide1).toHaveBeenCalledTimes(1)
rerender(
<PluginDetailPanel
detail={detail}
onUpdate={onUpdate}
onHide={onHide2}
/>,
)
onUpdate.mockClear()
fireEvent.click(screen.getByTestId('header-delete-btn'))
expect(onHide2).toHaveBeenCalledTimes(1)
})
})
describe('User Interactions and Event Handlers', () => {
it('should call onUpdate when update button is clicked', () => {
const detail = createPluginDetail()
render(
<PluginDetailPanel
detail={detail}
onUpdate={mockOnUpdate}
onHide={mockOnHide}
/>,
)
fireEvent.click(screen.getByTestId('header-update-btn'))
expect(mockOnUpdate).toHaveBeenCalledTimes(1)
expect(mockOnHide).not.toHaveBeenCalled()
})
it('should call onHide and onUpdate when delete is triggered', () => {
const detail = createPluginDetail()
render(
<PluginDetailPanel
detail={detail}
onUpdate={mockOnUpdate}
onHide={mockOnHide}
/>,
)
fireEvent.click(screen.getByTestId('header-delete-btn'))
expect(mockOnHide).toHaveBeenCalledTimes(1)
expect(mockOnUpdate).toHaveBeenCalledTimes(1)
})
it('should call onHide before onUpdate when isDelete is true', () => {
const callOrder: string[] = []
const onUpdate = vi.fn(() => callOrder.push('update'))
const onHide = vi.fn(() => callOrder.push('hide'))
const detail = createPluginDetail()
render(
<PluginDetailPanel
detail={detail}
onUpdate={onUpdate}
onHide={onHide}
/>,
)
fireEvent.click(screen.getByTestId('header-delete-btn'))
expect(callOrder).toEqual(['hide', 'update'])
})
it('should call only onUpdate when isDelete is false', () => {
const detail = createPluginDetail()
render(
<PluginDetailPanel
detail={detail}
onUpdate={mockOnUpdate}
onHide={mockOnHide}
/>,
)
fireEvent.click(screen.getByTestId('header-update-btn'))
expect(mockOnUpdate).toHaveBeenCalledTimes(1)
expect(mockOnHide).not.toHaveBeenCalled()
})
it('should call onHide when hide button is clicked', () => {
const detail = createPluginDetail()
render(
<PluginDetailPanel
detail={detail}
onUpdate={mockOnUpdate}
onHide={mockOnHide}
/>,
)
fireEvent.click(screen.getByTestId('header-hide-btn'))
expect(mockOnHide).toHaveBeenCalledTimes(1)
})
it('should call onHide when drawer close is triggered', () => {
const detail = createPluginDetail()
render(
<PluginDetailPanel
detail={detail}
onUpdate={mockOnUpdate}
onHide={mockOnHide}
/>,
)
// Click the hide button in the header to close the drawer
fireEvent.click(screen.getByTestId('header-hide-btn'))
expect(mockOnHide).toHaveBeenCalledTimes(1)
})
})
describe('Edge Cases and Error Handling', () => {
it('should handle plugin with empty declaration name gracefully', () => {
const detail = createPluginDetail({
declaration: {
...createPluginDetail().declaration,
name: '',
},
})
render(
<PluginDetailPanel
detail={detail}
onUpdate={mockOnUpdate}
onHide={mockOnHide}
/>,
)
expect(mockSetDetail).toHaveBeenCalledWith(expect.objectContaining({
provider: expect.stringContaining('/'),
}))
})
it('should handle plugin with empty plugin_unique_identifier', () => {
const detail = createPluginDetail({
plugin_unique_identifier: '',
})
render(
<PluginDetailPanel
detail={detail}
onUpdate={mockOnUpdate}
onHide={mockOnHide}
/>,
)
expect(mockSetDetail).toHaveBeenCalledWith(expect.objectContaining({
plugin_unique_identifier: '',
}))
})
it('should handle plugin with undefined plugin_unique_identifier', () => {
const detail = createPluginDetail({
plugin_unique_identifier: undefined as unknown as string,
})
render(
<PluginDetailPanel
detail={detail}
onUpdate={mockOnUpdate}
onHide={mockOnHide}
/>,
)
expect(screen.getByRole('dialog')).toBeInTheDocument()
})
it('should handle plugin without tool, model, endpoint, agent_strategy, or datasource', () => {
const emptyDeclaration = {
...createPluginDetail().declaration,
tool: undefined,
model: undefined,
endpoint: undefined,
agent_strategy: undefined,
datasource: undefined,
category: PluginCategoryEnum.extension,
} as unknown as PluginDeclaration
const detail = createPluginDetail({
declaration: emptyDeclaration,
})
render(
<PluginDetailPanel
detail={detail}
onUpdate={mockOnUpdate}
onHide={mockOnHide}
/>,
)
expect(screen.getByRole('dialog')).toBeInTheDocument()
expect(screen.queryByTestId('action-list')).not.toBeInTheDocument()
expect(screen.queryByTestId('model-list')).not.toBeInTheDocument()
expect(screen.queryByTestId('endpoint-list')).not.toBeInTheDocument()
expect(screen.queryByTestId('agent-strategy-list')).not.toBeInTheDocument()
expect(screen.queryByTestId('datasource-action-list')).not.toBeInTheDocument()
})
it('should handle rapid prop changes without errors', () => {
const detail1 = createPluginDetail({ plugin_id: 'plugin-1' })
const detail2 = createPluginDetail({ plugin_id: 'plugin-2' })
const detail3 = createPluginDetail({ plugin_id: 'plugin-3' })
const { rerender } = render(
<PluginDetailPanel
detail={detail1}
onUpdate={mockOnUpdate}
onHide={mockOnHide}
/>,
)
act(() => {
rerender(
<PluginDetailPanel
detail={detail2}
onUpdate={mockOnUpdate}
onHide={mockOnHide}
/>,
)
})
act(() => {
rerender(
<PluginDetailPanel
detail={detail3}
onUpdate={mockOnUpdate}
onHide={mockOnHide}
/>,
)
})
expect(mockSetDetail).toHaveBeenCalledTimes(3)
expect(screen.getByRole('dialog')).toBeInTheDocument()
})
it('should handle toggle between defined and undefined detail', () => {
const detail = createPluginDetail()
const { rerender, container } = render(
<PluginDetailPanel
detail={detail}
onUpdate={mockOnUpdate}
onHide={mockOnHide}
/>,
)
expect(screen.getByRole('dialog')).toBeInTheDocument()
rerender(
<PluginDetailPanel
detail={undefined}
onUpdate={mockOnUpdate}
onHide={mockOnHide}
/>,
)
expect(container).toBeEmptyDOMElement()
rerender(
<PluginDetailPanel
detail={detail}
onUpdate={mockOnUpdate}
onHide={mockOnHide}
/>,
)
expect(screen.getByRole('dialog')).toBeInTheDocument()
})
})
describe('Props Variations', () => {
it('should pass correct props to DetailHeader', () => {
const detail = createPluginDetail({ name: 'Custom Plugin Name' })
render(
<PluginDetailPanel
detail={detail}
onUpdate={mockOnUpdate}
onHide={mockOnHide}
/>,
)
expect(screen.getByTestId('header-title')).toHaveTextContent('Custom Plugin Name')
})
it('should handle different plugin sources', () => {
const sources: PluginSource[] = [
PluginSource.marketplace,
PluginSource.github,
PluginSource.local,
PluginSource.debugging,
]
sources.forEach((source) => {
const detail = createPluginDetail({ source })
const { unmount } = render(
<PluginDetailPanel
detail={detail}
onUpdate={mockOnUpdate}
onHide={mockOnHide}
/>,
)
expect(screen.getByRole('dialog')).toBeInTheDocument()
unmount()
})
})
it('should handle different plugin statuses', () => {
const statuses: Array<'active' | 'deleted'> = ['active', 'deleted']
statuses.forEach((status) => {
const detail = createPluginDetail({ status })
const { unmount } = render(
<PluginDetailPanel
detail={detail}
onUpdate={mockOnUpdate}
onHide={mockOnHide}
/>,
)
expect(screen.getByRole('dialog')).toBeInTheDocument()
unmount()
})
})
it('should handle plugin with deprecated_reason', () => {
const detail = createPluginDetail({
deprecated_reason: 'This plugin is deprecated',
alternative_plugin_id: 'alternative-plugin',
})
render(
<PluginDetailPanel
detail={detail}
onUpdate={mockOnUpdate}
onHide={mockOnHide}
/>,
)
expect(screen.getByRole('dialog')).toBeInTheDocument()
})
it('should handle plugin with meta data for github source', () => {
const detail = createPluginDetail({
source: PluginSource.github,
meta: {
repo: 'owner/repo-name',
version: 'v1.2.3',
package: 'package.difypkg',
},
})
render(
<PluginDetailPanel
detail={detail}
onUpdate={mockOnUpdate}
onHide={mockOnHide}
/>,
)
expect(screen.getByRole('dialog')).toBeInTheDocument()
})
it('should handle plugin with different versions', () => {
const detail = createPluginDetail({
version: '1.0.0',
latest_version: '2.0.0',
latest_unique_identifier: 'new-uid',
})
render(
<PluginDetailPanel
detail={detail}
onUpdate={mockOnUpdate}
onHide={mockOnHide}
/>,
)
expect(screen.getByRole('dialog')).toBeInTheDocument()
})
it('should pass pluginDetail to SubscriptionList for trigger plugins', () => {
const detail = createTriggerPluginDetail({ plugin_id: 'trigger-plugin-123' })
render(
<PluginDetailPanel
detail={detail}
onUpdate={mockOnUpdate}
onHide={mockOnHide}
/>,
)
expect(screen.getByTestId('subscription-list-plugin-id')).toHaveTextContent('trigger-plugin-123')
})
it('should pass detail to ActionList for tool plugins', () => {
const detail = createPluginDetail({ plugin_id: 'tool-plugin-456' })
render(
<PluginDetailPanel
detail={detail}
onUpdate={mockOnUpdate}
onHide={mockOnHide}
/>,
)
expect(screen.getByTestId('action-list-plugin-id')).toHaveTextContent('tool-plugin-456')
})
})
describe('Store Integration', () => {
it('should construct provider correctly from plugin_id and declaration.name', () => {
const detail = createPluginDetail({
plugin_id: 'my-org/my-plugin',
declaration: {
...createPluginDetail().declaration,
name: 'my-tool-name',
},
})
render(
<PluginDetailPanel
detail={detail}
onUpdate={mockOnUpdate}
onHide={mockOnHide}
/>,
)
expect(mockSetDetail).toHaveBeenCalledWith(expect.objectContaining({
provider: 'my-org/my-plugin/my-tool-name',
}))
})
it('should include all required fields in setDetail payload', () => {
const detail = createPluginDetail()
render(
<PluginDetailPanel
detail={detail}
onUpdate={mockOnUpdate}
onHide={mockOnHide}
/>,
)
expect(mockSetDetail).toHaveBeenCalledWith({
plugin_id: detail.plugin_id,
provider: expect.any(String),
plugin_unique_identifier: detail.plugin_unique_identifier,
declaration: detail.declaration,
name: detail.name,
id: detail.id,
})
})
})
})