fix(workflows): replace invalid semantic-version action with fallback script

This commit is contained in:
CI
2025-11-29 01:34:52 +00:00
parent ebd8a8e92b
commit ce8a51e6c7
180 changed files with 9019 additions and 1036 deletions

View File

@@ -1,5 +1,6 @@
import { describe, it, expect, vi, afterEach } from 'vitest'
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import ImportReviewTable from '../ImportReviewTable'
import { mockImportPreview } from '../../test/mockData'
@@ -76,11 +77,11 @@ describe('ImportReviewTable', () => {
/>
)
const dropdown = screen.getByRole('combobox')
fireEvent.change(dropdown, { target: { value: 'overwrite' } })
const dropdown = screen.getByRole('combobox') as HTMLSelectElement
await userEvent.selectOptions(dropdown, 'overwrite')
const commitButton = screen.getByText('Commit Import')
fireEvent.click(commitButton)
await userEvent.click(commitButton)
await waitFor(() => {
expect(mockOnCommit).toHaveBeenCalledWith(
@@ -90,7 +91,7 @@ describe('ImportReviewTable', () => {
})
})
it('calls onCancel when cancel button is clicked', () => {
it('calls onCancel when cancel button is clicked', async () => {
render(
<ImportReviewTable
hosts={mockImportPreview.hosts}
@@ -102,8 +103,8 @@ describe('ImportReviewTable', () => {
/>
)
fireEvent.click(screen.getByText('Back'))
expect(mockOnCancel).toHaveBeenCalledOnce()
await userEvent.click(screen.getByText('Back'))
expect(mockOnCancel).toHaveBeenCalledTimes(1)
})
it('shows conflict indicator on conflicting hosts', () => {
@@ -123,7 +124,7 @@ describe('ImportReviewTable', () => {
expect(screen.queryByText('No conflict')).not.toBeInTheDocument()
})
it('expands and collapses conflict details', () => {
it('expands and collapses conflict details', async () => {
const conflicts = ['test.example.com']
const conflictDetails = {
'test.example.com': {
@@ -161,7 +162,7 @@ describe('ImportReviewTable', () => {
// Find and click expand button (it's the ▶ button)
const expandButton = screen.getByText('▶')
fireEvent.click(expandButton)
await userEvent.click(expandButton)
// Now should show details
expect(screen.getByText('Current Configuration')).toBeInTheDocument()
@@ -171,13 +172,13 @@ describe('ImportReviewTable', () => {
// Click collapse button
const collapseButton = screen.getByText('▼')
fireEvent.click(collapseButton)
await userEvent.click(collapseButton)
// Details should be hidden again
expect(screen.queryByText('Current Configuration')).not.toBeInTheDocument()
})
it('shows recommendation based on configuration differences', () => {
it('shows recommendation based on configuration differences', async () => {
const conflicts = ['test.example.com']
const conflictDetails = {
'test.example.com': {
@@ -212,13 +213,13 @@ describe('ImportReviewTable', () => {
// Expand to see recommendation
const expandButton = screen.getByText('▶')
fireEvent.click(expandButton)
await userEvent.click(expandButton)
// Should show recommendation about config changes (SSL differs)
expect(screen.getByText(/different SSL or WebSocket settings/i)).toBeInTheDocument()
})
it('highlights configuration differences', () => {
it('highlights configuration differences', async () => {
const conflicts = ['test.example.com']
const conflictDetails = {
'test.example.com': {
@@ -252,7 +253,7 @@ describe('ImportReviewTable', () => {
)
const expandButton = screen.getByText('▶')
fireEvent.click(expandButton)
await userEvent.click(expandButton)
// Check for differences being displayed
expect(screen.getByText('https://192.168.1.2:9090')).toBeInTheDocument()

View File

@@ -1,6 +1,7 @@
import { ReactNode } from 'react'
import { describe, it, expect, vi } from 'vitest'
import { render, screen, fireEvent } from '@testing-library/react'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { BrowserRouter } from 'react-router-dom'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import Layout from '../Layout'
@@ -91,20 +92,19 @@ describe('Layout', () => {
expect(await screen.findByText('Version 0.1.0')).toBeInTheDocument()
})
it('calls logout when logout button is clicked', () => {
it('calls logout when logout button is clicked', async () => {
renderWithProviders(
<Layout>
<div>Test Content</div>
</Layout>
)
const logoutButton = screen.getByText('Logout')
fireEvent.click(logoutButton)
await userEvent.click(screen.getByText('Logout'))
expect(mockLogout).toHaveBeenCalled()
})
it('toggles sidebar on mobile', () => {
it('toggles sidebar on mobile', async () => {
renderWithProviders(
<Layout>
<div>Test Content</div>
@@ -113,8 +113,7 @@ describe('Layout', () => {
// Initially sidebar is hidden on mobile (by CSS class, but we can check if the toggle button exists)
// The toggle button has text '☰' when closed
const toggleButton = screen.getByText('☰')
fireEvent.click(toggleButton)
await userEvent.click(screen.getByText('☰'))
// Now it should show '✕'
expect(screen.getByText('✕')).toBeInTheDocument()
@@ -125,7 +124,7 @@ describe('Layout', () => {
// Let's try to click the overlay. It doesn't have text.
// We can query by selector if we add a test id or just rely on structure.
// But let's just click the toggle button again to close.
fireEvent.click(screen.getByText('✕'))
await userEvent.click(screen.getByText('✕'))
expect(screen.getByText('☰')).toBeInTheDocument()
})
})

View File

@@ -1,5 +1,6 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import NotificationCenter from '../NotificationCenter'
import * as api from '../../api/system'
@@ -90,7 +91,7 @@ describe('NotificationCenter', () => {
render(<NotificationCenter />, { wrapper: createWrapper() })
const bellButton = screen.getByRole('button', { name: /notifications/i })
fireEvent.click(bellButton)
await userEvent.click(bellButton)
await waitFor(() => {
expect(screen.getByText('Notifications')).toBeInTheDocument()
@@ -106,7 +107,7 @@ describe('NotificationCenter', () => {
render(<NotificationCenter />, { wrapper: createWrapper() })
const bellButton = screen.getByRole('button', { name: /notifications/i })
fireEvent.click(bellButton)
await userEvent.click(bellButton)
await waitFor(() => {
expect(screen.getByText('No new notifications')).toBeInTheDocument()
@@ -119,14 +120,14 @@ describe('NotificationCenter', () => {
render(<NotificationCenter />, { wrapper: createWrapper() })
fireEvent.click(screen.getByRole('button', { name: /notifications/i }))
await userEvent.click(screen.getByRole('button', { name: /notifications/i }))
await waitFor(() => {
expect(screen.getByText('Info Notification')).toBeInTheDocument()
})
const closeButtons = screen.getAllByRole('button', { name: /close/i })
fireEvent.click(closeButtons[0])
await userEvent.click(closeButtons[0])
await waitFor(() => {
expect(api.markNotificationRead).toHaveBeenCalledWith('1', expect.anything())
@@ -139,13 +140,13 @@ describe('NotificationCenter', () => {
render(<NotificationCenter />, { wrapper: createWrapper() })
fireEvent.click(screen.getByRole('button', { name: /notifications/i }))
await userEvent.click(screen.getByRole('button', { name: /notifications/i }))
await waitFor(() => {
expect(screen.getByText('Mark all read')).toBeInTheDocument()
})
fireEvent.click(screen.getByText('Mark all read'))
await userEvent.click(screen.getByText('Mark all read'))
await waitFor(() => {
expect(api.markAllNotificationsRead).toHaveBeenCalled()
@@ -156,13 +157,13 @@ describe('NotificationCenter', () => {
vi.mocked(api.getNotifications).mockResolvedValue(mockNotifications)
render(<NotificationCenter />, { wrapper: createWrapper() })
fireEvent.click(screen.getByRole('button', { name: /notifications/i }))
await userEvent.click(screen.getByRole('button', { name: /notifications/i }))
await waitFor(() => {
expect(screen.getByText('Notifications')).toBeInTheDocument()
})
fireEvent.click(screen.getByTestId('notification-backdrop'))
await userEvent.click(screen.getByTestId('notification-backdrop'))
await waitFor(() => {
expect(screen.queryByText('Notifications')).not.toBeInTheDocument()

View File

@@ -1,5 +1,7 @@
import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest'
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { act } from 'react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import ProxyHostForm from '../ProxyHostForm'
import { mockRemoteServers } from '../../test/mockData'
@@ -41,7 +43,7 @@ vi.mock('../../hooks/useDomains', () => ({
domains: [
{ uuid: 'domain-1', name: 'existing.com' }
],
createDomain: vi.fn().mockResolvedValue({}),
createDomain: vi.fn().mockResolvedValue({ uuid: 'domain-1', name: 'existing.com' }),
isLoading: false,
error: null,
})),
@@ -50,7 +52,7 @@ vi.mock('../../hooks/useDomains', () => ({
vi.mock('../../hooks/useCertificates', () => ({
useCertificates: vi.fn(() => ({
certificates: [
{ id: 1, name: 'Cert 1', domain: 'example.com', provider: 'custom' }
{ id: 1, name: 'Cert 1', domain: 'example.com', provider: 'custom', issuer: 'Custom', expires_at: '2026-01-01' }
],
isLoading: false,
error: null,
@@ -91,6 +93,12 @@ const renderWithClient = (ui: React.ReactElement) => {
)
}
const renderWithClientAct = async (ui: React.ReactElement) => {
await act(async () => {
renderWithClient(ui)
})
}
import { testProxyHostConnection } from '../../api/proxyHosts'
describe('ProxyHostForm', () => {
@@ -109,7 +117,7 @@ describe('ProxyHostForm', () => {
})
it('handles scheme selection', async () => {
renderWithClient(
await renderWithClientAct(
<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
)
@@ -119,22 +127,22 @@ describe('ProxyHostForm', () => {
// Find scheme select - it defaults to HTTP
// We can find it by label "Scheme"
const schemeSelect = screen.getByLabelText('Scheme')
fireEvent.change(schemeSelect, { target: { value: 'https' } })
const schemeSelect = screen.getByLabelText('Scheme') as HTMLSelectElement
await userEvent.selectOptions(schemeSelect, 'https')
expect(schemeSelect).toHaveValue('https')
})
it('prompts to save new base domain', async () => {
renderWithClient(
await renderWithClientAct(
<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
)
const domainInput = screen.getByPlaceholderText('example.com, www.example.com')
// Enter a subdomain of a new base domain
fireEvent.change(domainInput, { target: { value: 'sub.newdomain.com' } })
fireEvent.blur(domainInput)
await userEvent.type(domainInput, 'sub.newdomain.com')
await userEvent.tab()
await waitFor(() => {
expect(screen.getByText('New Base Domain Detected')).toBeInTheDocument()
@@ -142,7 +150,7 @@ describe('ProxyHostForm', () => {
})
// Click "Yes, save it"
fireEvent.click(screen.getByText('Yes, save it'))
await userEvent.click(screen.getByText('Yes, save it'))
await waitFor(() => {
expect(screen.queryByText('New Base Domain Detected')).not.toBeInTheDocument()
@@ -150,33 +158,33 @@ describe('ProxyHostForm', () => {
})
it('respects "Dont ask me again" for new domains', async () => {
renderWithClient(
await renderWithClientAct(
<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
)
const domainInput = screen.getByPlaceholderText('example.com, www.example.com')
// Trigger prompt
fireEvent.change(domainInput, { target: { value: 'sub.another.com' } })
fireEvent.blur(domainInput)
await userEvent.type(domainInput, 'sub.another.com')
await userEvent.tab()
await waitFor(() => {
expect(screen.getByText('New Base Domain Detected')).toBeInTheDocument()
})
// Check "Don't ask me again"
fireEvent.click(screen.getByLabelText("Don't ask me again"))
await userEvent.click(screen.getByLabelText("Don't ask me again"))
// Click "No, thanks"
fireEvent.click(screen.getByText('No, thanks'))
await userEvent.click(screen.getByText('No, thanks'))
await waitFor(() => {
expect(screen.queryByText('New Base Domain Detected')).not.toBeInTheDocument()
})
// Try another new domain - should not prompt
fireEvent.change(domainInput, { target: { value: 'sub.yetanother.com' } })
fireEvent.blur(domainInput)
await userEvent.type(domainInput, 'sub.yetanother.com')
await userEvent.tab()
// Should not see prompt
expect(screen.queryByText('New Base Domain Detected')).not.toBeInTheDocument()
@@ -185,16 +193,17 @@ describe('ProxyHostForm', () => {
it('tests connection successfully', async () => {
vi.mocked(testProxyHostConnection).mockResolvedValue(undefined)
renderWithClient(
await renderWithClientAct(
<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
)
// Fill required fields for test connection
fireEvent.change(screen.getByLabelText(/^Host$/), { target: { value: '10.0.0.5' } })
fireEvent.change(screen.getByLabelText(/^Port$/), { target: { value: '80' } })
await userEvent.type(screen.getByLabelText(/^Host$/), '10.0.0.5')
await userEvent.clear(screen.getByLabelText(/^Port$/))
await userEvent.type(screen.getByLabelText(/^Port$/), '80')
const testBtn = screen.getByTitle('Test connection to the forward host')
fireEvent.click(testBtn)
await userEvent.click(testBtn)
await waitFor(() => {
expect(testProxyHostConnection).toHaveBeenCalledWith('10.0.0.5', 80)
@@ -204,15 +213,16 @@ describe('ProxyHostForm', () => {
it('handles connection test failure', async () => {
vi.mocked(testProxyHostConnection).mockRejectedValue(new Error('Connection failed'))
renderWithClient(
await renderWithClientAct(
<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
)
fireEvent.change(screen.getByLabelText(/^Host$/), { target: { value: '10.0.0.5' } })
fireEvent.change(screen.getByLabelText(/^Port$/), { target: { value: '80' } })
await userEvent.type(screen.getByLabelText(/^Host$/), '10.0.0.5')
await userEvent.clear(screen.getByLabelText(/^Port$/))
await userEvent.type(screen.getByLabelText(/^Port$/), '80')
const testBtn = screen.getByTitle('Test connection to the forward host')
fireEvent.click(testBtn)
await userEvent.click(testBtn)
await waitFor(() => {
expect(testProxyHostConnection).toHaveBeenCalled()
@@ -226,7 +236,7 @@ describe('ProxyHostForm', () => {
})
it('handles base domain selection', async () => {
renderWithClient(
await renderWithClientAct(
<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
)
@@ -234,14 +244,15 @@ describe('ProxyHostForm', () => {
expect(screen.getByLabelText('Base Domain (Auto-fill)')).toBeInTheDocument()
})
fireEvent.change(screen.getByLabelText('Base Domain (Auto-fill)'), { target: { value: 'existing.com' } })
await userEvent.selectOptions(screen.getByLabelText('Base Domain (Auto-fill)') as HTMLSelectElement, 'existing.com')
// Should not update domain names yet as no container selected
expect(screen.getByLabelText(/Domain Names/i)).toHaveValue('')
// Select container then base domain
fireEvent.change(screen.getByLabelText('Containers'), { target: { value: 'container-123' } })
fireEvent.change(screen.getByLabelText('Base Domain (Auto-fill)'), { target: { value: 'existing.com' } })
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')
expect(screen.getByLabelText(/Domain Names/i)).toHaveValue('my-app.existing.com')
})
@@ -249,7 +260,7 @@ describe('ProxyHostForm', () => {
// Application Preset Tests
describe('Application Presets', () => {
it('renders application preset dropdown with all options', async () => {
renderWithClient(
await renderWithClientAct(
<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
)
@@ -267,7 +278,7 @@ describe('ProxyHostForm', () => {
})
it('defaults to none preset', async () => {
renderWithClient(
await renderWithClientAct(
<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
)
@@ -276,35 +287,35 @@ describe('ProxyHostForm', () => {
})
it('enables websockets when selecting plex preset', async () => {
renderWithClient(
await renderWithClientAct(
<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
)
// First uncheck websockets
const websocketCheckbox = screen.getByLabelText(/Websockets Support/i)
if (websocketCheckbox.getAttribute('checked') !== null) {
fireEvent.click(websocketCheckbox)
await userEvent.click(websocketCheckbox)
}
// Select Plex preset
fireEvent.change(screen.getByLabelText(/Application Preset/i), { target: { value: 'plex' } })
await act(async () => {
await userEvent.selectOptions(screen.getByLabelText(/Application Preset/i) as HTMLSelectElement, 'plex')
})
// Websockets should be enabled
expect(screen.getByLabelText(/Websockets Support/i)).toBeChecked()
})
it('shows plex config helper with external URL when preset is selected', async () => {
renderWithClient(
await renderWithClientAct(
<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
)
// Fill in domain names
fireEvent.change(screen.getByPlaceholderText('example.com, www.example.com'), {
target: { value: 'plex.mydomain.com' }
})
await userEvent.type(screen.getByPlaceholderText('example.com, www.example.com'), 'plex.mydomain.com')
// Select Plex preset
fireEvent.change(screen.getByLabelText(/Application Preset/i), { target: { value: 'plex' } })
await userEvent.selectOptions(screen.getByLabelText(/Application Preset/i) as HTMLSelectElement, 'plex')
// Should show the helper with external URL
await waitFor(() => {
@@ -314,17 +325,17 @@ describe('ProxyHostForm', () => {
})
it('shows jellyfin config helper with internal IP', async () => {
renderWithClient(
await renderWithClientAct(
<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
)
// Fill in domain names
fireEvent.change(screen.getByPlaceholderText('example.com, www.example.com'), {
target: { value: 'jellyfin.mydomain.com' }
})
await userEvent.type(screen.getByPlaceholderText('example.com, www.example.com'), 'jellyfin.mydomain.com')
// Select Jellyfin preset
fireEvent.change(screen.getByLabelText(/Application Preset/i), { target: { value: 'jellyfin' } })
await act(async () => {
await userEvent.selectOptions(screen.getByLabelText(/Application Preset/i) as HTMLSelectElement, 'jellyfin')
})
// Wait for health API fetch and show helper
await waitFor(() => {
@@ -334,17 +345,17 @@ describe('ProxyHostForm', () => {
})
it('shows home assistant config helper with yaml snippet', async () => {
renderWithClient(
await renderWithClientAct(
<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
)
// Fill in domain names
fireEvent.change(screen.getByPlaceholderText('example.com, www.example.com'), {
target: { value: 'ha.mydomain.com' }
})
await userEvent.type(screen.getByPlaceholderText('example.com, www.example.com'), 'ha.mydomain.com')
// Select Home Assistant preset
fireEvent.change(screen.getByLabelText(/Application Preset/i), { target: { value: 'homeassistant' } })
await act(async () => {
await userEvent.selectOptions(screen.getByLabelText(/Application Preset/i) as HTMLSelectElement, 'homeassistant')
})
// Wait for health API fetch and show helper
await waitFor(() => {
@@ -355,17 +366,17 @@ describe('ProxyHostForm', () => {
})
it('shows nextcloud config helper with php snippet', async () => {
renderWithClient(
await renderWithClientAct(
<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
)
// Fill in domain names
fireEvent.change(screen.getByPlaceholderText('example.com, www.example.com'), {
target: { value: 'nextcloud.mydomain.com' }
})
await userEvent.type(screen.getByPlaceholderText('example.com, www.example.com'), 'nextcloud.mydomain.com')
// Select Nextcloud preset
fireEvent.change(screen.getByLabelText(/Application Preset/i), { target: { value: 'nextcloud' } })
await act(async () => {
await userEvent.selectOptions(screen.getByLabelText(/Application Preset/i) as HTMLSelectElement, 'nextcloud')
})
// Wait for health API fetch and show helper
await waitFor(() => {
@@ -376,17 +387,15 @@ describe('ProxyHostForm', () => {
})
it('shows vaultwarden helper text', async () => {
renderWithClient(
await renderWithClientAct(
<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
)
// Fill in domain names
fireEvent.change(screen.getByPlaceholderText('example.com, www.example.com'), {
target: { value: 'vault.mydomain.com' }
})
await userEvent.type(screen.getByPlaceholderText('example.com, www.example.com'), 'vault.mydomain.com')
// Select Vaultwarden preset
fireEvent.change(screen.getByLabelText(/Application Preset/i), { target: { value: 'vaultwarden' } })
await userEvent.selectOptions(screen.getByLabelText(/Application Preset/i) as HTMLSelectElement, 'vaultwarden')
// Wait for helper text
await waitFor(() => {
@@ -422,35 +431,138 @@ describe('ProxyHostForm', () => {
)
// Select local source
fireEvent.change(screen.getByLabelText('Source'), { target: { value: 'local' } })
await userEvent.selectOptions(screen.getByLabelText('Source') as HTMLSelectElement, 'local')
// Select the plex container
await waitFor(() => {
expect(screen.getByText('plex (linuxserver/plex:latest)')).toBeInTheDocument()
})
fireEvent.change(screen.getByLabelText('Containers'), { target: { value: 'plex-container' } })
await userEvent.selectOptions(screen.getByLabelText('Containers') as HTMLSelectElement, 'plex-container')
// The preset should be auto-detected as plex
expect(screen.getByLabelText(/Application Preset/i)).toHaveValue('plex')
})
it('auto-populates advanced_config when selecting plex preset and field empty', async () => {
renderWithClient(
<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
)
// Ensure advanced config is empty
const textarea = screen.getByLabelText(/Advanced Caddy Config/i)
expect(textarea).toHaveValue('')
// Select Plex preset
await act(async () => {
await userEvent.selectOptions(screen.getByLabelText(/Application Preset/i) as HTMLSelectElement, 'plex')
})
// Value should now be populated with a snippet containing X-Real-IP which is used by the preset
await waitFor(() => {
expect((screen.getByLabelText(/Advanced Caddy Config/i) as HTMLTextAreaElement).value).toContain('X-Real-IP')
})
})
it('prompts to confirm overwrite when selecting preset and advanced_config is non-empty', async () => {
const existingHost = {
uuid: 'test-uuid',
name: 'ConfTest',
domain_names: 'test.example.com',
forward_scheme: 'http',
forward_host: '192.168.1.2',
forward_port: 8080,
advanced_config: '{"handler":"headers","request":{"set":{"X-Test":"value"}}}',
advanced_config_backup: '',
ssl_forced: true,
http2_support: true,
hsts_enabled: true,
hsts_subdomains: false,
block_exploits: true,
websocket_support: true,
application: 'none' as const,
locations: [],
enabled: true,
created_at: '2025-01-01',
updated_at: '2025-01-01',
}
renderWithClient(
<ProxyHostForm host={existingHost as any} onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
)
// Select Plex preset (should prompt since advanced_config is non-empty)
await userEvent.selectOptions(screen.getByLabelText(/Application Preset/i) as HTMLSelectElement, 'plex')
await waitFor(() => {
expect(screen.getByText('Confirm Preset Overwrite')).toBeInTheDocument()
})
// Click Overwrite
await userEvent.click(screen.getByText('Overwrite'))
// After overwrite, the textarea should contain the preset 'X-Real-IP' snippet
await waitFor(() => {
expect((screen.getByLabelText(/Advanced Caddy Config/i) as HTMLTextAreaElement).value).toContain('X-Real-IP')
})
})
it('restores previous advanced_config from backup when clicking restore', async () => {
const existingHost = {
uuid: 'test-uuid',
name: 'RestoreTest',
domain_names: 'test.example.com',
forward_scheme: 'http',
forward_host: '192.168.1.2',
forward_port: 8080,
advanced_config: '{"handler":"headers","request":{"set":{"X-Test":"value"}}}',
advanced_config_backup: '{"handler":"headers","request":{"set":{"X-Prev":"backup"}}}',
ssl_forced: true,
http2_support: true,
hsts_enabled: true,
hsts_subdomains: false,
block_exploits: true,
websocket_support: true,
application: 'none' as const,
locations: [],
enabled: true,
created_at: '2025-01-01',
updated_at: '2025-01-01',
}
renderWithClient(
<ProxyHostForm host={existingHost as any} onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
)
// The restore button should be visible
const restoreBtn = await screen.findByText('Restore previous config')
expect(restoreBtn).toBeInTheDocument()
// Click restore and expect the textarea to have backup value
await userEvent.click(restoreBtn)
await waitFor(() => {
expect((screen.getByLabelText(/Advanced Caddy Config/i) as HTMLTextAreaElement).value).toContain('X-Prev')
})
})
it('includes application field in form submission', async () => {
renderWithClient(
<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
)
// Fill required fields
fireEvent.change(screen.getByPlaceholderText('My Service'), { target: { value: 'My Plex Server' } })
fireEvent.change(screen.getByPlaceholderText('example.com, www.example.com'), { target: { value: 'plex.test.com' } })
fireEvent.change(screen.getByLabelText(/^Host$/), { target: { value: '192.168.1.100' } })
fireEvent.change(screen.getByLabelText(/^Port$/), { target: { value: '32400' } })
await userEvent.type(screen.getByPlaceholderText('My Service'), 'My Plex Server')
await userEvent.type(screen.getByPlaceholderText('example.com, www.example.com'), 'plex.test.com')
await userEvent.type(screen.getByLabelText(/^Host$/), '192.168.1.100')
await userEvent.clear(screen.getByLabelText(/^Port$/))
await userEvent.type(screen.getByLabelText(/^Port$/), '32400')
// Select Plex preset
fireEvent.change(screen.getByLabelText(/Application Preset/i), { target: { value: 'plex' } })
await userEvent.selectOptions(screen.getByLabelText(/Application Preset/i) as HTMLSelectElement, 'plex')
// Submit form
fireEvent.click(screen.getByText('Save'))
await userEvent.click(screen.getByText('Save'))
await waitFor(() => {
expect(mockOnSubmit).toHaveBeenCalledWith(
@@ -502,8 +614,8 @@ describe('ProxyHostForm', () => {
)
// Fill in domain names
fireEvent.change(screen.getByPlaceholderText('example.com, www.example.com'), {
target: { value: 'test.mydomain.com' }
await act(async () => {
await userEvent.type(screen.getByPlaceholderText('example.com, www.example.com'), 'test.mydomain.com')
})
// Preset defaults to none, so no helper should be shown
@@ -524,12 +636,10 @@ describe('ProxyHostForm', () => {
)
// Fill in domain names
fireEvent.change(screen.getByPlaceholderText('example.com, www.example.com'), {
target: { value: 'plex.mydomain.com' }
})
await userEvent.type(screen.getByPlaceholderText('example.com, www.example.com'), 'plex.mydomain.com')
// Select Plex preset
fireEvent.change(screen.getByLabelText(/Application Preset/i), { target: { value: 'plex' } })
await userEvent.selectOptions(screen.getByLabelText(/Application Preset/i) as HTMLSelectElement, 'plex')
// Wait for helper to appear
await waitFor(() => {
@@ -538,7 +648,7 @@ describe('ProxyHostForm', () => {
// Click the copy button
const copyButtons = screen.getAllByText('Copy')
fireEvent.click(copyButtons[0])
await userEvent.click(copyButtons[0])
await waitFor(() => {
expect(mockWriteText).toHaveBeenCalledWith('https://plex.mydomain.com:443')

View File

@@ -1,5 +1,6 @@
import { describe, it, expect, vi, afterEach } from 'vitest'
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import RemoteServerForm from '../RemoteServerForm'
import * as remoteServersApi from '../../api/remoteServers'
@@ -76,13 +77,13 @@ describe('RemoteServerForm', () => {
expect(screen.getByText('Test Connection')).toBeInTheDocument()
})
it('calls onCancel when cancel button is clicked', () => {
it('calls onCancel when cancel button is clicked', async () => {
render(
<RemoteServerForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
)
fireEvent.click(screen.getByText('Cancel'))
expect(mockOnCancel).toHaveBeenCalledOnce()
await userEvent.click(screen.getByText('Cancel'))
expect(mockOnCancel).toHaveBeenCalledTimes(1)
})
it('submits form with correct data', async () => {
@@ -94,11 +95,14 @@ describe('RemoteServerForm', () => {
const hostInput = screen.getByPlaceholderText('192.168.1.100')
const portInput = screen.getByDisplayValue('22')
fireEvent.change(nameInput, { target: { value: 'New Server' } })
fireEvent.change(hostInput, { target: { value: '10.0.0.5' } })
fireEvent.change(portInput, { target: { value: '9090' } })
await userEvent.clear(nameInput)
await userEvent.type(nameInput, 'New Server')
await userEvent.clear(hostInput)
await userEvent.type(hostInput, '10.0.0.5')
await userEvent.clear(portInput)
await userEvent.type(portInput, '9090')
fireEvent.click(screen.getByText('Create'))
await userEvent.click(screen.getByText('Create'))
await waitFor(() => {
expect(mockOnSubmit).toHaveBeenCalledWith(
@@ -111,13 +115,13 @@ describe('RemoteServerForm', () => {
})
})
it('handles provider selection', () => {
it('handles provider selection', async () => {
render(
<RemoteServerForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
)
const providerSelect = screen.getByDisplayValue('Generic')
fireEvent.change(providerSelect, { target: { value: 'docker' } })
await userEvent.selectOptions(providerSelect, 'docker')
expect(providerSelect).toHaveValue('docker')
})
@@ -129,10 +133,12 @@ describe('RemoteServerForm', () => {
)
// Fill required fields
fireEvent.change(screen.getByPlaceholderText('My Production Server'), { target: { value: 'Test Server' } })
fireEvent.change(screen.getByPlaceholderText('192.168.1.100'), { target: { value: '10.0.0.1' } })
await userEvent.clear(screen.getByPlaceholderText('My Production Server'))
await userEvent.type(screen.getByPlaceholderText('My Production Server'), 'Test Server')
await userEvent.clear(screen.getByPlaceholderText('192.168.1.100'))
await userEvent.type(screen.getByPlaceholderText('192.168.1.100'), '10.0.0.1')
fireEvent.click(screen.getByText('Create'))
await userEvent.click(screen.getByText('Create'))
await waitFor(() => {
expect(screen.getByText('Submission failed')).toBeInTheDocument()
@@ -157,7 +163,7 @@ describe('RemoteServerForm', () => {
)
const testButton = screen.getByText('Test Connection')
fireEvent.click(testButton)
await userEvent.click(testButton)
await waitFor(() => {
// Check for success state (green background)
@@ -185,7 +191,7 @@ describe('RemoteServerForm', () => {
<RemoteServerForm server={mockServer} onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
)
fireEvent.click(screen.getByText('Test Connection'))
await userEvent.click(screen.getByText('Test Connection'))
await waitFor(() => {
expect(screen.getByText('Connection failed')).toBeInTheDocument()