mirror of https://github.com/langgenius/dify.git
526 lines
17 KiB
TypeScript
526 lines
17 KiB
TypeScript
import type { Plugin, PluginDeclaration, UpdateFromGitHubPayload } from '../../../types'
|
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { PluginCategoryEnum, TaskStatus } from '../../../types'
|
|
import Loaded from './loaded'
|
|
|
|
// Mock dependencies
|
|
const mockUseCheckInstalled = vi.fn()
|
|
vi.mock('@/app/components/plugins/install-plugin/hooks/use-check-installed', () => ({
|
|
default: (params: { pluginIds: string[], enabled: boolean }) => mockUseCheckInstalled(params),
|
|
}))
|
|
|
|
const mockUpdateFromGitHub = vi.fn()
|
|
vi.mock('@/service/plugins', () => ({
|
|
updateFromGitHub: (...args: unknown[]) => mockUpdateFromGitHub(...args),
|
|
}))
|
|
|
|
const mockInstallPackageFromGitHub = vi.fn()
|
|
const mockHandleRefetch = vi.fn()
|
|
vi.mock('@/service/use-plugins', () => ({
|
|
useInstallPackageFromGitHub: () => ({ mutateAsync: mockInstallPackageFromGitHub }),
|
|
usePluginTaskList: () => ({ handleRefetch: mockHandleRefetch }),
|
|
}))
|
|
|
|
const mockCheck = vi.fn()
|
|
vi.mock('../../base/check-task-status', () => ({
|
|
default: () => ({ check: mockCheck }),
|
|
}))
|
|
|
|
// Mock Card component
|
|
vi.mock('../../../card', () => ({
|
|
default: ({ payload, titleLeft }: { payload: Plugin, titleLeft?: React.ReactNode }) => (
|
|
<div data-testid="plugin-card">
|
|
<span data-testid="card-name">{payload.name}</span>
|
|
{titleLeft && <span data-testid="title-left">{titleLeft}</span>}
|
|
</div>
|
|
),
|
|
}))
|
|
|
|
// Mock Version component
|
|
vi.mock('../../base/version', () => ({
|
|
default: ({ hasInstalled, installedVersion, toInstallVersion }: {
|
|
hasInstalled: boolean
|
|
installedVersion?: string
|
|
toInstallVersion: string
|
|
}) => (
|
|
<span data-testid="version-info">
|
|
{hasInstalled ? `Update from ${installedVersion} to ${toInstallVersion}` : `Install ${toInstallVersion}`}
|
|
</span>
|
|
),
|
|
}))
|
|
|
|
// Factory functions
|
|
const createMockPayload = (overrides: Partial<PluginDeclaration> = {}): PluginDeclaration => ({
|
|
plugin_unique_identifier: 'test-uid',
|
|
version: '1.0.0',
|
|
author: 'test-author',
|
|
icon: 'icon.png',
|
|
name: 'Test Plugin',
|
|
category: PluginCategoryEnum.tool,
|
|
label: { 'en-US': 'Test' } as PluginDeclaration['label'],
|
|
description: { 'en-US': 'Test Description' } as PluginDeclaration['description'],
|
|
created_at: '2024-01-01',
|
|
resource: {},
|
|
plugins: [],
|
|
verified: true,
|
|
endpoint: { settings: [], endpoints: [] },
|
|
model: null,
|
|
tags: [],
|
|
agent_strategy: null,
|
|
meta: { version: '1.0.0' },
|
|
trigger: {} as PluginDeclaration['trigger'],
|
|
...overrides,
|
|
})
|
|
|
|
const createMockPluginPayload = (overrides: Partial<Plugin> = {}): Plugin => ({
|
|
type: 'plugin',
|
|
org: 'test-org',
|
|
name: 'Test Plugin',
|
|
plugin_id: 'test-plugin-id',
|
|
version: '1.0.0',
|
|
latest_version: '1.0.0',
|
|
latest_package_identifier: 'test-pkg',
|
|
icon: 'icon.png',
|
|
verified: true,
|
|
label: { 'en-US': 'Test' },
|
|
brief: { 'en-US': 'Brief' },
|
|
description: { 'en-US': 'Description' },
|
|
introduction: 'Intro',
|
|
repository: '',
|
|
category: PluginCategoryEnum.tool,
|
|
install_count: 100,
|
|
endpoint: { settings: [] },
|
|
tags: [],
|
|
badges: [],
|
|
verification: { authorized_category: 'langgenius' },
|
|
from: 'github',
|
|
...overrides,
|
|
})
|
|
|
|
const createUpdatePayload = (): UpdateFromGitHubPayload => ({
|
|
originalPackageInfo: {
|
|
id: 'original-id',
|
|
repo: 'owner/repo',
|
|
version: 'v0.9.0',
|
|
package: 'plugin.zip',
|
|
releases: [],
|
|
},
|
|
})
|
|
|
|
describe('Loaded', () => {
|
|
const defaultProps = {
|
|
updatePayload: undefined,
|
|
uniqueIdentifier: 'test-unique-id',
|
|
payload: createMockPayload() as PluginDeclaration | Plugin,
|
|
repoUrl: 'https://github.com/owner/repo',
|
|
selectedVersion: 'v1.0.0',
|
|
selectedPackage: 'plugin.zip',
|
|
onBack: vi.fn(),
|
|
onStartToInstall: vi.fn(),
|
|
onInstalled: vi.fn(),
|
|
onFailed: vi.fn(),
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
mockUseCheckInstalled.mockReturnValue({
|
|
installedInfo: {},
|
|
isLoading: false,
|
|
})
|
|
mockUpdateFromGitHub.mockResolvedValue({ all_installed: true, task_id: 'task-1' })
|
|
mockInstallPackageFromGitHub.mockResolvedValue({ all_installed: true, task_id: 'task-1' })
|
|
mockCheck.mockResolvedValue({ status: TaskStatus.success, error: null })
|
|
})
|
|
|
|
// ================================
|
|
// Rendering Tests
|
|
// ================================
|
|
describe('Rendering', () => {
|
|
it('should render ready to install message', () => {
|
|
render(<Loaded {...defaultProps} />)
|
|
|
|
expect(screen.getByText('plugin.installModal.readyToInstall')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render plugin card', () => {
|
|
render(<Loaded {...defaultProps} />)
|
|
|
|
expect(screen.getByTestId('plugin-card')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render back button when not installing', () => {
|
|
render(<Loaded {...defaultProps} />)
|
|
|
|
expect(screen.getByRole('button', { name: 'plugin.installModal.back' })).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render install button', () => {
|
|
render(<Loaded {...defaultProps} />)
|
|
|
|
expect(screen.getByRole('button', { name: /plugin.installModal.install/i })).toBeInTheDocument()
|
|
})
|
|
|
|
it('should show version info in card title', () => {
|
|
render(<Loaded {...defaultProps} />)
|
|
|
|
expect(screen.getByTestId('version-info')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
// ================================
|
|
// Props Tests
|
|
// ================================
|
|
describe('Props', () => {
|
|
it('should display plugin name from payload', () => {
|
|
render(<Loaded {...defaultProps} />)
|
|
|
|
expect(screen.getByTestId('card-name')).toHaveTextContent('Test Plugin')
|
|
})
|
|
|
|
it('should pass correct version to Version component', () => {
|
|
render(<Loaded {...defaultProps} payload={createMockPayload({ version: '2.0.0' })} />)
|
|
|
|
expect(screen.getByTestId('version-info')).toHaveTextContent('Install 2.0.0')
|
|
})
|
|
})
|
|
|
|
// ================================
|
|
// Button State Tests
|
|
// ================================
|
|
describe('Button State', () => {
|
|
it('should disable install button while loading', () => {
|
|
mockUseCheckInstalled.mockReturnValue({
|
|
installedInfo: {},
|
|
isLoading: true,
|
|
})
|
|
|
|
render(<Loaded {...defaultProps} />)
|
|
|
|
expect(screen.getByRole('button', { name: /plugin.installModal.install/i })).toBeDisabled()
|
|
})
|
|
|
|
it('should enable install button when not loading', () => {
|
|
render(<Loaded {...defaultProps} />)
|
|
|
|
expect(screen.getByRole('button', { name: /plugin.installModal.install/i })).not.toBeDisabled()
|
|
})
|
|
})
|
|
|
|
// ================================
|
|
// User Interactions Tests
|
|
// ================================
|
|
describe('User Interactions', () => {
|
|
it('should call onBack when back button is clicked', () => {
|
|
const onBack = vi.fn()
|
|
render(<Loaded {...defaultProps} onBack={onBack} />)
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.back' }))
|
|
|
|
expect(onBack).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('should call onStartToInstall when install starts', async () => {
|
|
const onStartToInstall = vi.fn()
|
|
render(<Loaded {...defaultProps} onStartToInstall={onStartToInstall} />)
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /plugin.installModal.install/i }))
|
|
|
|
await waitFor(() => {
|
|
expect(onStartToInstall).toHaveBeenCalledTimes(1)
|
|
})
|
|
})
|
|
})
|
|
|
|
// ================================
|
|
// Installation Flow Tests
|
|
// ================================
|
|
describe('Installation Flows', () => {
|
|
it('should call installPackageFromGitHub for fresh install', async () => {
|
|
const onInstalled = vi.fn()
|
|
render(<Loaded {...defaultProps} onInstalled={onInstalled} />)
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /plugin.installModal.install/i }))
|
|
|
|
await waitFor(() => {
|
|
expect(mockInstallPackageFromGitHub).toHaveBeenCalledWith({
|
|
repoUrl: 'owner/repo',
|
|
selectedVersion: 'v1.0.0',
|
|
selectedPackage: 'plugin.zip',
|
|
uniqueIdentifier: 'test-unique-id',
|
|
})
|
|
})
|
|
})
|
|
|
|
it('should call updateFromGitHub when updatePayload is provided', async () => {
|
|
const updatePayload = createUpdatePayload()
|
|
render(<Loaded {...defaultProps} updatePayload={updatePayload} />)
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /plugin.installModal.install/i }))
|
|
|
|
await waitFor(() => {
|
|
expect(mockUpdateFromGitHub).toHaveBeenCalledWith(
|
|
'owner/repo',
|
|
'v1.0.0',
|
|
'plugin.zip',
|
|
'original-id',
|
|
'test-unique-id',
|
|
)
|
|
})
|
|
})
|
|
|
|
it('should call updateFromGitHub when plugin is already installed', async () => {
|
|
mockUseCheckInstalled.mockReturnValue({
|
|
installedInfo: {
|
|
'test-plugin-id': {
|
|
installedVersion: '0.9.0',
|
|
uniqueIdentifier: 'installed-uid',
|
|
},
|
|
},
|
|
isLoading: false,
|
|
})
|
|
|
|
render(<Loaded {...defaultProps} payload={createMockPluginPayload()} />)
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /plugin.installModal.install/i }))
|
|
|
|
await waitFor(() => {
|
|
expect(mockUpdateFromGitHub).toHaveBeenCalledWith(
|
|
'owner/repo',
|
|
'v1.0.0',
|
|
'plugin.zip',
|
|
'installed-uid',
|
|
'test-unique-id',
|
|
)
|
|
})
|
|
})
|
|
|
|
it('should call onInstalled when installation completes immediately', async () => {
|
|
mockInstallPackageFromGitHub.mockResolvedValue({ all_installed: true, task_id: 'task-1' })
|
|
|
|
const onInstalled = vi.fn()
|
|
render(<Loaded {...defaultProps} onInstalled={onInstalled} />)
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /plugin.installModal.install/i }))
|
|
|
|
await waitFor(() => {
|
|
expect(onInstalled).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
it('should check task status when not immediately installed', async () => {
|
|
mockInstallPackageFromGitHub.mockResolvedValue({ all_installed: false, task_id: 'task-1' })
|
|
|
|
render(<Loaded {...defaultProps} />)
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /plugin.installModal.install/i }))
|
|
|
|
await waitFor(() => {
|
|
expect(mockHandleRefetch).toHaveBeenCalled()
|
|
expect(mockCheck).toHaveBeenCalledWith({
|
|
taskId: 'task-1',
|
|
pluginUniqueIdentifier: 'test-unique-id',
|
|
})
|
|
})
|
|
})
|
|
|
|
it('should call onInstalled with true when task succeeds', async () => {
|
|
mockInstallPackageFromGitHub.mockResolvedValue({ all_installed: false, task_id: 'task-1' })
|
|
mockCheck.mockResolvedValue({ status: TaskStatus.success, error: null })
|
|
|
|
const onInstalled = vi.fn()
|
|
render(<Loaded {...defaultProps} onInstalled={onInstalled} />)
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /plugin.installModal.install/i }))
|
|
|
|
await waitFor(() => {
|
|
expect(onInstalled).toHaveBeenCalledWith(true)
|
|
})
|
|
})
|
|
})
|
|
|
|
// ================================
|
|
// Error Handling Tests
|
|
// ================================
|
|
describe('Error Handling', () => {
|
|
it('should call onFailed when task fails', async () => {
|
|
mockInstallPackageFromGitHub.mockResolvedValue({ all_installed: false, task_id: 'task-1' })
|
|
mockCheck.mockResolvedValue({ status: TaskStatus.failed, error: 'Installation failed' })
|
|
|
|
const onFailed = vi.fn()
|
|
render(<Loaded {...defaultProps} onFailed={onFailed} />)
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /plugin.installModal.install/i }))
|
|
|
|
await waitFor(() => {
|
|
expect(onFailed).toHaveBeenCalledWith('Installation failed')
|
|
})
|
|
})
|
|
|
|
it('should call onFailed with string error', async () => {
|
|
mockInstallPackageFromGitHub.mockRejectedValue('String error message')
|
|
|
|
const onFailed = vi.fn()
|
|
render(<Loaded {...defaultProps} onFailed={onFailed} />)
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /plugin.installModal.install/i }))
|
|
|
|
await waitFor(() => {
|
|
expect(onFailed).toHaveBeenCalledWith('String error message')
|
|
})
|
|
})
|
|
|
|
it('should call onFailed without message for non-string errors', async () => {
|
|
mockInstallPackageFromGitHub.mockRejectedValue(new Error('Error object'))
|
|
|
|
const onFailed = vi.fn()
|
|
render(<Loaded {...defaultProps} onFailed={onFailed} />)
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /plugin.installModal.install/i }))
|
|
|
|
await waitFor(() => {
|
|
expect(onFailed).toHaveBeenCalledWith()
|
|
})
|
|
})
|
|
})
|
|
|
|
// ================================
|
|
// Auto-install Effect Tests
|
|
// ================================
|
|
describe('Auto-install Effect', () => {
|
|
it('should call onInstalled when already installed with same identifier', () => {
|
|
mockUseCheckInstalled.mockReturnValue({
|
|
installedInfo: {
|
|
'test-plugin-id': {
|
|
installedVersion: '1.0.0',
|
|
uniqueIdentifier: 'test-unique-id',
|
|
},
|
|
},
|
|
isLoading: false,
|
|
})
|
|
|
|
const onInstalled = vi.fn()
|
|
render(<Loaded {...defaultProps} payload={createMockPluginPayload()} onInstalled={onInstalled} />)
|
|
|
|
expect(onInstalled).toHaveBeenCalled()
|
|
})
|
|
|
|
it('should not call onInstalled when identifiers differ', () => {
|
|
mockUseCheckInstalled.mockReturnValue({
|
|
installedInfo: {
|
|
'test-plugin-id': {
|
|
installedVersion: '1.0.0',
|
|
uniqueIdentifier: 'different-uid',
|
|
},
|
|
},
|
|
isLoading: false,
|
|
})
|
|
|
|
const onInstalled = vi.fn()
|
|
render(<Loaded {...defaultProps} payload={createMockPluginPayload()} onInstalled={onInstalled} />)
|
|
|
|
expect(onInstalled).not.toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
// ================================
|
|
// Installing State Tests
|
|
// ================================
|
|
describe('Installing State', () => {
|
|
it('should hide back button while installing', async () => {
|
|
let resolveInstall: (value: { all_installed: boolean, task_id: string }) => void
|
|
mockInstallPackageFromGitHub.mockImplementation(() => new Promise((resolve) => {
|
|
resolveInstall = resolve
|
|
}))
|
|
|
|
render(<Loaded {...defaultProps} />)
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /plugin.installModal.install/i }))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.queryByRole('button', { name: 'plugin.installModal.back' })).not.toBeInTheDocument()
|
|
})
|
|
|
|
resolveInstall!({ all_installed: true, task_id: 'task-1' })
|
|
})
|
|
|
|
it('should show installing text while installing', async () => {
|
|
let resolveInstall: (value: { all_installed: boolean, task_id: string }) => void
|
|
mockInstallPackageFromGitHub.mockImplementation(() => new Promise((resolve) => {
|
|
resolveInstall = resolve
|
|
}))
|
|
|
|
render(<Loaded {...defaultProps} />)
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /plugin.installModal.install/i }))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('plugin.installModal.installing')).toBeInTheDocument()
|
|
})
|
|
|
|
resolveInstall!({ all_installed: true, task_id: 'task-1' })
|
|
})
|
|
|
|
it('should not trigger install twice when already installing', async () => {
|
|
let resolveInstall: (value: { all_installed: boolean, task_id: string }) => void
|
|
mockInstallPackageFromGitHub.mockImplementation(() => new Promise((resolve) => {
|
|
resolveInstall = resolve
|
|
}))
|
|
|
|
render(<Loaded {...defaultProps} />)
|
|
|
|
const installButton = screen.getByRole('button', { name: /plugin.installModal.install/i })
|
|
|
|
// Click twice
|
|
fireEvent.click(installButton)
|
|
fireEvent.click(installButton)
|
|
|
|
await waitFor(() => {
|
|
expect(mockInstallPackageFromGitHub).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
resolveInstall!({ all_installed: true, task_id: 'task-1' })
|
|
})
|
|
})
|
|
|
|
// ================================
|
|
// Edge Cases Tests
|
|
// ================================
|
|
describe('Edge Cases', () => {
|
|
it('should handle missing onStartToInstall callback', async () => {
|
|
render(<Loaded {...defaultProps} onStartToInstall={undefined} />)
|
|
|
|
// Should not throw when callback is undefined
|
|
expect(() => {
|
|
fireEvent.click(screen.getByRole('button', { name: /plugin.installModal.install/i }))
|
|
}).not.toThrow()
|
|
|
|
await waitFor(() => {
|
|
expect(mockInstallPackageFromGitHub).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
it('should handle plugin without plugin_id', () => {
|
|
mockUseCheckInstalled.mockReturnValue({
|
|
installedInfo: {},
|
|
isLoading: false,
|
|
})
|
|
|
|
render(<Loaded {...defaultProps} payload={createMockPayload()} />)
|
|
|
|
expect(mockUseCheckInstalled).toHaveBeenCalledWith({
|
|
pluginIds: [undefined],
|
|
enabled: false,
|
|
})
|
|
})
|
|
|
|
it('should preserve state after component update', () => {
|
|
const { rerender } = render(<Loaded {...defaultProps} />)
|
|
|
|
rerender(<Loaded {...defaultProps} />)
|
|
|
|
expect(screen.getByTestId('plugin-card')).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|