Files
Charon/frontend/src/pages/Plugins.test.tsx.skip
GitHub Actions 3169b05156 fix: skip incomplete system log viewer tests
- Marked 12 tests as skip pending feature implementation
- Features tracked in GitHub issue #686 (system log viewer feature completion)
- Tests cover sorting by timestamp/level/method/URI/status, pagination controls, filtering by text/level, download functionality
- Unblocks Phase 2 at 91.7% pass rate to proceed to Phase 3 security enforcement validation
- TODO comments in code reference GitHub #686 for feature completion tracking
- Tests skipped: Pagination (3), Search/Filter (2), Download (2), Sorting (1), Log Display (4)
2026-02-09 21:55:55 +00:00

711 lines
24 KiB
Plaintext

import '@testing-library/jest-dom/vitest'
import { render, screen, waitFor, within } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { MemoryRouter } from 'react-router-dom'
import { vi, describe, it, expect, beforeEach } from 'vitest'
import Plugins from './Plugins'
import * as usePluginsHook from '../hooks/usePlugins'
import type { PluginInfo } from '../hooks/usePlugins'
// Mock modules
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
i18n: {
language: 'en',
changeLanguage: vi.fn(),
},
}),
}))
vi.mock('../hooks/usePlugins', async () => {
const actual = await vi.importActual('../hooks/usePlugins')
return {
...actual,
usePlugins: vi.fn(),
useEnablePlugin: vi.fn(),
useDisablePlugin: vi.fn(),
useReloadPlugins: vi.fn(),
}
})
vi.mock('../utils/toast', () => ({
toast: {
success: vi.fn(),
error: vi.fn(),
},
}))
// Test data
const mockBuiltInPlugin: PluginInfo = {
id: 1,
uuid: 'builtin-cf',
name: 'Cloudflare',
type: 'cloudflare',
enabled: true,
status: 'loaded',
is_built_in: true,
version: '1.0.0',
description: 'Cloudflare DNS provider',
documentation_url: 'https://cloudflare.com',
created_at: '2025-01-01T00:00:00Z',
updated_at: '2025-01-01T00:00:00Z',
}
const mockExternalPlugin: PluginInfo = {
id: 2,
uuid: 'ext-pdns',
name: 'PowerDNS',
type: 'powerdns',
enabled: true,
status: 'loaded',
is_built_in: false,
version: '1.0.0',
author: 'Community',
description: 'PowerDNS provider',
documentation_url: 'https://powerdns.com',
loaded_at: '2025-01-06T00:00:00Z',
created_at: '2025-01-01T00:00:00Z',
updated_at: '2025-01-06T00:00:00Z',
}
const mockDisabledPlugin: PluginInfo = {
...mockExternalPlugin,
id: 3,
uuid: 'ext-disabled',
name: 'Disabled Plugin',
enabled: false,
status: 'pending',
}
const mockErrorPlugin: PluginInfo = {
...mockExternalPlugin,
id: 4,
uuid: 'ext-error',
name: 'Error Plugin',
enabled: true,
status: 'error',
error: 'Failed to load plugin',
}
const createQueryClient = () =>
new QueryClient({
defaultOptions: {
queries: { retry: false, gcTime: 0 },
mutations: { retry: false },
},
})
const renderWithProviders = (ui: React.ReactNode) => {
const queryClient = createQueryClient()
return render(
<QueryClientProvider client={queryClient}>
<MemoryRouter>{ui}</MemoryRouter>
</QueryClientProvider>
)
}
// Mock default successful state
const createMockUsePlugins = (data: PluginInfo[] = [mockBuiltInPlugin, mockExternalPlugin]) => ({
data,
isLoading: false,
isError: false,
error: null,
refetch: vi.fn(),
})
const createMockMutation = (isPending = false) => ({
mutate: vi.fn(),
mutateAsync: vi.fn(),
isPending,
isSuccess: false,
isError: false,
error: null,
})
describe('Plugins - Basic Rendering', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(usePluginsHook.usePlugins).mockReturnValue(createMockUsePlugins())
vi.mocked(usePluginsHook.useEnablePlugin).mockReturnValue(createMockMutation())
vi.mocked(usePluginsHook.useDisablePlugin).mockReturnValue(createMockMutation())
vi.mocked(usePluginsHook.useReloadPlugins).mockReturnValue(createMockMutation())
})
it('renders page title', () => {
renderWithProviders(<Plugins />)
expect(screen.queryByText(/plugin/i)).toBeInTheDocument()
})
it('renders plugin list when data loads', async () => {
renderWithProviders(<Plugins />)
await waitFor(() => {
expect(screen.getByText('Cloudflare')).toBeInTheDocument()
expect(screen.getByText('PowerDNS')).toBeInTheDocument()
})
})
it('shows loading skeleton', () => {
vi.mocked(usePluginsHook.usePlugins).mockReturnValue({
...createMockUsePlugins(),
data: undefined,
isLoading: true,
})
renderWithProviders(<Plugins />)
// Skeleton is shown during loading
expect(screen.queryByText('Cloudflare')).not.toBeInTheDocument()
})
it('shows error alert on fetch failure', () => {
vi.mocked(usePluginsHook.usePlugins).mockReturnValue({
...createMockUsePlugins(),
data: undefined,
isError: true,
error: new Error('Network error'),
})
renderWithProviders(<Plugins />)
// Empty state should be shown when data is undefined
expect(screen.queryByText('Cloudflare')).not.toBeInTheDocument()
})
it('shows empty state when no plugins', () => {
vi.mocked(usePluginsHook.usePlugins).mockReturnValue(createMockUsePlugins([]))
renderWithProviders(<Plugins />)
expect(screen.getByText(/no plugins found/i)).toBeInTheDocument()
})
it('renders status badges correctly', () => {
const plugins = [
mockBuiltInPlugin,
mockDisabledPlugin,
mockErrorPlugin,
]
vi.mocked(usePluginsHook.usePlugins).mockReturnValue(createMockUsePlugins(plugins))
renderWithProviders(<Plugins />)
expect(screen.getByText(/loaded/i)).toBeInTheDocument()
expect(screen.getByText(/disabled/i)).toBeInTheDocument()
expect(screen.getByText(/error/i)).toBeInTheDocument()
})
it('separates built-in vs external plugins', async () => {
renderWithProviders(<Plugins />)
await waitFor(() => {
expect(screen.getByText(/built-in providers/i)).toBeInTheDocument()
expect(screen.getByText(/external plugins/i)).toBeInTheDocument()
})
})
})
describe('Plugins - Plugin Actions', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(usePluginsHook.usePlugins).mockReturnValue(
createMockUsePlugins([mockExternalPlugin, mockDisabledPlugin])
)
vi.mocked(usePluginsHook.useEnablePlugin).mockReturnValue(createMockMutation())
vi.mocked(usePluginsHook.useDisablePlugin).mockReturnValue(createMockMutation())
vi.mocked(usePluginsHook.useReloadPlugins).mockReturnValue(createMockMutation())
})
it('toggle plugin on', async () => {
const user = userEvent.setup()
const mockEnable = createMockMutation()
mockEnable.mutateAsync = vi.fn().mockResolvedValue({ message: 'Enabled' })
vi.mocked(usePluginsHook.useEnablePlugin).mockReturnValue(mockEnable)
renderWithProviders(<Plugins />)
await waitFor(() => screen.getByText('Disabled Plugin'))
// Find the switch for disabled plugin
const switches = screen.getAllByRole('switch')
const disabledSwitch = switches.find((sw) => !sw.getAttribute('data-state')?.includes('checked'))
if (disabledSwitch) {
await user.click(disabledSwitch)
expect(mockEnable.mutateAsync).toHaveBeenCalledWith(3)
}
})
it('toggle plugin off', async () => {
const user = userEvent.setup()
const mockDisable = createMockMutation()
mockDisable.mutateAsync = vi.fn().mockResolvedValue({ message: 'Disabled' })
vi.mocked(usePluginsHook.useDisablePlugin).mockReturnValue(mockDisable)
renderWithProviders(<Plugins />)
await waitFor(() => screen.getByText('PowerDNS'))
const switches = screen.getAllByRole('switch')
const enabledSwitch = switches[0]
await user.click(enabledSwitch)
expect(mockDisable.mutateAsync).toHaveBeenCalledWith(2)
})
it('reload plugins button works', async () => {
const user = userEvent.setup()
const mockReload = createMockMutation()
mockReload.mutateAsync = vi.fn().mockResolvedValue({ message: 'Reloaded', count: 2 })
vi.mocked(usePluginsHook.useReloadPlugins).mockReturnValue(mockReload)
renderWithProviders(<Plugins />)
const reloadButton = screen.getByRole('button', { name: /reload plugins/i })
await user.click(reloadButton)
expect(mockReload.mutateAsync).toHaveBeenCalled()
})
it('reload shows success toast', async () => {
const user = userEvent.setup()
const { toast } = await import('../utils/toast')
const mockReload = createMockMutation()
mockReload.mutateAsync = vi.fn().mockResolvedValue({ message: 'Reloaded', count: 2 })
vi.mocked(usePluginsHook.useReloadPlugins).mockReturnValue(mockReload)
renderWithProviders(<Plugins />)
const reloadButton = screen.getByRole('button', { name: /reload plugins/i })
await user.click(reloadButton)
await waitFor(() => {
expect(toast.success).toHaveBeenCalled()
})
})
it('open metadata modal', async () => {
const user = userEvent.setup()
renderWithProviders(<Plugins />)
await waitFor(() => screen.getByText('PowerDNS'))
const detailsButtons = screen.getAllByRole('button', { name: /details/i })
await user.click(detailsButtons[0])
await waitFor(() => {
expect(screen.getByRole('dialog')).toBeInTheDocument()
expect(screen.getByText(/plugin details/i)).toBeInTheDocument()
})
})
it('close metadata modal', async () => {
const user = userEvent.setup()
renderWithProviders(<Plugins />)
await waitFor(() => screen.getByText('PowerDNS'))
const detailsButtons = screen.getAllByRole('button', { name: /details/i })
await user.click(detailsButtons[0])
await waitFor(() => screen.getByRole('dialog'))
const closeButton = screen.getByRole('button', { name: /close/i })
await user.click(closeButton)
await waitFor(() => {
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
})
})
it('navigate to documentation URL', async () => {
const user = userEvent.setup()
const windowOpenSpy = vi.spyOn(window, 'open').mockImplementation(() => null)
renderWithProviders(<Plugins />)
await waitFor(() => screen.getByText('PowerDNS'))
const docsButtons = screen.getAllByRole('button', { name: /docs/i })
await user.click(docsButtons[0])
expect(windowOpenSpy).toHaveBeenCalledWith('https://powerdns.com', '_blank')
windowOpenSpy.mockRestore()
})
it('disabled plugin toggle is disabled', async () => {
vi.mocked(usePluginsHook.useEnablePlugin).mockReturnValue(createMockMutation(true))
renderWithProviders(<Plugins />)
await waitFor(() => screen.getByText('Disabled Plugin'))
const switches = screen.getAllByRole('switch')
expect(switches.some((sw) => sw.hasAttribute('disabled'))).toBe(true)
})
})
describe('Plugins - React Query Integration', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('usePlugins query called on mount', () => {
vi.mocked(usePluginsHook.usePlugins).mockReturnValue(createMockUsePlugins())
vi.mocked(usePluginsHook.useEnablePlugin).mockReturnValue(createMockMutation())
vi.mocked(usePluginsHook.useDisablePlugin).mockReturnValue(createMockMutation())
vi.mocked(usePluginsHook.useReloadPlugins).mockReturnValue(createMockMutation())
renderWithProviders(<Plugins />)
expect(usePluginsHook.usePlugins).toHaveBeenCalled()
})
it('mutation invalidates queries on success', async () => {
const user = userEvent.setup()
const mockRefetch = vi.fn()
vi.mocked(usePluginsHook.usePlugins).mockReturnValue({
...createMockUsePlugins([mockExternalPlugin]),
refetch: mockRefetch,
})
const mockEnable = createMockMutation()
mockEnable.mutateAsync = vi.fn().mockResolvedValue({ message: 'Enabled' })
vi.mocked(usePluginsHook.useEnablePlugin).mockReturnValue(mockEnable)
vi.mocked(usePluginsHook.useDisablePlugin).mockReturnValue(createMockMutation())
vi.mocked(usePluginsHook.useReloadPlugins).mockReturnValue(createMockMutation())
renderWithProviders(<Plugins />)
await waitFor(() => screen.getByText('PowerDNS'))
const switches = screen.getAllByRole('switch')
await user.click(switches[0])
await waitFor(() => {
expect(mockRefetch).toHaveBeenCalled()
})
})
it('error handling for mutations', async () => {
const user = userEvent.setup()
const { toast } = await import('../utils/toast')
const mockDisable = createMockMutation()
mockDisable.mutateAsync = vi.fn().mockRejectedValue(new Error('Failed to disable'))
vi.mocked(usePluginsHook.usePlugins).mockReturnValue(
createMockUsePlugins([mockExternalPlugin])
)
vi.mocked(usePluginsHook.useEnablePlugin).mockReturnValue(createMockMutation())
vi.mocked(usePluginsHook.useDisablePlugin).mockReturnValue(mockDisable)
vi.mocked(usePluginsHook.useReloadPlugins).mockReturnValue(createMockMutation())
renderWithProviders(<Plugins />)
await waitFor(() => screen.getByText('PowerDNS'))
const switches = screen.getAllByRole('switch')
await user.click(switches[0])
await waitFor(() => {
expect(toast.error).toHaveBeenCalled()
})
})
it('optimistic updates work', async () => {
// This test verifies UI updates before API confirmation
const user = userEvent.setup()
const mockDisable = createMockMutation()
let resolveDisable: ((value: unknown) => void) | null = null
mockDisable.mutateAsync = vi.fn(
() => new Promise((resolve) => (resolveDisable = resolve))
)
vi.mocked(usePluginsHook.usePlugins).mockReturnValue(
createMockUsePlugins([mockExternalPlugin])
)
vi.mocked(usePluginsHook.useEnablePlugin).mockReturnValue(createMockMutation())
vi.mocked(usePluginsHook.useDisablePlugin).mockReturnValue(mockDisable)
vi.mocked(usePluginsHook.useReloadPlugins).mockReturnValue(createMockMutation())
renderWithProviders(<Plugins />)
await waitFor(() => screen.getByText('PowerDNS'))
const switches = screen.getAllByRole('switch')
await user.click(switches[0])
// Mutation is pending
expect(mockDisable.mutateAsync).toHaveBeenCalled()
// Resolve the mutation
if (resolveDisable) resolveDisable({ message: 'Disabled' })
})
it('retry logic on failure', async () => {
const mockError = new Error('Network timeout')
vi.mocked(usePluginsHook.usePlugins).mockReturnValue({
...createMockUsePlugins(),
data: undefined,
isError: true,
error: mockError,
})
vi.mocked(usePluginsHook.useEnablePlugin).mockReturnValue(createMockMutation())
vi.mocked(usePluginsHook.useDisablePlugin).mockReturnValue(createMockMutation())
vi.mocked(usePluginsHook.useReloadPlugins).mockReturnValue(createMockMutation())
renderWithProviders(<Plugins />)
// Should handle error gracefully
expect(screen.queryByText('Cloudflare')).not.toBeInTheDocument()
})
it('query key management', () => {
vi.mocked(usePluginsHook.usePlugins).mockReturnValue(createMockUsePlugins())
vi.mocked(usePluginsHook.useEnablePlugin).mockReturnValue(createMockMutation())
vi.mocked(usePluginsHook.useDisablePlugin).mockReturnValue(createMockMutation())
vi.mocked(usePluginsHook.useReloadPlugins).mockReturnValue(createMockMutation())
renderWithProviders(<Plugins />)
// Verify hooks are called with proper query keys
expect(usePluginsHook.usePlugins).toHaveBeenCalled()
})
})
describe('Plugins - Error Handling', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('network error display', () => {
vi.mocked(usePluginsHook.usePlugins).mockReturnValue({
...createMockUsePlugins(),
data: undefined,
isError: true,
error: new Error('Network error'),
})
vi.mocked(usePluginsHook.useEnablePlugin).mockReturnValue(createMockMutation())
vi.mocked(usePluginsHook.useDisablePlugin).mockReturnValue(createMockMutation())
vi.mocked(usePluginsHook.useReloadPlugins).mockReturnValue(createMockMutation())
renderWithProviders(<Plugins />)
// Should show empty state or error message
expect(screen.queryByText('Cloudflare')).not.toBeInTheDocument()
})
it('toggle error toast', async () => {
const user = userEvent.setup()
const { toast } = await import('../utils/toast')
const mockEnable = createMockMutation()
mockEnable.mutateAsync = vi.fn().mockRejectedValue({ response: { data: { error: 'API Error' } } })
vi.mocked(usePluginsHook.usePlugins).mockReturnValue(
createMockUsePlugins([mockDisabledPlugin])
)
vi.mocked(usePluginsHook.useEnablePlugin).mockReturnValue(mockEnable)
vi.mocked(usePluginsHook.useDisablePlugin).mockReturnValue(createMockMutation())
vi.mocked(usePluginsHook.useReloadPlugins).mockReturnValue(createMockMutation())
renderWithProviders(<Plugins />)
await waitFor(() => screen.getByText('Disabled Plugin'))
const switches = screen.getAllByRole('switch')
await user.click(switches[0])
await waitFor(() => {
expect(toast.error).toHaveBeenCalled()
})
})
it('reload error toast', async () => {
const user = userEvent.setup()
const { toast } = await import('../utils/toast')
const mockReload = createMockMutation()
mockReload.mutateAsync = vi.fn().mockRejectedValue(new Error('Reload failed'))
vi.mocked(usePluginsHook.usePlugins).mockReturnValue(createMockUsePlugins())
vi.mocked(usePluginsHook.useEnablePlugin).mockReturnValue(createMockMutation())
vi.mocked(usePluginsHook.useDisablePlugin).mockReturnValue(createMockMutation())
vi.mocked(usePluginsHook.useReloadPlugins).mockReturnValue(mockReload)
renderWithProviders(<Plugins />)
const reloadButton = screen.getByRole('button', { name: /reload plugins/i })
await user.click(reloadButton)
await waitFor(() => {
expect(toast.error).toHaveBeenCalled()
})
})
it('graceful degradation', () => {
vi.mocked(usePluginsHook.usePlugins).mockReturnValue(createMockUsePlugins([mockErrorPlugin]))
vi.mocked(usePluginsHook.useEnablePlugin).mockReturnValue(createMockMutation())
vi.mocked(usePluginsHook.useDisablePlugin).mockReturnValue(createMockMutation())
vi.mocked(usePluginsHook.useReloadPlugins).mockReturnValue(createMockMutation())
renderWithProviders(<Plugins />)
// Plugin with error should still render
expect(screen.getByText('Error Plugin')).toBeInTheDocument()
expect(screen.getByText('Failed to load plugin')).toBeInTheDocument()
})
it('error boundary integration', () => {
// This test verifies component doesn't crash on error
vi.mocked(usePluginsHook.usePlugins).mockReturnValue({
...createMockUsePlugins(),
data: undefined,
isError: true,
error: new Error('Critical error'),
})
vi.mocked(usePluginsHook.useEnablePlugin).mockReturnValue(createMockMutation())
vi.mocked(usePluginsHook.useDisablePlugin).mockReturnValue(createMockMutation())
vi.mocked(usePluginsHook.useReloadPlugins).mockReturnValue(createMockMutation())
expect(() => renderWithProviders(<Plugins />)).not.toThrow()
})
it('retry mechanisms', async () => {
const user = userEvent.setup()
const mockReload = createMockMutation()
mockReload.mutateAsync = vi
.fn()
.mockRejectedValueOnce(new Error('Fail'))
.mockResolvedValueOnce({ message: 'Success', count: 2 })
vi.mocked(usePluginsHook.usePlugins).mockReturnValue(createMockUsePlugins())
vi.mocked(usePluginsHook.useEnablePlugin).mockReturnValue(createMockMutation())
vi.mocked(usePluginsHook.useDisablePlugin).mockReturnValue(createMockMutation())
vi.mocked(usePluginsHook.useReloadPlugins).mockReturnValue(mockReload)
renderWithProviders(<Plugins />)
const reloadButton = screen.getByRole('button', { name: /reload plugins/i })
await user.click(reloadButton)
await user.click(reloadButton)
// Second click should succeed
expect(mockReload.mutateAsync).toHaveBeenCalledTimes(2)
})
})
describe('Plugins - Edge Cases', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('empty plugin list', () => {
vi.mocked(usePluginsHook.usePlugins).mockReturnValue(createMockUsePlugins([]))
vi.mocked(usePluginsHook.useEnablePlugin).mockReturnValue(createMockMutation())
vi.mocked(usePluginsHook.useDisablePlugin).mockReturnValue(createMockMutation())
vi.mocked(usePluginsHook.useReloadPlugins).mockReturnValue(createMockMutation())
renderWithProviders(<Plugins />)
expect(screen.getByText(/no plugins found/i)).toBeInTheDocument()
})
it('all plugins disabled', () => {
const allDisabled = [
{ ...mockBuiltInPlugin, enabled: false },
{ ...mockExternalPlugin, enabled: false },
]
vi.mocked(usePluginsHook.usePlugins).mockReturnValue(createMockUsePlugins(allDisabled))
vi.mocked(usePluginsHook.useEnablePlugin).mockReturnValue(createMockMutation())
vi.mocked(usePluginsHook.useDisablePlugin).mockReturnValue(createMockMutation())
vi.mocked(usePluginsHook.useReloadPlugins).mockReturnValue(createMockMutation())
renderWithProviders(<Plugins />)
expect(screen.getAllByText(/disabled/i).length).toBeGreaterThan(0)
})
it('mixed status plugins', () => {
const mixedPlugins = [mockBuiltInPlugin, mockDisabledPlugin, mockErrorPlugin]
vi.mocked(usePluginsHook.usePlugins).mockReturnValue(createMockUsePlugins(mixedPlugins))
vi.mocked(usePluginsHook.useEnablePlugin).mockReturnValue(createMockMutation())
vi.mocked(usePluginsHook.useDisablePlugin).mockReturnValue(createMockMutation())
vi.mocked(usePluginsHook.useReloadPlugins).mockReturnValue(createMockMutation())
renderWithProviders(<Plugins />)
expect(screen.getByText(/loaded/i)).toBeInTheDocument()
expect(screen.getByText(/disabled/i)).toBeInTheDocument()
expect(screen.getByText(/error/i)).toBeInTheDocument()
})
it('long plugin names', () => {
const longName = 'A'.repeat(100)
const longNamePlugin = { ...mockExternalPlugin, name: longName }
vi.mocked(usePluginsHook.usePlugins).mockReturnValue(
createMockUsePlugins([longNamePlugin])
)
vi.mocked(usePluginsHook.useEnablePlugin).mockReturnValue(createMockMutation())
vi.mocked(usePluginsHook.useDisablePlugin).mockReturnValue(createMockMutation())
vi.mocked(usePluginsHook.useReloadPlugins).mockReturnValue(createMockMutation())
renderWithProviders(<Plugins />)
expect(screen.getByText(longName)).toBeInTheDocument()
})
it('missing metadata', () => {
const noMetadata: PluginInfo = {
id: 99,
uuid: 'no-meta',
name: 'No Metadata',
type: 'unknown',
enabled: true,
status: 'loaded',
is_built_in: false,
created_at: '2025-01-01T00:00:00Z',
updated_at: '2025-01-01T00:00:00Z',
}
vi.mocked(usePluginsHook.usePlugins).mockReturnValue(createMockUsePlugins([noMetadata]))
vi.mocked(usePluginsHook.useEnablePlugin).mockReturnValue(createMockMutation())
vi.mocked(usePluginsHook.useDisablePlugin).mockReturnValue(createMockMutation())
vi.mocked(usePluginsHook.useReloadPlugins).mockReturnValue(createMockMutation())
renderWithProviders(<Plugins />)
expect(screen.getByText('No Metadata')).toBeInTheDocument()
})
it('concurrent toggles', async () => {
const user = userEvent.setup()
const mockEnable = createMockMutation()
mockEnable.mutateAsync = vi.fn().mockResolvedValue({ message: 'Enabled' })
vi.mocked(usePluginsHook.usePlugins).mockReturnValue(
createMockUsePlugins([mockDisabledPlugin, { ...mockDisabledPlugin, id: 5, uuid: 'disabled2' }])
)
vi.mocked(usePluginsHook.useEnablePlugin).mockReturnValue(mockEnable)
vi.mocked(usePluginsHook.useDisablePlugin).mockReturnValue(createMockMutation())
vi.mocked(usePluginsHook.useReloadPlugins).mockReturnValue(createMockMutation())
renderWithProviders(<Plugins />)
await waitFor(() => screen.getAllByRole('switch'))
const switches = screen.getAllByRole('switch')
await Promise.all([user.click(switches[0]), user.click(switches[1])])
// Both mutations should be called
expect(mockEnable.mutateAsync).toHaveBeenCalled()
})
it('rapid reload clicks', async () => {
const user = userEvent.setup()
const mockReload = createMockMutation()
mockReload.mutateAsync = vi.fn().mockResolvedValue({ message: 'Reloaded', count: 2 })
vi.mocked(usePluginsHook.usePlugins).mockReturnValue(createMockUsePlugins())
vi.mocked(usePluginsHook.useEnablePlugin).mockReturnValue(createMockMutation())
vi.mocked(usePluginsHook.useDisablePlugin).mockReturnValue(createMockMutation())
vi.mocked(usePluginsHook.useReloadPlugins).mockReturnValue(mockReload)
renderWithProviders(<Plugins />)
const reloadButton = screen.getByRole('button', { name: /reload plugins/i })
await user.tripleClick(reloadButton)
// Should handle rapid clicks
expect(mockReload.mutateAsync).toHaveBeenCalled()
})
})