chore: refactor tests to improve clarity and reliability

- Removed unnecessary test.skip() calls in various test files, replacing them with comments for clarity.
- Enhanced retry logic in TestDataManager for API requests to handle rate limiting more gracefully.
- Updated security helper functions to include retry mechanisms for fetching security status and setting module states.
- Improved loading completion checks to handle page closure scenarios.
- Adjusted WebKit-specific tests to run in all browsers, removing the previous skip logic.
- General cleanup and refactoring across multiple test files to enhance readability and maintainability.
This commit is contained in:
GitHub Actions
2026-02-08 00:02:09 +00:00
parent 5054a334f2
commit aa85c911c0
71 changed files with 22475 additions and 3241 deletions

View File

@@ -232,12 +232,8 @@ describe('AccessListForm', () => {
await user.selectOptions(typeSelect, 'whitelist');
// Toggle local network only
const localNetworkSwitch = screen.getByLabelText(/Local Network Only/i)
.querySelector('input[type="checkbox"]');
if (localNetworkSwitch) {
await user.click(localNetworkSwitch);
}
const localNetworkSwitch = screen.getByRole('checkbox', { name: /Local Network Only/i });
await user.click(localNetworkSwitch);
// IP inputs should be hidden
expect(screen.queryByPlaceholderText(/192.168.1.0\/24/i)).not.toBeInTheDocument();
@@ -260,7 +256,7 @@ describe('AccessListForm', () => {
/>
);
const submitBtn = screen.getByRole('button', { name: /Create/i });
const submitBtn = screen.getByRole('button', { name: /Saving.../i });
expect(submitBtn).toBeDisabled();
const cancelBtn = screen.getByRole('button', { name: /Cancel/i });
@@ -278,7 +274,7 @@ describe('AccessListForm', () => {
/>
);
const deleteBtn = screen.getByRole('button', { name: /Delete/i });
const deleteBtn = screen.getByRole('button', { name: /Deleting.../i });
expect(deleteBtn).toBeDisabled();
});
@@ -401,17 +397,17 @@ describe('AccessListForm', () => {
/>
);
const deleteBtn = screen.getByRole('button', { name: /Delete/i });
const deleteBtn = screen.getByRole('button', { name: /Deleting.../i });
expect(deleteBtn).toBeDisabled();
});
it('applies security preset for blacklist', async () => {
it('applies security preset for geo blacklist', async () => {
render(<AccessListForm onSubmit={mockSubmit} onCancel={mockCancel} />);
await user.type(screen.getByLabelText(/Name/i), 'Preset Test');
const typeSelect = screen.getByLabelText(/Type/i);
await user.selectOptions(typeSelect, 'blacklist');
await user.selectOptions(typeSelect, 'geo_blacklist');
const showBtn = screen.getByRole('button', { name: /Show Presets/i });
await user.click(showBtn);
@@ -449,12 +445,8 @@ describe('AccessListForm', () => {
await user.type(screen.getByLabelText(/Name/i), 'Switch Test');
const enabledSwitch = screen.getByLabelText(/^Enabled$/)
.querySelector('input[type="checkbox"]');
if (enabledSwitch) {
await user.click(enabledSwitch);
}
const enabledSwitch = screen.getByRole('checkbox', { name: /^Enabled$/i });
await user.click(enabledSwitch);
await user.click(screen.getByRole('button', { name: /Create/i }));
@@ -565,7 +557,7 @@ describe('AccessListForm', () => {
const typeSelect = screen.getByLabelText(/Type/i);
await user.selectOptions(typeSelect, 'blacklist');
expect(screen.getByText(/Recommended: Block lists are safer/i)).toBeInTheDocument();
expect(screen.getByText(/Block lists are safer/i)).toBeInTheDocument();
});
it('renders best practices link', () => {

View File

@@ -1,6 +1,7 @@
import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import userEvent from '@testing-library/user-event';
import AccessListSelector from '../AccessListSelector';
import * as useAccessListsHook from '../../hooks/useAccessLists';
import type { AccessList } from '../../api/accessLists';
@@ -35,11 +36,12 @@ describe('AccessListSelector', () => {
</Wrapper>
);
expect(screen.getByRole('combobox')).toBeInTheDocument();
const trigger = screen.getByRole('combobox', { name: /Access Control List/i });
expect(trigger).toBeInTheDocument();
expect(screen.getByText('No Access Control (Public)')).toBeInTheDocument();
});
it('should render with access lists and show only enabled ones', () => {
it('should render with access lists and show only enabled ones', async () => {
const mockLists: AccessList[] = [
{
id: 1,
@@ -75,6 +77,7 @@ describe('AccessListSelector', () => {
const mockOnChange = vi.fn();
const Wrapper = createWrapper();
const user = userEvent.setup();
render(
<Wrapper>
@@ -82,9 +85,11 @@ describe('AccessListSelector', () => {
</Wrapper>
);
expect(screen.getByRole('combobox')).toBeInTheDocument();
expect(screen.getByText('Test ACL 1 (whitelist)')).toBeInTheDocument();
expect(screen.queryByText('Test ACL 2 (blacklist)')).not.toBeInTheDocument();
const trigger = screen.getByRole('combobox', { name: /Access Control List/i });
await user.click(trigger);
expect(screen.getByRole('option', { name: 'Test ACL 1 (whitelist)' })).toBeInTheDocument();
expect(screen.queryByRole('option', { name: 'Test ACL 2 (blacklist)' })).not.toBeInTheDocument();
});
it('should show selected ACL details', () => {

View File

@@ -1,20 +1,16 @@
import { describe, it, expect, vi } from 'vitest'
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { QueryClientProvider } from '@tanstack/react-query'
import CertificateList from '../CertificateList'
import { createTestQueryClient } from '../../test/createTestQueryClient'
import { useCertificates } from '../../hooks/useCertificates'
import { useProxyHosts } from '../../hooks/useProxyHosts'
import type { Certificate } from '../../api/certificates'
import type { ProxyHost } from '../../api/proxyHosts'
vi.mock('../../hooks/useCertificates', () => ({
useCertificates: vi.fn(() => ({
certificates: [
{ id: 1, name: 'CustomCert', domain: 'example.com', issuer: 'Custom CA', expires_at: new Date().toISOString(), status: 'expired', provider: 'custom' },
{ id: 2, name: 'LE Staging', domain: 'staging.example.com', issuer: "Let's Encrypt Staging", expires_at: new Date().toISOString(), status: 'untrusted', provider: 'letsencrypt-staging' },
{ id: 3, name: 'ActiveCert', domain: 'active.example.com', issuer: 'Custom CA', expires_at: new Date().toISOString(), status: 'valid', provider: 'custom' },
],
isLoading: false,
error: null,
}))
useCertificates: vi.fn(),
}))
vi.mock('../../api/certificates', () => ({
@@ -26,19 +22,7 @@ vi.mock('../../api/backups', () => ({
}))
vi.mock('../../hooks/useProxyHosts', () => ({
useProxyHosts: vi.fn(() => ({
hosts: [
{ uuid: 'h1', name: 'Host1', certificate_id: 3 },
],
loading: false,
isFetching: false,
error: null,
createHost: vi.fn(),
updateHost: vi.fn(),
deleteHost: vi.fn(),
bulkUpdateACL: vi.fn(),
isBulkUpdating: false,
})),
useProxyHosts: vi.fn(),
}))
vi.mock('../../utils/toast', () => ({
@@ -50,6 +34,76 @@ function renderWithClient(ui: React.ReactNode) {
return render(<QueryClientProvider client={qc}>{ui}</QueryClientProvider>)
}
const createCertificatesValue = (overrides: Partial<ReturnType<typeof useCertificates>> = {}) => {
const certificates: Certificate[] = [
{ id: 1, name: 'CustomCert', domain: 'example.com', issuer: 'Custom CA', expires_at: '2026-03-01T00:00:00Z', status: 'expired', provider: 'custom' },
{ id: 2, name: 'LE Staging', domain: 'staging.example.com', issuer: "Let's Encrypt Staging", expires_at: '2026-04-01T00:00:00Z', status: 'untrusted', provider: 'letsencrypt-staging' },
{ id: 3, name: 'ActiveCert', domain: 'active.example.com', issuer: 'Custom CA', expires_at: '2026-02-01T00:00:00Z', status: 'valid', provider: 'custom' },
{ id: 4, name: 'UnusedValidCert', domain: 'unused.example.com', issuer: 'Custom CA', expires_at: '2026-05-01T00:00:00Z', status: 'valid', provider: 'custom' },
]
return {
certificates,
isLoading: false,
error: null,
refetch: vi.fn(),
...overrides,
}
}
const createProxyHost = (overrides: Partial<ProxyHost> = {}): ProxyHost => ({
uuid: 'h1',
name: 'Host1',
domain_names: 'host1.example.com',
forward_scheme: 'http',
forward_host: '127.0.0.1',
forward_port: 80,
ssl_forced: false,
http2_support: true,
hsts_enabled: false,
hsts_subdomains: false,
block_exploits: false,
websocket_support: false,
application: 'none',
locations: [],
enabled: true,
created_at: '2026-02-01T00:00:00Z',
updated_at: '2026-02-01T00:00:00Z',
certificate_id: 3,
...overrides,
})
const createProxyHostsValue = (overrides: Partial<ReturnType<typeof useProxyHosts>> = {}): ReturnType<typeof useProxyHosts> => ({
hosts: [
createProxyHost(),
],
loading: false,
isFetching: false,
error: null,
createHost: vi.fn(),
updateHost: vi.fn(),
deleteHost: vi.fn(),
bulkUpdateACL: vi.fn(),
bulkUpdateSecurityHeaders: vi.fn(),
isCreating: false,
isUpdating: false,
isDeleting: false,
isBulkUpdating: false,
...overrides,
})
const getRowNames = () =>
screen
.getAllByRole('row')
.slice(1)
.map(row => row.querySelector('td')?.textContent?.trim() ?? '')
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(useCertificates).mockReturnValue(createCertificatesValue())
vi.mocked(useProxyHosts).mockReturnValue(createProxyHostsValue())
})
describe('CertificateList', () => {
it('deletes custom certificate when confirmed', async () => {
const confirmSpy = vi.spyOn(window, 'confirm').mockImplementation(() => true)
@@ -86,28 +140,54 @@ describe('CertificateList', () => {
confirmSpy.mockRestore()
})
it('blocks deletion when certificate is in use by a proxy host', async () => {
const { toast } = await import('../../utils/toast')
it('deletes valid custom certificate when not in use', async () => {
const confirmSpy = vi.spyOn(window, 'confirm').mockImplementation(() => true)
const { deleteCertificate } = await import('../../api/certificates')
const { createBackup } = await import('../../api/backups')
const user = userEvent.setup()
renderWithClient(<CertificateList />)
const deleteButtons = await screen.findAllByTitle('Delete Certificate')
// Find button corresponding to ActiveCert (id 3)
const activeButton = deleteButtons.find(btn => btn.closest('tr')?.querySelector('td')?.textContent?.includes('ActiveCert'))
expect(activeButton).toBeTruthy()
if (activeButton) await user.click(activeButton)
await waitFor(() => expect(toast.error).toHaveBeenCalledWith(expect.stringContaining('in use')))
const rows = await screen.findAllByRole('row')
const unusedRow = rows.find(r => r.querySelector('td')?.textContent?.includes('UnusedValidCert')) as HTMLElement
expect(unusedRow).toBeTruthy()
const unusedButton = unusedRow.querySelector('button[title="Delete Certificate"]') as HTMLButtonElement
expect(unusedButton).toBeTruthy()
await user.click(unusedButton)
await waitFor(() => expect(createBackup).toHaveBeenCalled())
await waitFor(() => expect(deleteCertificate).toHaveBeenCalledWith(4))
confirmSpy.mockRestore()
})
it('blocks deletion when certificate status is active (valid/expiring)', async () => {
const { toast } = await import('../../utils/toast')
const user = userEvent.setup()
it('renders empty state when no certificates exist', async () => {
vi.mocked(useCertificates).mockReturnValue(createCertificatesValue({ certificates: [] }))
renderWithClient(<CertificateList />)
const deleteButtons = await screen.findAllByTitle('Delete Certificate')
// ActiveCert (valid) should block even if not linked ensure hosts mock links it so previous test covers linkage.
// Here, simulate clicking a valid cert button if present
const validButton = deleteButtons.find(btn => btn.closest('tr')?.querySelector('td')?.textContent?.includes('ActiveCert'))
expect(validButton).toBeTruthy()
if (validButton) await user.click(validButton)
await waitFor(() => expect(toast.error).toHaveBeenCalled())
expect(await screen.findByText('No certificates found.')).toBeInTheDocument()
})
it('shows error state when certificate load fails', async () => {
vi.mocked(useCertificates).mockReturnValue(createCertificatesValue({ error: new Error('boom') }))
renderWithClient(<CertificateList />)
expect(await screen.findByText('Failed to load certificates')).toBeInTheDocument()
})
it('sorts certificates by name and expiry when headers are clicked', async () => {
const certificates: Certificate[] = [
{ id: 10, name: 'Zulu', domain: 'z.example.com', issuer: 'Custom CA', expires_at: '2026-03-01T00:00:00Z', status: 'valid', provider: 'custom' },
{ id: 11, name: 'Alpha', domain: 'a.example.com', issuer: 'Custom CA', expires_at: '2026-01-01T00:00:00Z', status: 'valid', provider: 'custom' },
]
const user = userEvent.setup()
vi.mocked(useCertificates).mockReturnValue(createCertificatesValue({ certificates }))
renderWithClient(<CertificateList />)
expect(getRowNames()).toEqual(['Alpha', 'Zulu'])
await user.click(screen.getByText('Expires'))
expect(getRowNames()).toEqual(['Alpha', 'Zulu'])
await user.click(screen.getByText('Expires'))
expect(getRowNames()).toEqual(['Zulu', 'Alpha'])
})
})

View File

@@ -1,7 +1,7 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { QueryClient, QueryClientProvider, type UseMutationResult } from '@tanstack/react-query'
import CredentialManager from '../CredentialManager'
import {
useCredentials,
@@ -20,7 +20,7 @@ vi.mock('react-i18next', () => ({
}),
}))
import type { DNSProvider, DNSProviderTypeInfo } from '../../api/dnsProviders'
import type { DNSProviderCredential } from '../../api/credentials'
import type { CredentialRequest, CredentialTestResult, DNSProviderCredential } from '../../api/credentials'
vi.mock('../../hooks/useCredentials')
vi.mock('../../utils/toast', () => ({
@@ -87,6 +87,28 @@ const mockCredentials: DNSProviderCredential[] = [
},
]
const createCredentialsQueryResult = (
overrides: Record<string, unknown> = {}
): ReturnType<typeof useCredentials> => ({
data: mockCredentials,
isLoading: false,
refetch: vi.fn(),
error: null,
isError: false,
isSuccess: true,
...overrides,
} as unknown as ReturnType<typeof useCredentials>)
const createMutationResult = <TData, TVariables>(
mutateAsync: ReturnType<typeof vi.fn>,
overrides: Partial<UseMutationResult<TData, Error, TVariables, unknown>> = {}
): UseMutationResult<TData, Error, TVariables, unknown> => ({
mutate: vi.fn() as UseMutationResult<TData, Error, TVariables, unknown>['mutate'],
mutateAsync: mutateAsync as UseMutationResult<TData, Error, TVariables, unknown>['mutateAsync'],
isPending: false,
...overrides,
} as UseMutationResult<TData, Error, TVariables, unknown>)
const renderWithClient = (ui: React.ReactElement) => {
const queryClient = new QueryClient({
defaultOptions: {
@@ -99,7 +121,6 @@ const renderWithClient = (ui: React.ReactElement) => {
describe('CredentialManager', () => {
const mockOnOpenChange = vi.fn()
const mockRefetch = vi.fn()
const mockCreateMutate = vi.fn()
const mockUpdateMutate = vi.fn()
const mockDeleteMutate = vi.fn()
@@ -108,34 +129,32 @@ describe('CredentialManager', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(useCredentials).mockReturnValue({
data: mockCredentials,
isLoading: false,
refetch: mockRefetch,
error: null,
isError: false,
isSuccess: true,
} as any)
vi.mocked(useCredentials).mockReturnValue(createCredentialsQueryResult())
vi.mocked(useCreateCredential).mockReturnValue({
mutateAsync: mockCreateMutate,
isPending: false,
} as any)
vi.mocked(useCreateCredential).mockReturnValue(
createMutationResult<DNSProviderCredential, { providerId: number; data: CredentialRequest }>(
mockCreateMutate
)
)
vi.mocked(useUpdateCredential).mockReturnValue({
mutateAsync: mockUpdateMutate,
isPending: false,
} as any)
vi.mocked(useUpdateCredential).mockReturnValue(
createMutationResult<
DNSProviderCredential,
{ providerId: number; credentialId: number; data: CredentialRequest }
>(mockUpdateMutate)
)
vi.mocked(useDeleteCredential).mockReturnValue({
mutateAsync: mockDeleteMutate,
isPending: false,
} as any)
vi.mocked(useDeleteCredential).mockReturnValue(
createMutationResult<void, { providerId: number; credentialId: number }>(
mockDeleteMutate
)
)
vi.mocked(useTestCredential).mockReturnValue({
mutateAsync: mockTestMutate,
isPending: false,
} as any)
vi.mocked(useTestCredential).mockReturnValue(
createMutationResult<CredentialTestResult, { providerId: number; credentialId: number }>(
mockTestMutate
)
)
})
// 1. Rendering Checks
@@ -350,14 +369,9 @@ describe('CredentialManager', () => {
// 7. Empty Credential List Rendering
it('renders empty state when no credentials exist', () => {
vi.mocked(useCredentials).mockReturnValue({
data: [],
isLoading: false,
refetch: mockRefetch,
error: null,
isError: false,
isSuccess: true,
} as any)
vi.mocked(useCredentials).mockReturnValue(
createCredentialsQueryResult({ data: [] })
)
renderWithClient(
<CredentialManager
@@ -375,14 +389,15 @@ describe('CredentialManager', () => {
// 8. Loading State
it('renders loading state while fetching credentials', () => {
vi.mocked(useCredentials).mockReturnValue({
data: [],
isLoading: true,
refetch: mockRefetch,
error: null,
isError: false,
isSuccess: false,
} as any)
vi.mocked(useCredentials).mockReturnValue(
createCredentialsQueryResult({
data: [],
isLoading: true,
isSuccess: false,
status: 'loading',
fetchStatus: 'fetching',
})
)
renderWithClient(
<CredentialManager
@@ -527,21 +542,16 @@ describe('CredentialManager', () => {
key_version: 1,
success_count: 5,
failure_count: 2,
last_used_at: null,
last_error: null,
last_used_at: undefined,
last_error: undefined,
created_at: '2025-01-02T00:00:00Z',
updated_at: '2025-01-02T00:00:00Z',
}
]
vi.mocked(useCredentials).mockReturnValue({
data: multipleCreds,
isLoading: false,
refetch: mockRefetch,
error: null,
isError: false,
isSuccess: true,
} as any)
vi.mocked(useCredentials).mockReturnValue(
createCredentialsQueryResult({ data: multipleCreds })
)
renderWithClient(
<CredentialManager
@@ -564,14 +574,9 @@ describe('CredentialManager', () => {
enabled: false,
}
vi.mocked(useCredentials).mockReturnValue({
data: [disabledCred],
isLoading: false,
refetch: mockRefetch,
error: null,
isError: false,
isSuccess: true,
} as any)
vi.mocked(useCredentials).mockReturnValue(
createCredentialsQueryResult({ data: [disabledCred] })
)
renderWithClient(
<CredentialManager
@@ -593,14 +598,9 @@ describe('CredentialManager', () => {
last_error: 'API rate limit exceeded',
}
vi.mocked(useCredentials).mockReturnValue({
data: [errorCred],
isLoading: false,
refetch: mockRefetch,
error: null,
isError: false,
isSuccess: true,
} as any)
vi.mocked(useCredentials).mockReturnValue(
createCredentialsQueryResult({ data: [errorCred] })
)
renderWithClient(
<CredentialManager

View File

@@ -77,23 +77,40 @@ describe('Layout', () => {
})
it('renders all navigation items', async () => {
const user = userEvent.setup()
renderWithProviders(
<Layout>
<div>Test Content</div>
</Layout>
)
expect(screen.getByText('Dashboard')).toBeInTheDocument()
expect(screen.getByText('Proxy Hosts')).toBeInTheDocument()
expect(screen.getByText('Remote Servers')).toBeInTheDocument()
expect(screen.getByText('Certificates')).toBeInTheDocument()
expect(await screen.findByText('Dashboard')).toBeInTheDocument()
expect(await screen.findByText('Proxy Hosts')).toBeInTheDocument()
expect(await screen.findByText('Remote Servers')).toBeInTheDocument()
expect(await screen.findByText('Domains')).toBeInTheDocument()
expect(await screen.findByText('Certificates')).toBeInTheDocument()
expect(await screen.findByText('DNS')).toBeInTheDocument()
expect(await screen.findByText('Settings')).toBeInTheDocument()
// Expand DNS to see nested items
await user.click(await screen.findByRole('button', { name: /dns/i }))
expect(await screen.findByText('DNS Providers')).toBeInTheDocument()
expect(await screen.findByText('Plugins')).toBeInTheDocument()
// Expand Security to see nested items
await user.click(await screen.findByRole('button', { name: /security/i }))
expect(await screen.findByText('Access Lists')).toBeInTheDocument()
expect(await screen.findByText('Rate Limiting')).toBeInTheDocument()
// Expand Tasks and Import to see nested items
await userEvent.click(screen.getByText('Tasks'))
expect(screen.getByText('Import')).toBeInTheDocument()
await userEvent.click(screen.getByText('Import'))
expect(screen.getByText('Caddyfile')).toBeInTheDocument()
expect(screen.getByText('CrowdSec')).toBeInTheDocument()
expect(screen.getByText('Settings')).toBeInTheDocument()
await user.click(await screen.findByRole('button', { name: /tasks/i }))
expect(await screen.findByText('Import')).toBeInTheDocument()
await user.click(await screen.findByRole('button', { name: /import/i }))
expect(await screen.findByText('Caddyfile')).toBeInTheDocument()
const crowdSecLinks = await screen.findAllByRole('link', { name: 'CrowdSec' })
expect(crowdSecLinks.some(link => link.getAttribute('href') === '/tasks/import/crowdsec')).toBe(true)
expect(await screen.findByText('Import NPM')).toBeInTheDocument()
expect(await screen.findByText('Import JSON')).toBeInTheDocument()
})
it('renders children content', () => {
@@ -281,8 +298,7 @@ describe('Layout', () => {
})
it('defaults to showing Security and Uptime when feature flags are loading', async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
vi.mocked(featureFlagsApi.getFeatureFlags).mockResolvedValue({} as any)
vi.mocked(featureFlagsApi.getFeatureFlags).mockResolvedValue({})
renderWithProviders(
<Layout>

View File

@@ -87,11 +87,12 @@ describe('NotificationCenter', () => {
})
it('opens notification panel on click', async () => {
const user = userEvent.setup()
vi.mocked(api.getNotifications).mockResolvedValue(mockNotifications)
render(<NotificationCenter />, { wrapper: createWrapper() })
const bellButton = screen.getByRole('button', { name: /notifications/i })
await userEvent.click(bellButton)
await user.click(bellButton)
await waitFor(() => {
expect(screen.getByText('Notifications')).toBeInTheDocument()
@@ -103,11 +104,12 @@ describe('NotificationCenter', () => {
})
it('displays empty state when no notifications', async () => {
const user = userEvent.setup()
vi.mocked(api.getNotifications).mockResolvedValue([])
render(<NotificationCenter />, { wrapper: createWrapper() })
const bellButton = screen.getByRole('button', { name: /notifications/i })
await userEvent.click(bellButton)
await user.click(bellButton)
await waitFor(() => {
expect(screen.getByText('No new notifications')).toBeInTheDocument()
@@ -115,19 +117,20 @@ describe('NotificationCenter', () => {
})
it('marks single notification as read', async () => {
const user = userEvent.setup()
vi.mocked(api.getNotifications).mockResolvedValue(mockNotifications)
vi.mocked(api.markNotificationRead).mockResolvedValue()
render(<NotificationCenter />, { wrapper: createWrapper() })
await userEvent.click(screen.getByRole('button', { name: /notifications/i }))
await user.click(screen.getByRole('button', { name: /notifications/i }))
await waitFor(() => {
expect(screen.getByText('Info Notification')).toBeInTheDocument()
})
const closeButtons = screen.getAllByRole('button', { name: /close/i })
await userEvent.click(closeButtons[0])
await user.click(closeButtons[0])
await waitFor(() => {
expect(api.markNotificationRead).toHaveBeenCalledWith('1', expect.anything())
@@ -135,18 +138,19 @@ describe('NotificationCenter', () => {
})
it('marks all notifications as read', async () => {
const user = userEvent.setup()
vi.mocked(api.getNotifications).mockResolvedValue(mockNotifications)
vi.mocked(api.markAllNotificationsRead).mockResolvedValue()
render(<NotificationCenter />, { wrapper: createWrapper() })
await userEvent.click(screen.getByRole('button', { name: /notifications/i }))
await user.click(screen.getByRole('button', { name: /notifications/i }))
await waitFor(() => {
expect(screen.getByText('Mark all read')).toBeInTheDocument()
})
await userEvent.click(screen.getByText('Mark all read'))
await user.click(screen.getByText('Mark all read'))
await waitFor(() => {
expect(api.markAllNotificationsRead).toHaveBeenCalled()
@@ -154,16 +158,17 @@ describe('NotificationCenter', () => {
})
it('closes panel when clicking outside', async () => {
const user = userEvent.setup()
vi.mocked(api.getNotifications).mockResolvedValue(mockNotifications)
render(<NotificationCenter />, { wrapper: createWrapper() })
await userEvent.click(screen.getByRole('button', { name: /notifications/i }))
await user.click(screen.getByRole('button', { name: /notifications/i }))
await waitFor(() => {
expect(screen.getByText('Notifications')).toBeInTheDocument()
})
await userEvent.click(screen.getByTestId('notification-backdrop'))
await user.click(screen.getByTestId('notification-backdrop'))
await waitFor(() => {
expect(screen.queryByText('Notifications')).not.toBeInTheDocument()

View File

@@ -81,6 +81,30 @@ vi.mock('../../hooks/useDNSProviders', () => ({
})),
}))
vi.mock('../../hooks/useDNSDetection', () => ({
useDetectDNSProvider: vi.fn(() => ({
mutateAsync: vi.fn().mockResolvedValue({
domain: 'example.com',
detected: false,
nameservers: [],
confidence: 'none',
}),
isPending: false,
data: undefined,
reset: vi.fn(),
})),
}))
vi.mock('../../api/dnsDetection', () => ({
detectDNSProvider: vi.fn().mockResolvedValue({
domain: 'example.com',
detected: false,
nameservers: [],
confidence: 'none',
}),
getDetectionPatterns: vi.fn().mockResolvedValue([]),
}))
vi.mock('../../api/proxyHosts', () => ({
testProxyHostConnection: vi.fn(),
}))

View File

@@ -83,7 +83,12 @@ vi.mock('../../hooks/useSecurityHeaders', () => ({
vi.mock('../../hooks/useDNSDetection', () => ({
useDetectDNSProvider: vi.fn(() => ({
mutateAsync: vi.fn(),
mutateAsync: vi.fn().mockResolvedValue({
domain: 'example.com',
detected: false,
nameservers: [],
confidence: 'none',
}),
isPending: false,
data: null,
reset: vi.fn(),
@@ -144,6 +149,13 @@ const renderWithClientAct = async (ui: React.ReactElement) => {
})
}
const selectComboboxOption = async (label: string | RegExp, optionText: string) => {
const trigger = screen.getByRole('combobox', { name: label })
await userEvent.click(trigger)
const option = await screen.findByRole('option', { name: optionText })
await userEvent.click(option)
}
import { testProxyHostConnection } from '../../api/proxyHosts'
describe('ProxyHostForm', () => {
@@ -170,12 +182,7 @@ describe('ProxyHostForm', () => {
expect(screen.getByText('Add Proxy Host')).toBeInTheDocument()
})
// Find scheme select - it defaults to HTTP
// We can find it by label "Scheme"
const schemeSelect = screen.getByLabelText('Scheme') as HTMLSelectElement
await userEvent.selectOptions(schemeSelect, 'https')
expect(schemeSelect).toHaveValue('https')
await selectComboboxOption('Scheme', 'HTTPS')
})
it('prompts to save new base domain', async () => {
@@ -289,15 +296,15 @@ describe('ProxyHostForm', () => {
expect(screen.getByLabelText('Base Domain (Auto-fill)')).toBeInTheDocument()
})
await userEvent.selectOptions(screen.getByLabelText('Base Domain (Auto-fill)') as HTMLSelectElement, 'existing.com')
await selectComboboxOption(/Base Domain/i, 'existing.com')
// Should not update domain names yet as no container selected
expect(screen.getByLabelText(/Domain Names/i)).toHaveValue('')
// Select container then base domain
await userEvent.selectOptions(screen.getByLabelText('Source') as HTMLSelectElement, 'local')
await userEvent.selectOptions(screen.getByLabelText('Containers') as HTMLSelectElement, 'container-123')
await userEvent.selectOptions(screen.getByLabelText('Base Domain (Auto-fill)') as HTMLSelectElement, 'existing.com')
await selectComboboxOption('Source', 'Local (Docker Socket)')
await selectComboboxOption('Containers', 'my-app (nginx:latest)')
await selectComboboxOption(/Base Domain/i, 'existing.com')
expect(screen.getByLabelText(/Domain Names/i)).toHaveValue('my-app.existing.com')
})
@@ -309,17 +316,20 @@ describe('ProxyHostForm', () => {
<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
)
const presetSelect = screen.getByLabelText(/Application Preset/i)
expect(presetSelect).toBeInTheDocument()
const presetTrigger = screen.getByRole('combobox', { name: /Application Preset/i })
expect(presetTrigger).toBeInTheDocument()
await userEvent.click(presetTrigger)
const presetListbox = screen.getByRole('listbox')
// Check that all presets are available
expect(screen.getByText('None - Standard reverse proxy')).toBeInTheDocument()
expect(screen.getByText('Plex - Media server with remote access')).toBeInTheDocument()
expect(screen.getByText('Jellyfin - Open source media server')).toBeInTheDocument()
expect(screen.getByText('Emby - Media server')).toBeInTheDocument()
expect(screen.getByText('Home Assistant - Home automation')).toBeInTheDocument()
expect(screen.getByText('Nextcloud - File sync and share')).toBeInTheDocument()
expect(screen.getByText('Vaultwarden - Password manager')).toBeInTheDocument()
expect(within(presetListbox).getByText('None - Standard reverse proxy')).toBeInTheDocument()
expect(within(presetListbox).getByText('Plex - Media server with remote access')).toBeInTheDocument()
expect(within(presetListbox).getByText('Jellyfin - Open source media server')).toBeInTheDocument()
expect(within(presetListbox).getByText('Emby - Media server')).toBeInTheDocument()
expect(within(presetListbox).getByText('Home Assistant - Home automation')).toBeInTheDocument()
expect(within(presetListbox).getByText('Nextcloud - File sync and share')).toBeInTheDocument()
expect(within(presetListbox).getByText('Vaultwarden - Password manager')).toBeInTheDocument()
})
it('defaults to none preset', async () => {
@@ -327,8 +337,8 @@ describe('ProxyHostForm', () => {
<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
)
const presetSelect = screen.getByLabelText(/Application Preset/i)
expect(presetSelect).toHaveValue('none')
const presetTrigger = screen.getByRole('combobox', { name: /Application Preset/i })
expect(presetTrigger).toHaveTextContent('None - Standard reverse proxy')
})
it('enables websockets when selecting plex preset', async () => {
@@ -343,9 +353,7 @@ describe('ProxyHostForm', () => {
}
// Select Plex preset
await act(async () => {
await userEvent.selectOptions(screen.getByLabelText(/Application Preset/i) as HTMLSelectElement, 'plex')
})
await selectComboboxOption(/Application Preset/i, 'Plex - Media server with remote access')
// Websockets should be enabled
expect(screen.getByLabelText(/Websockets Support/i)).toBeChecked()
@@ -360,7 +368,7 @@ describe('ProxyHostForm', () => {
await userEvent.type(screen.getByPlaceholderText('example.com, www.example.com'), 'plex.mydomain.com')
// Select Plex preset
await userEvent.selectOptions(screen.getByLabelText(/Application Preset/i) as HTMLSelectElement, 'plex')
await selectComboboxOption(/Application Preset/i, 'Plex - Media server with remote access')
// Should show the helper with external URL
await waitFor(() => {
@@ -378,9 +386,7 @@ describe('ProxyHostForm', () => {
await userEvent.type(screen.getByPlaceholderText('example.com, www.example.com'), 'jellyfin.mydomain.com')
// Select Jellyfin preset
await act(async () => {
await userEvent.selectOptions(screen.getByLabelText(/Application Preset/i) as HTMLSelectElement, 'jellyfin')
})
await selectComboboxOption(/Application Preset/i, 'Jellyfin - Open source media server')
// Wait for health API fetch and show helper
await waitFor(() => {
@@ -398,9 +404,7 @@ describe('ProxyHostForm', () => {
await userEvent.type(screen.getByPlaceholderText('example.com, www.example.com'), 'ha.mydomain.com')
// Select Home Assistant preset
await act(async () => {
await userEvent.selectOptions(screen.getByLabelText(/Application Preset/i) as HTMLSelectElement, 'homeassistant')
})
await selectComboboxOption(/Application Preset/i, 'Home Assistant - Home automation')
// Wait for health API fetch and show helper
await waitFor(() => {
@@ -419,9 +423,7 @@ describe('ProxyHostForm', () => {
await userEvent.type(screen.getByPlaceholderText('example.com, www.example.com'), 'nextcloud.mydomain.com')
// Select Nextcloud preset
await act(async () => {
await userEvent.selectOptions(screen.getByLabelText(/Application Preset/i) as HTMLSelectElement, 'nextcloud')
})
await selectComboboxOption(/Application Preset/i, 'Nextcloud - File sync and share')
// Wait for health API fetch and show helper
await waitFor(() => {
@@ -440,7 +442,7 @@ describe('ProxyHostForm', () => {
await userEvent.type(screen.getByPlaceholderText('example.com, www.example.com'), 'vault.mydomain.com')
// Select Vaultwarden preset
await userEvent.selectOptions(screen.getByLabelText(/Application Preset/i) as HTMLSelectElement, 'vaultwarden')
await selectComboboxOption(/Application Preset/i, 'Vaultwarden - Password manager')
// Wait for helper text
await waitFor(() => {
@@ -476,17 +478,17 @@ describe('ProxyHostForm', () => {
)
// Select local source
await userEvent.selectOptions(screen.getByLabelText('Source') as HTMLSelectElement, 'local')
await selectComboboxOption('Source', 'Local (Docker Socket)')
// Select the plex container
await waitFor(() => {
expect(screen.getByText('plex (linuxserver/plex:latest)')).toBeInTheDocument()
})
await userEvent.selectOptions(screen.getByLabelText('Containers') as HTMLSelectElement, 'plex-container')
await selectComboboxOption('Containers', 'plex (linuxserver/plex:latest)')
// The preset should be auto-detected as plex
expect(screen.getByLabelText(/Application Preset/i)).toHaveValue('plex')
expect(screen.getByRole('combobox', { name: /Application Preset/i })).toHaveTextContent('Plex')
})
it('auto-populates advanced_config when selecting plex preset and field empty', async () => {
@@ -499,9 +501,7 @@ describe('ProxyHostForm', () => {
expect(textarea).toHaveValue('')
// Select Plex preset
await act(async () => {
await userEvent.selectOptions(screen.getByLabelText(/Application Preset/i) as HTMLSelectElement, 'plex')
})
await selectComboboxOption(/Application Preset/i, 'Plex - Media server with remote access')
// Value should now be populated with a snippet containing X-Real-IP which is used by the preset
await waitFor(() => {
@@ -537,7 +537,7 @@ describe('ProxyHostForm', () => {
)
// Select Plex preset (should prompt since advanced_config is non-empty)
await userEvent.selectOptions(screen.getByLabelText(/Application Preset/i) as HTMLSelectElement, 'plex')
await selectComboboxOption(/Application Preset/i, 'Plex - Media server with remote access')
await waitFor(() => {
expect(screen.getByText('Confirm Preset Overwrite')).toBeInTheDocument()
@@ -604,7 +604,7 @@ describe('ProxyHostForm', () => {
await userEvent.type(screen.getByLabelText(/^Port$/), '32400')
// Select Plex preset
await userEvent.selectOptions(screen.getByLabelText(/Application Preset/i) as HTMLSelectElement, 'plex')
await selectComboboxOption(/Application Preset/i, 'Plex - Media server with remote access')
// Submit form
await userEvent.click(screen.getByText('Save'))
@@ -645,7 +645,7 @@ describe('ProxyHostForm', () => {
)
// The preset should be pre-selected
expect(screen.getByLabelText(/Application Preset/i)).toHaveValue('plex')
expect(screen.getByRole('combobox', { name: /Application Preset/i })).toHaveTextContent('Plex')
// The config helper should be visible
await waitFor(() => {
@@ -684,7 +684,7 @@ describe('ProxyHostForm', () => {
await userEvent.type(screen.getByPlaceholderText('example.com, www.example.com'), 'plex.mydomain.com')
// Select Plex preset
await userEvent.selectOptions(screen.getByLabelText(/Application Preset/i) as HTMLSelectElement, 'plex')
await selectComboboxOption(/Application Preset/i, 'Plex - Media server with remote access')
// Wait for helper to appear
await waitFor(() => {
@@ -742,11 +742,10 @@ describe('ProxyHostForm', () => {
// Select 'Trusted IPs'
// Need to find the select by label/aria-label? AccessListSelector has label "Access Control List"
const aclSelect = screen.getByLabelText(/Access Control List/i)
await userEvent.selectOptions(aclSelect, '10')
await selectComboboxOption(/Access Control List/i, 'Trusted IPs (allow list)')
// Verify it was selected
expect(aclSelect).toHaveValue('10')
expect(screen.getByRole('combobox', { name: /Access Control List/i })).toHaveTextContent('Trusted IPs')
// Verify description appears
expect(screen.getByText('Trusted IPs')).toBeInTheDocument()
@@ -836,8 +835,8 @@ describe('ProxyHostForm', () => {
)
await userEvent.type(screen.getByLabelText(/^Name/), 'My Service')
await userEvent.type(screen.getByPlaceholderText('example.com, www.example.com'), 'myservice.com')
await userEvent.selectOptions(screen.getByLabelText(/^Scheme/), 'https')
await userEvent.type(screen.getByPlaceholderText('example.com, www.example.com'), 'myservice.existing.com')
await selectComboboxOption('Scheme', 'HTTPS')
await userEvent.type(screen.getByLabelText(/^Host/), '192.168.1.100')
await userEvent.clear(screen.getByLabelText(/^Port/))
await userEvent.type(screen.getByLabelText(/^Port/), '8080')
@@ -847,7 +846,7 @@ describe('ProxyHostForm', () => {
await waitFor(() => {
expect(mockOnSubmit).toHaveBeenCalledWith(expect.objectContaining({
name: 'My Service',
domain_names: 'myservice.com',
domain_names: 'myservice.existing.com',
forward_scheme: 'https',
forward_host: '192.168.1.100',
forward_port: 8080,
@@ -861,13 +860,12 @@ describe('ProxyHostForm', () => {
)
await userEvent.type(screen.getByLabelText(/^Name/), 'Cert Test')
await userEvent.type(screen.getByPlaceholderText('example.com, www.example.com'), 'cert.example.com')
await userEvent.type(screen.getByPlaceholderText('example.com, www.example.com'), 'cert.existing.com')
await userEvent.type(screen.getByLabelText(/^Host/), '192.168.1.100')
await userEvent.type(screen.getByLabelText(/^Port/), '80')
// Select certificate
const certSelect = screen.getByLabelText(/Certificate/i)
await userEvent.selectOptions(certSelect, '1')
await selectComboboxOption(/SSL Certificate/i, 'Cert 1 (custom)')
await userEvent.click(screen.getByText('Save'))
@@ -884,13 +882,12 @@ describe('ProxyHostForm', () => {
)
await userEvent.type(screen.getByLabelText(/^Name/), 'Security Headers Test')
await userEvent.type(screen.getByPlaceholderText('example.com, www.example.com'), 'secure.example.com')
await userEvent.type(screen.getByPlaceholderText('example.com, www.example.com'), 'secure.existing.com')
await userEvent.type(screen.getByLabelText(/^Host/), '192.168.1.100')
await userEvent.type(screen.getByLabelText(/^Port/), '80')
// Select security header profile
const profileSelect = screen.getByLabelText(/Security Headers/i)
await userEvent.selectOptions(profileSelect, '100')
await selectComboboxOption(/Security Headers/i, 'Strict Profile (Score: 90/100)')
await userEvent.click(screen.getByText('Save'))
@@ -936,7 +933,7 @@ describe('ProxyHostForm', () => {
// Fields should be pre-filled
expect(screen.getByDisplayValue('Existing Service')).toBeInTheDocument()
expect(screen.getByDisplayValue('existing.com')).toBeInTheDocument()
expect(screen.getByLabelText(/Domain Names/i)).toHaveValue('existing.com')
expect(screen.getByDisplayValue('192.168.1.50')).toBeInTheDocument()
// Update and save
@@ -997,61 +994,25 @@ describe('ProxyHostForm', () => {
})
describe('Scheme Selection', () => {
it('shows scheme options http, https, ws, wss', async () => {
it('shows scheme options http and https', async () => {
await renderWithClientAct(
<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
)
const schemeSelect = screen.getByLabelText('Scheme')
expect(schemeSelect).toBeInTheDocument()
const schemeTrigger = screen.getByRole('combobox', { name: 'Scheme' })
await userEvent.click(schemeTrigger)
const options = schemeSelect.querySelectorAll('option')
const values = Array.from(options).map(o => o.value)
expect(values).toContain('http')
expect(values).toContain('https')
expect(values).toContain('ws')
expect(values).toContain('wss')
expect(await screen.findByRole('option', { name: 'HTTP' })).toBeInTheDocument()
expect(await screen.findByRole('option', { name: 'HTTPS' })).toBeInTheDocument()
})
it('accepts websockets scheme', async () => {
it('accepts https scheme', async () => {
await renderWithClientAct(
<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
)
await userEvent.type(screen.getByLabelText(/^Name/), 'WS Test')
await userEvent.type(screen.getByPlaceholderText('example.com, www.example.com'), 'ws.example.com')
await userEvent.selectOptions(screen.getByLabelText('Scheme') as HTMLSelectElement, 'ws')
await userEvent.type(screen.getByLabelText(/^Host/), '192.168.1.100')
await userEvent.type(screen.getByLabelText(/^Port/), '8000')
await userEvent.click(screen.getByText('Save'))
await waitFor(() => {
expect(mockOnSubmit).toHaveBeenCalledWith(expect.objectContaining({
forward_scheme: 'ws',
}))
})
})
it('accepts secure websockets scheme', async () => {
await renderWithClientAct(
<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
)
await userEvent.type(screen.getByLabelText(/^Name/), 'WSS Test')
await userEvent.type(screen.getByPlaceholderText('example.com, www.example.com'), 'wss.example.com')
await userEvent.selectOptions(screen.getByLabelText('Scheme') as HTMLSelectElement, 'wss')
await userEvent.type(screen.getByLabelText(/^Host/), '192.168.1.100')
await userEvent.type(screen.getByLabelText(/^Port/), '8000')
await userEvent.click(screen.getByText('Save'))
await waitFor(() => {
expect(mockOnSubmit).toHaveBeenCalledWith(expect.objectContaining({
forward_scheme: 'wss',
}))
})
await selectComboboxOption('Scheme', 'HTTPS')
expect(screen.getByRole('combobox', { name: 'Scheme' })).toHaveTextContent('HTTPS')
})
})
@@ -1075,11 +1036,11 @@ describe('ProxyHostForm', () => {
)
// Select Plex preset
await userEvent.selectOptions(screen.getByLabelText(/Application Preset/i) as HTMLSelectElement, 'plex')
await selectComboboxOption(/Application Preset/i, 'Plex - Media server with remote access')
// Find advanced config field (it's in a collapsible section)
// Check that advanced config JSON for plex has been populated
const advancedConfigField = screen.getByPlaceholderText(/Caddy JSON config/i) as HTMLTextAreaElement
const advancedConfigField = screen.getByLabelText(/Advanced Caddy Config/i) as HTMLTextAreaElement
// Verify it contains JSON (Plex has some default config)
if (advancedConfigField.value) {
@@ -1093,7 +1054,7 @@ describe('ProxyHostForm', () => {
)
await userEvent.type(screen.getByLabelText(/^Name/), 'Custom Config')
await userEvent.type(screen.getByPlaceholderText('example.com, www.example.com'), 'custom.example.com')
await userEvent.type(screen.getByPlaceholderText('example.com, www.example.com'), 'custom.existing.com')
await userEvent.type(screen.getByLabelText(/^Host/), '192.168.1.100')
await userEvent.type(screen.getByLabelText(/^Port/), '80')
@@ -1117,7 +1078,7 @@ describe('ProxyHostForm', () => {
)
await userEvent.type(screen.getByLabelText(/^Name/), 'Port Test')
await userEvent.type(screen.getByPlaceholderText('example.com, www.example.com'), 'port.example.com')
await userEvent.type(screen.getByPlaceholderText('example.com, www.example.com'), 'port.existing.com')
await userEvent.type(screen.getByLabelText(/^Host/), '192.168.1.100')
// Clear and set invalid port
@@ -1125,13 +1086,11 @@ describe('ProxyHostForm', () => {
await userEvent.clear(portInput)
await userEvent.type(portInput, '99999')
// The form should still allow submission (validation happens server-side usually)
// But port should be converted to number
// Invalid port should block submission via native validation
await userEvent.click(screen.getByText('Save'))
await waitFor(() => {
expect(mockOnSubmit).toHaveBeenCalled()
})
expect(portInput).toBeInvalid()
expect(mockOnSubmit).not.toHaveBeenCalled()
})
})
@@ -1142,8 +1101,9 @@ describe('ProxyHostForm', () => {
)
await userEvent.type(screen.getByLabelText(/^Name/), 'Docker Container')
await userEvent.type(screen.getByPlaceholderText('example.com, www.example.com'), 'docker.example.com')
await userEvent.type(screen.getByPlaceholderText('example.com, www.example.com'), 'docker.existing.com')
await userEvent.type(screen.getByLabelText(/^Host/), '172.17.0.2')
await userEvent.clear(screen.getByLabelText(/^Port/))
await userEvent.type(screen.getByLabelText(/^Port/), '8080')
await userEvent.click(screen.getByText('Save'))
@@ -1161,8 +1121,9 @@ describe('ProxyHostForm', () => {
)
await userEvent.type(screen.getByLabelText(/^Name/), 'Localhost')
await userEvent.type(screen.getByPlaceholderText('example.com, www.example.com'), 'localhost.example.com')
await userEvent.type(screen.getByPlaceholderText('example.com, www.example.com'), 'localhost.existing.com')
await userEvent.type(screen.getByLabelText(/^Host/), 'localhost')
await userEvent.clear(screen.getByLabelText(/^Port/))
await userEvent.type(screen.getByLabelText(/^Port/), '3000')
await userEvent.click(screen.getByText('Save'))
@@ -1182,7 +1143,7 @@ describe('ProxyHostForm', () => {
)
await userEvent.type(screen.getByLabelText(/^Name/), 'Enabled Test')
await userEvent.type(screen.getByPlaceholderText('example.com, www.example.com'), 'enabled.example.com')
await userEvent.type(screen.getByPlaceholderText('example.com, www.example.com'), 'enabled.existing.com')
await userEvent.type(screen.getByLabelText(/^Host/), '192.168.1.100')
await userEvent.type(screen.getByLabelText(/^Port/), '80')
@@ -1214,7 +1175,7 @@ describe('ProxyHostForm', () => {
expect(standardHeadersCheckbox).not.toBeChecked()
await userEvent.type(screen.getByLabelText(/^Name/), 'No Headers')
await userEvent.type(screen.getByPlaceholderText('example.com, www.example.com'), 'no-headers.com')
await userEvent.type(screen.getByPlaceholderText('example.com, www.example.com'), 'no-headers.existing.com')
await userEvent.type(screen.getByLabelText(/^Host/), '192.168.1.100')
await userEvent.type(screen.getByLabelText(/^Port/), '80')

View File

@@ -70,18 +70,14 @@ describe('SecurityNotificationSettingsModal', () => {
renderModal();
await waitFor(() => {
expect(screen.getByLabelText('Enable Notifications')).toBeTruthy();
const enableSwitch = screen.getByLabelText('Enable Notifications') as HTMLInputElement;
const levelSelect = screen.getByLabelText(/minimum log level/i) as HTMLSelectElement;
const webhookInput = screen.getByPlaceholderText(/your-webhook-endpoint/i) as HTMLInputElement;
expect(enableSwitch.checked).toBe(true);
expect(levelSelect.value).toBe('warn');
expect(webhookInput.value).toBe('https://example.com/webhook');
});
// Check that settings are loaded
const enableSwitch = screen.getByLabelText('Enable Notifications') as HTMLInputElement;
expect(enableSwitch.checked).toBe(true);
const levelSelect = screen.getByLabelText(/minimum log level/i) as HTMLSelectElement;
expect(levelSelect.value).toBe('warn');
const webhookInput = screen.getByPlaceholderText(/your-webhook-endpoint/i) as HTMLInputElement;
expect(webhookInput.value).toBe('https://example.com/webhook');
});
it('closes modal when close button is clicked', async () => {