dify/web/app/components/plugins/plugin-mutation-model/index.spec.tsx

1162 lines
34 KiB
TypeScript

import type { Plugin } from '../types'
import { fireEvent, render, screen } from '@testing-library/react'
import * as React from 'react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { PluginCategoryEnum } from '../types'
import PluginMutationModal from './index'
// ================================
// Mock External Dependencies Only
// ================================
// Mock react-i18next (translation hook)
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}))
// Mock useMixedTranslation hook
vi.mock('../marketplace/hooks', () => ({
useMixedTranslation: (_locale?: string) => ({
t: (key: string, options?: { ns?: string }) => {
const fullKey = options?.ns ? `${options.ns}.${key}` : key
return fullKey
},
}),
}))
// Mock useGetLanguage context
vi.mock('@/context/i18n', () => ({
useGetLanguage: () => 'en-US',
}))
// Mock useTheme hook
vi.mock('@/hooks/use-theme', () => ({
default: () => ({ theme: 'light' }),
}))
// Mock i18n-config
vi.mock('@/i18n-config', () => ({
renderI18nObject: (obj: Record<string, string>, locale: string) => {
return obj?.[locale] || obj?.['en-US'] || ''
},
}))
// Mock i18n-config/language
vi.mock('@/i18n-config/language', () => ({
getLanguage: (locale: string) => locale || 'en-US',
}))
// Mock useCategories hook
const mockCategoriesMap: Record<string, { label: string }> = {
'tool': { label: 'Tool' },
'model': { label: 'Model' },
'extension': { label: 'Extension' },
'agent-strategy': { label: 'Agent' },
'datasource': { label: 'Datasource' },
'trigger': { label: 'Trigger' },
'bundle': { label: 'Bundle' },
}
vi.mock('../hooks', () => ({
useCategories: () => ({
categoriesMap: mockCategoriesMap,
}),
}))
// Mock formatNumber utility
vi.mock('@/utils/format', () => ({
formatNumber: (num: number) => num.toLocaleString(),
}))
// Mock shouldUseMcpIcon utility
vi.mock('@/utils/mcp', () => ({
shouldUseMcpIcon: (src: unknown) =>
typeof src === 'object'
&& src !== null
&& (src as { content?: string })?.content === '🔗',
}))
// Mock AppIcon component
vi.mock('@/app/components/base/app-icon', () => ({
default: ({
icon,
background,
innerIcon,
size,
iconType,
}: {
icon?: string
background?: string
innerIcon?: React.ReactNode
size?: string
iconType?: string
}) => (
<div
data-testid="app-icon"
data-icon={icon}
data-background={background}
data-size={size}
data-icon-type={iconType}
>
{innerIcon && <div data-testid="inner-icon">{innerIcon}</div>}
</div>
),
}))
// Mock Mcp icon component
vi.mock('@/app/components/base/icons/src/vender/other', () => ({
Mcp: ({ className }: { className?: string }) => (
<div data-testid="mcp-icon" className={className}>
MCP
</div>
),
Group: ({ className }: { className?: string }) => (
<div data-testid="group-icon" className={className}>
Group
</div>
),
}))
// Mock LeftCorner icon component
vi.mock('../../base/icons/src/vender/plugin', () => ({
LeftCorner: ({ className }: { className?: string }) => (
<div data-testid="left-corner" className={className}>
LeftCorner
</div>
),
}))
// Mock Partner badge
vi.mock('../base/badges/partner', () => ({
default: ({ className, text }: { className?: string, text?: string }) => (
<div data-testid="partner-badge" className={className} title={text}>
Partner
</div>
),
}))
// Mock Verified badge
vi.mock('../base/badges/verified', () => ({
default: ({ className, text }: { className?: string, text?: string }) => (
<div data-testid="verified-badge" className={className} title={text}>
Verified
</div>
),
}))
// Mock Remix icons
vi.mock('@remixicon/react', () => ({
RiCheckLine: ({ className }: { className?: string }) => (
<span data-testid="ri-check-line" className={className}>
</span>
),
RiCloseLine: ({ className }: { className?: string }) => (
<span data-testid="ri-close-line" className={className}>
</span>
),
RiInstallLine: ({ className }: { className?: string }) => (
<span data-testid="ri-install-line" className={className}>
</span>
),
RiAlertFill: ({ className }: { className?: string }) => (
<span data-testid="ri-alert-fill" className={className}>
</span>
),
RiLoader2Line: ({ className }: { className?: string }) => (
<span data-testid="ri-loader-line" className={className}>
</span>
),
}))
// Mock Skeleton components
vi.mock('@/app/components/base/skeleton', () => ({
SkeletonContainer: ({ children }: { children: React.ReactNode }) => (
<div data-testid="skeleton-container">{children}</div>
),
SkeletonPoint: () => <div data-testid="skeleton-point" />,
SkeletonRectangle: ({ className }: { className?: string }) => (
<div data-testid="skeleton-rectangle" className={className} />
),
SkeletonRow: ({
children,
className,
}: {
children: React.ReactNode
className?: string
}) => (
<div data-testid="skeleton-row" className={className}>
{children}
</div>
),
}))
// ================================
// Test Data Factories
// ================================
const createMockPlugin = (overrides?: Partial<Plugin>): Plugin => ({
type: 'plugin',
org: 'test-org',
name: 'test-plugin',
plugin_id: 'plugin-123',
version: '1.0.0',
latest_version: '1.0.0',
latest_package_identifier: 'test-org/test-plugin:1.0.0',
icon: '/test-icon.png',
verified: false,
label: { 'en-US': 'Test Plugin' },
brief: { 'en-US': 'Test plugin description' },
description: { 'en-US': 'Full test plugin description' },
introduction: 'Test plugin introduction',
repository: 'https://github.com/test/plugin',
category: PluginCategoryEnum.tool,
install_count: 1000,
endpoint: { settings: [] },
tags: [{ name: 'search' }],
badges: [],
verification: { authorized_category: 'community' },
from: 'marketplace',
...overrides,
})
type MockMutation = {
isSuccess: boolean
isPending: boolean
}
const createMockMutation = (
overrides?: Partial<MockMutation>,
): MockMutation => ({
isSuccess: false,
isPending: false,
...overrides,
})
type PluginMutationModalProps = {
plugin: Plugin
onCancel: () => void
mutation: MockMutation
mutate: () => void
confirmButtonText: React.ReactNode
cancelButtonText: React.ReactNode
modelTitle: React.ReactNode
description: React.ReactNode
cardTitleLeft: React.ReactNode
modalBottomLeft?: React.ReactNode
}
const createDefaultProps = (
overrides?: Partial<PluginMutationModalProps>,
): PluginMutationModalProps => ({
plugin: createMockPlugin(),
onCancel: vi.fn(),
mutation: createMockMutation(),
mutate: vi.fn(),
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
modelTitle: 'Modal Title',
description: 'Modal Description',
cardTitleLeft: null,
...overrides,
})
// ================================
// PluginMutationModal Component Tests
// ================================
describe('PluginMutationModal', () => {
beforeEach(() => {
vi.clearAllMocks()
})
// ================================
// Rendering Tests
// ================================
describe('Rendering', () => {
it('should render without crashing', () => {
const props = createDefaultProps()
render(<PluginMutationModal {...props} />)
expect(document.body).toBeInTheDocument()
})
it('should render modal title', () => {
const props = createDefaultProps({
modelTitle: 'Update Plugin',
})
render(<PluginMutationModal {...props} />)
expect(screen.getByText('Update Plugin')).toBeInTheDocument()
})
it('should render description', () => {
const props = createDefaultProps({
description: 'Are you sure you want to update this plugin?',
})
render(<PluginMutationModal {...props} />)
expect(
screen.getByText('Are you sure you want to update this plugin?'),
).toBeInTheDocument()
})
it('should render plugin card with plugin info', () => {
const plugin = createMockPlugin({
label: { 'en-US': 'My Test Plugin' },
brief: { 'en-US': 'A test plugin' },
})
const props = createDefaultProps({ plugin })
render(<PluginMutationModal {...props} />)
expect(screen.getByText('My Test Plugin')).toBeInTheDocument()
expect(screen.getByText('A test plugin')).toBeInTheDocument()
})
it('should render confirm button', () => {
const props = createDefaultProps({
confirmButtonText: 'Install Now',
})
render(<PluginMutationModal {...props} />)
expect(
screen.getByRole('button', { name: /Install Now/i }),
).toBeInTheDocument()
})
it('should render cancel button when not pending', () => {
const props = createDefaultProps({
cancelButtonText: 'Cancel Installation',
mutation: createMockMutation({ isPending: false }),
})
render(<PluginMutationModal {...props} />)
expect(
screen.getByRole('button', { name: /Cancel Installation/i }),
).toBeInTheDocument()
})
it('should render modal with closable prop', () => {
const props = createDefaultProps()
render(<PluginMutationModal {...props} />)
// The modal should have a close button
expect(screen.getByTestId('ri-close-line')).toBeInTheDocument()
})
})
// ================================
// Props Testing
// ================================
describe('Props', () => {
it('should render cardTitleLeft when provided', () => {
const props = createDefaultProps({
cardTitleLeft: <span data-testid="version-badge">v2.0.0</span>,
})
render(<PluginMutationModal {...props} />)
expect(screen.getByTestId('version-badge')).toBeInTheDocument()
})
it('should render modalBottomLeft when provided', () => {
const props = createDefaultProps({
modalBottomLeft: (
<span data-testid="bottom-left-content">Additional Info</span>
),
})
render(<PluginMutationModal {...props} />)
expect(screen.getByTestId('bottom-left-content')).toBeInTheDocument()
})
it('should not render modalBottomLeft when not provided', () => {
const props = createDefaultProps({
modalBottomLeft: undefined,
})
render(<PluginMutationModal {...props} />)
expect(
screen.queryByTestId('bottom-left-content'),
).not.toBeInTheDocument()
})
it('should render custom ReactNode for modelTitle', () => {
const props = createDefaultProps({
modelTitle: <div data-testid="custom-title">Custom Title Node</div>,
})
render(<PluginMutationModal {...props} />)
expect(screen.getByTestId('custom-title')).toBeInTheDocument()
})
it('should render custom ReactNode for description', () => {
const props = createDefaultProps({
description: (
<div data-testid="custom-description">
<strong>Warning:</strong>
{' '}
This action is irreversible.
</div>
),
})
render(<PluginMutationModal {...props} />)
expect(screen.getByTestId('custom-description')).toBeInTheDocument()
})
it('should render custom ReactNode for confirmButtonText', () => {
const props = createDefaultProps({
confirmButtonText: (
<span>
<span data-testid="confirm-icon"></span>
{' '}
Confirm Action
</span>
),
})
render(<PluginMutationModal {...props} />)
expect(screen.getByTestId('confirm-icon')).toBeInTheDocument()
})
it('should render custom ReactNode for cancelButtonText', () => {
const props = createDefaultProps({
cancelButtonText: (
<span>
<span data-testid="cancel-icon"></span>
{' '}
Abort
</span>
),
})
render(<PluginMutationModal {...props} />)
expect(screen.getByTestId('cancel-icon')).toBeInTheDocument()
})
})
// ================================
// User Interactions
// ================================
describe('User Interactions', () => {
it('should call onCancel when cancel button is clicked', () => {
const onCancel = vi.fn()
const props = createDefaultProps({ onCancel })
render(<PluginMutationModal {...props} />)
const cancelButton = screen.getByRole('button', { name: /Cancel/i })
fireEvent.click(cancelButton)
expect(onCancel).toHaveBeenCalledTimes(1)
})
it('should call mutate when confirm button is clicked', () => {
const mutate = vi.fn()
const props = createDefaultProps({ mutate })
render(<PluginMutationModal {...props} />)
const confirmButton = screen.getByRole('button', { name: /Confirm/i })
fireEvent.click(confirmButton)
expect(mutate).toHaveBeenCalledTimes(1)
})
it('should render close button in modal header', () => {
const props = createDefaultProps()
render(<PluginMutationModal {...props} />)
// Find the close icon - the Modal component handles the onClose callback
const closeIcon = screen.getByTestId('ri-close-line')
expect(closeIcon).toBeInTheDocument()
})
it('should not call mutate when button is disabled during pending', () => {
const mutate = vi.fn()
const props = createDefaultProps({
mutate,
mutation: createMockMutation({ isPending: true }),
})
render(<PluginMutationModal {...props} />)
const confirmButton = screen.getByRole('button', { name: /Confirm/i })
expect(confirmButton).toBeDisabled()
fireEvent.click(confirmButton)
// Button is disabled, so mutate might still be called depending on implementation
// The important thing is the button has disabled attribute
expect(confirmButton).toHaveAttribute('disabled')
})
})
// ================================
// Mutation State Tests
// ================================
describe('Mutation States', () => {
describe('when isPending is true', () => {
it('should hide cancel button', () => {
const props = createDefaultProps({
mutation: createMockMutation({ isPending: true }),
})
render(<PluginMutationModal {...props} />)
expect(
screen.queryByRole('button', { name: /Cancel/i }),
).not.toBeInTheDocument()
})
it('should show loading state on confirm button', () => {
const props = createDefaultProps({
mutation: createMockMutation({ isPending: true }),
})
render(<PluginMutationModal {...props} />)
const confirmButton = screen.getByRole('button', { name: /Confirm/i })
expect(confirmButton).toBeDisabled()
})
it('should disable confirm button', () => {
const props = createDefaultProps({
mutation: createMockMutation({ isPending: true }),
})
render(<PluginMutationModal {...props} />)
const confirmButton = screen.getByRole('button', { name: /Confirm/i })
expect(confirmButton).toBeDisabled()
})
})
describe('when isPending is false', () => {
it('should show cancel button', () => {
const props = createDefaultProps({
mutation: createMockMutation({ isPending: false }),
})
render(<PluginMutationModal {...props} />)
expect(
screen.getByRole('button', { name: /Cancel/i }),
).toBeInTheDocument()
})
it('should enable confirm button', () => {
const props = createDefaultProps({
mutation: createMockMutation({ isPending: false }),
})
render(<PluginMutationModal {...props} />)
const confirmButton = screen.getByRole('button', { name: /Confirm/i })
expect(confirmButton).not.toBeDisabled()
})
})
describe('when isSuccess is true', () => {
it('should show installed state on card', () => {
const props = createDefaultProps({
mutation: createMockMutation({ isSuccess: true }),
})
render(<PluginMutationModal {...props} />)
// The Card component should receive installed=true
// This will show a check icon
expect(screen.getByTestId('ri-check-line')).toBeInTheDocument()
})
})
describe('when isSuccess is false', () => {
it('should not show installed state on card', () => {
const props = createDefaultProps({
mutation: createMockMutation({ isSuccess: false }),
})
render(<PluginMutationModal {...props} />)
// The check icon should not be present (installed=false)
expect(screen.queryByTestId('ri-check-line')).not.toBeInTheDocument()
})
})
describe('state combinations', () => {
it('should handle isPending=true and isSuccess=false', () => {
const props = createDefaultProps({
mutation: createMockMutation({ isPending: true, isSuccess: false }),
})
render(<PluginMutationModal {...props} />)
expect(
screen.queryByRole('button', { name: /Cancel/i }),
).not.toBeInTheDocument()
expect(screen.queryByTestId('ri-check-line')).not.toBeInTheDocument()
})
it('should handle isPending=false and isSuccess=true', () => {
const props = createDefaultProps({
mutation: createMockMutation({ isPending: false, isSuccess: true }),
})
render(<PluginMutationModal {...props} />)
expect(
screen.getByRole('button', { name: /Cancel/i }),
).toBeInTheDocument()
expect(screen.getByTestId('ri-check-line')).toBeInTheDocument()
})
it('should handle both isPending=true and isSuccess=true', () => {
const props = createDefaultProps({
mutation: createMockMutation({ isPending: true, isSuccess: true }),
})
render(<PluginMutationModal {...props} />)
expect(
screen.queryByRole('button', { name: /Cancel/i }),
).not.toBeInTheDocument()
expect(screen.getByTestId('ri-check-line')).toBeInTheDocument()
})
})
})
// ================================
// Plugin Card Integration Tests
// ================================
describe('Plugin Card Integration', () => {
it('should display plugin label', () => {
const plugin = createMockPlugin({
label: { 'en-US': 'Amazing Plugin' },
})
const props = createDefaultProps({ plugin })
render(<PluginMutationModal {...props} />)
expect(screen.getByText('Amazing Plugin')).toBeInTheDocument()
})
it('should display plugin brief description', () => {
const plugin = createMockPlugin({
brief: { 'en-US': 'This is an amazing plugin' },
})
const props = createDefaultProps({ plugin })
render(<PluginMutationModal {...props} />)
expect(screen.getByText('This is an amazing plugin')).toBeInTheDocument()
})
it('should display plugin org and name', () => {
const plugin = createMockPlugin({
org: 'my-organization',
name: 'my-plugin-name',
})
const props = createDefaultProps({ plugin })
render(<PluginMutationModal {...props} />)
expect(screen.getByText('my-organization')).toBeInTheDocument()
expect(screen.getByText('my-plugin-name')).toBeInTheDocument()
})
it('should display plugin category', () => {
const plugin = createMockPlugin({
category: PluginCategoryEnum.model,
})
const props = createDefaultProps({ plugin })
render(<PluginMutationModal {...props} />)
expect(screen.getByText('Model')).toBeInTheDocument()
})
it('should display verified badge when plugin is verified', () => {
const plugin = createMockPlugin({
verified: true,
})
const props = createDefaultProps({ plugin })
render(<PluginMutationModal {...props} />)
expect(screen.getByTestId('verified-badge')).toBeInTheDocument()
})
it('should display partner badge when plugin has partner badge', () => {
const plugin = createMockPlugin({
badges: ['partner'],
})
const props = createDefaultProps({ plugin })
render(<PluginMutationModal {...props} />)
expect(screen.getByTestId('partner-badge')).toBeInTheDocument()
})
})
// ================================
// Memoization Tests
// ================================
describe('Memoization', () => {
it('should be memoized with React.memo', () => {
// Verify the component is wrapped with memo
expect(PluginMutationModal).toBeDefined()
expect(typeof PluginMutationModal).toBe('object')
})
it('should have displayName set', () => {
// The component sets displayName = 'PluginMutationModal'
const displayName
= (PluginMutationModal as any).type?.displayName
|| (PluginMutationModal as any).displayName
expect(displayName).toBe('PluginMutationModal')
})
it('should not re-render when props unchanged', () => {
const renderCount = vi.fn()
const TestWrapper = ({ props }: { props: PluginMutationModalProps }) => {
renderCount()
return <PluginMutationModal {...props} />
}
const props = createDefaultProps()
const { rerender } = render(<TestWrapper props={props} />)
expect(renderCount).toHaveBeenCalledTimes(1)
// Re-render with same props reference
rerender(<TestWrapper props={props} />)
expect(renderCount).toHaveBeenCalledTimes(2)
})
})
// ================================
// Edge Cases Tests
// ================================
describe('Edge Cases', () => {
it('should handle empty label object', () => {
const plugin = createMockPlugin({
label: {},
})
const props = createDefaultProps({ plugin })
render(<PluginMutationModal {...props} />)
expect(document.body).toBeInTheDocument()
})
it('should handle empty brief object', () => {
const plugin = createMockPlugin({
brief: {},
})
const props = createDefaultProps({ plugin })
render(<PluginMutationModal {...props} />)
expect(document.body).toBeInTheDocument()
})
it('should handle plugin with undefined badges', () => {
const plugin = createMockPlugin()
// @ts-expect-error - Testing undefined badges
plugin.badges = undefined
const props = createDefaultProps({ plugin })
render(<PluginMutationModal {...props} />)
expect(document.body).toBeInTheDocument()
})
it('should handle empty string description', () => {
const props = createDefaultProps({
description: '',
})
render(<PluginMutationModal {...props} />)
expect(document.body).toBeInTheDocument()
})
it('should handle empty string modelTitle', () => {
const props = createDefaultProps({
modelTitle: '',
})
render(<PluginMutationModal {...props} />)
expect(document.body).toBeInTheDocument()
})
it('should handle special characters in plugin name', () => {
const plugin = createMockPlugin({
name: 'plugin-with-special<chars>!@#$%',
org: 'org<script>test</script>',
})
const props = createDefaultProps({ plugin })
render(<PluginMutationModal {...props} />)
expect(screen.getByText('plugin-with-special<chars>!@#$%')).toBeInTheDocument()
})
it('should handle very long title', () => {
const longTitle = 'A'.repeat(500)
const plugin = createMockPlugin({
label: { 'en-US': longTitle },
})
const props = createDefaultProps({ plugin })
render(<PluginMutationModal {...props} />)
// Should render the long title text
expect(screen.getByText(longTitle)).toBeInTheDocument()
})
it('should handle very long description', () => {
const longDescription = 'B'.repeat(1000)
const plugin = createMockPlugin({
brief: { 'en-US': longDescription },
})
const props = createDefaultProps({ plugin })
render(<PluginMutationModal {...props} />)
// Should render the long description text
expect(screen.getByText(longDescription)).toBeInTheDocument()
})
it('should handle unicode characters in title', () => {
const props = createDefaultProps({
modelTitle: '更新插件 🎉',
})
render(<PluginMutationModal {...props} />)
expect(screen.getByText('更新插件 🎉')).toBeInTheDocument()
})
it('should handle unicode characters in description', () => {
const props = createDefaultProps({
description: '确定要更新这个插件吗?この操作は元に戻せません。',
})
render(<PluginMutationModal {...props} />)
expect(
screen.getByText('确定要更新这个插件吗?この操作は元に戻せません。'),
).toBeInTheDocument()
})
it('should handle null cardTitleLeft', () => {
const props = createDefaultProps({
cardTitleLeft: null,
})
render(<PluginMutationModal {...props} />)
expect(document.body).toBeInTheDocument()
})
it('should handle undefined modalBottomLeft', () => {
const props = createDefaultProps({
modalBottomLeft: undefined,
})
render(<PluginMutationModal {...props} />)
expect(document.body).toBeInTheDocument()
})
})
// ================================
// Modal Behavior Tests
// ================================
describe('Modal Behavior', () => {
it('should render modal with isShow=true', () => {
const props = createDefaultProps()
render(<PluginMutationModal {...props} />)
// Modal should be visible - check for dialog role using screen query
expect(screen.getByRole('dialog')).toBeInTheDocument()
})
it('should have modal structure', () => {
const props = createDefaultProps()
render(<PluginMutationModal {...props} />)
// Check that modal content is rendered
expect(screen.getByRole('dialog')).toBeInTheDocument()
// Modal should have title
expect(screen.getByText('Modal Title')).toBeInTheDocument()
})
it('should render modal as closable', () => {
const props = createDefaultProps()
render(<PluginMutationModal {...props} />)
// Close icon should be present
expect(screen.getByTestId('ri-close-line')).toBeInTheDocument()
})
})
// ================================
// Button Styling Tests
// ================================
describe('Button Styling', () => {
it('should render confirm button with primary variant', () => {
const props = createDefaultProps()
render(<PluginMutationModal {...props} />)
const confirmButton = screen.getByRole('button', { name: /Confirm/i })
// Button component with variant="primary" should have primary styling
expect(confirmButton).toBeInTheDocument()
})
it('should render cancel button with default variant', () => {
const props = createDefaultProps()
render(<PluginMutationModal {...props} />)
const cancelButton = screen.getByRole('button', { name: /Cancel/i })
expect(cancelButton).toBeInTheDocument()
})
})
// ================================
// Layout Tests
// ================================
describe('Layout', () => {
it('should render description text', () => {
const props = createDefaultProps({
description: 'Test Description Content',
})
render(<PluginMutationModal {...props} />)
// Description should be rendered
expect(screen.getByText('Test Description Content')).toBeInTheDocument()
})
it('should render card with plugin info', () => {
const plugin = createMockPlugin({
label: { 'en-US': 'Layout Test Plugin' },
})
const props = createDefaultProps({ plugin })
render(<PluginMutationModal {...props} />)
// Card should display plugin info
expect(screen.getByText('Layout Test Plugin')).toBeInTheDocument()
})
it('should render both cancel and confirm buttons', () => {
const props = createDefaultProps()
render(<PluginMutationModal {...props} />)
// Both buttons should be rendered
expect(screen.getByRole('button', { name: /Cancel/i })).toBeInTheDocument()
expect(screen.getByRole('button', { name: /Confirm/i })).toBeInTheDocument()
})
it('should render buttons in correct order', () => {
const props = createDefaultProps()
render(<PluginMutationModal {...props} />)
// Get all buttons and verify order
const buttons = screen.getAllByRole('button')
// Cancel button should come before Confirm button
const cancelIndex = buttons.findIndex(b => b.textContent?.includes('Cancel'))
const confirmIndex = buttons.findIndex(b => b.textContent?.includes('Confirm'))
expect(cancelIndex).toBeLessThan(confirmIndex)
})
})
// ================================
// Accessibility Tests
// ================================
describe('Accessibility', () => {
it('should have accessible dialog role', () => {
const props = createDefaultProps()
render(<PluginMutationModal {...props} />)
expect(screen.getByRole('dialog')).toBeInTheDocument()
})
it('should have accessible button roles', () => {
const props = createDefaultProps()
render(<PluginMutationModal {...props} />)
expect(screen.getAllByRole('button').length).toBeGreaterThan(0)
})
it('should have accessible text content', () => {
const props = createDefaultProps({
modelTitle: 'Accessible Title',
description: 'Accessible Description',
})
render(<PluginMutationModal {...props} />)
expect(screen.getByText('Accessible Title')).toBeInTheDocument()
expect(screen.getByText('Accessible Description')).toBeInTheDocument()
})
})
// ================================
// All Plugin Categories Tests
// ================================
describe('All Plugin Categories', () => {
const categories = [
{ category: PluginCategoryEnum.tool, label: 'Tool' },
{ category: PluginCategoryEnum.model, label: 'Model' },
{ category: PluginCategoryEnum.extension, label: 'Extension' },
{ category: PluginCategoryEnum.agent, label: 'Agent' },
{ category: PluginCategoryEnum.datasource, label: 'Datasource' },
{ category: PluginCategoryEnum.trigger, label: 'Trigger' },
]
categories.forEach(({ category, label }) => {
it(`should display ${label} category correctly`, () => {
const plugin = createMockPlugin({ category })
const props = createDefaultProps({ plugin })
render(<PluginMutationModal {...props} />)
expect(screen.getByText(label)).toBeInTheDocument()
})
})
})
// ================================
// Bundle Type Tests
// ================================
describe('Bundle Type', () => {
it('should display bundle label for bundle type plugin', () => {
const plugin = createMockPlugin({
type: 'bundle',
category: PluginCategoryEnum.tool,
})
const props = createDefaultProps({ plugin })
render(<PluginMutationModal {...props} />)
// For bundle type, should show 'Bundle' instead of category
expect(screen.getByText('Bundle')).toBeInTheDocument()
})
})
// ================================
// Event Handler Isolation Tests
// ================================
describe('Event Handler Isolation', () => {
it('should not call mutate when clicking cancel button', () => {
const mutate = vi.fn()
const onCancel = vi.fn()
const props = createDefaultProps({ mutate, onCancel })
render(<PluginMutationModal {...props} />)
const cancelButton = screen.getByRole('button', { name: /Cancel/i })
fireEvent.click(cancelButton)
expect(onCancel).toHaveBeenCalledTimes(1)
expect(mutate).not.toHaveBeenCalled()
})
it('should not call onCancel when clicking confirm button', () => {
const mutate = vi.fn()
const onCancel = vi.fn()
const props = createDefaultProps({ mutate, onCancel })
render(<PluginMutationModal {...props} />)
const confirmButton = screen.getByRole('button', { name: /Confirm/i })
fireEvent.click(confirmButton)
expect(mutate).toHaveBeenCalledTimes(1)
expect(onCancel).not.toHaveBeenCalled()
})
})
// ================================
// Multiple Renders Tests
// ================================
describe('Multiple Renders', () => {
it('should handle rapid state changes', () => {
const props = createDefaultProps()
const { rerender } = render(<PluginMutationModal {...props} />)
// Simulate rapid pending state changes
rerender(
<PluginMutationModal
{...props}
mutation={createMockMutation({ isPending: true })}
/>,
)
rerender(
<PluginMutationModal
{...props}
mutation={createMockMutation({ isPending: false })}
/>,
)
rerender(
<PluginMutationModal
{...props}
mutation={createMockMutation({ isSuccess: true })}
/>,
)
// Should show success state
expect(screen.getByTestId('ri-check-line')).toBeInTheDocument()
})
it('should handle plugin prop changes', () => {
const plugin1 = createMockPlugin({ label: { 'en-US': 'Plugin One' } })
const plugin2 = createMockPlugin({ label: { 'en-US': 'Plugin Two' } })
const props = createDefaultProps({ plugin: plugin1 })
const { rerender } = render(<PluginMutationModal {...props} />)
expect(screen.getByText('Plugin One')).toBeInTheDocument()
rerender(<PluginMutationModal {...props} plugin={plugin2} />)
expect(screen.getByText('Plugin Two')).toBeInTheDocument()
})
})
})