fix(ci): resolve E2E workflow failures and boost test coverage

E2E Workflow Fixes:

Add frontend dependency installation step (missing npm ci in frontend/)
Remove incorrect working-directory from backend build step
Update Node.js version from v18 to v20 (dependency requirements)
Backend Coverage: 84.9% → 85.0% (20+ new test functions):

Access list service validation and templates
Backup service error handling and edge cases
Security audit logs and rule sets
Auth service edge cases and token validation
Certificate service upload and sync error paths
Frontend Coverage: 85.06% → 85.66% (27 new tests):

Tabs component accessibility and keyboard navigation
Plugins page status badges and error handling
SecurityHeaders CRUD operations and presets
API wrappers for credentials and encryption endpoints
E2E Infrastructure:

Enhanced global-setup with emergency security module reset
Added retry logic and verification for settings propagation
Known Issues:

19 E2E tests still failing (ACL blocking security APIs - Issue #16)
7 Plugins modal UI tests failing (non-critical)
To be addressed in follow-up PR
Fixes #550 E2E workflow failures
Related to #16 ACL implementation
This commit is contained in:
GitHub Actions
2026-01-26 04:09:57 +00:00
parent 0b9484faf0
commit 29d2ec9cbf
20 changed files with 4280 additions and 1516 deletions
+710
View File
@@ -0,0 +1,710 @@
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()
})
})