chore: clean .gitignore cache
This commit is contained in:
@@ -1,710 +0,0 @@
|
||||
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()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user