- Add comprehensive design token system (colors, typography, spacing) - Create 12 new UI components with Radix UI primitives - Add layout components (PageShell, StatsCard, EmptyState, DataTable) - Polish all pages with new component library - Improve accessibility with WCAG 2.1 compliance - Add dark mode support with semantic color tokens - Update 947 tests to match new UI patterns Closes #409
182 lines
6.3 KiB
TypeScript
182 lines
6.3 KiB
TypeScript
import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest'
|
|
import { render, screen, waitFor, within } from '@testing-library/react'
|
|
import userEvent from '@testing-library/user-event'
|
|
import { act } from 'react'
|
|
import type { ProxyHost } from '../../api/proxyHosts'
|
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
|
|
// We'll use per-test module mocks via `vi.doMock` and dynamic imports to avoid
|
|
// leaking mocks into other tests. Each test creates its own QueryClient.
|
|
|
|
describe('ProxyHosts page - coverage targets (isolated)', () => {
|
|
beforeEach(() => {
|
|
vi.resetModules()
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers()
|
|
})
|
|
|
|
const renderPage = async () => {
|
|
// Dynamic mocks
|
|
const mockUpdateHost = vi.fn()
|
|
|
|
vi.doMock('react-hot-toast', () => ({ toast: { success: vi.fn(), error: vi.fn(), loading: vi.fn(), dismiss: vi.fn() } }))
|
|
|
|
vi.doMock('../../hooks/useProxyHosts', () => ({
|
|
useProxyHosts: vi.fn(() => ({
|
|
hosts: [
|
|
{
|
|
uuid: 'host-1',
|
|
name: 'StagingHost',
|
|
domain_names: 'staging.example.com',
|
|
forward_scheme: 'http',
|
|
forward_host: '10.0.0.1',
|
|
forward_port: 80,
|
|
ssl_forced: true,
|
|
websocket_support: true,
|
|
certificate: undefined,
|
|
enabled: true,
|
|
created_at: '2025-01-01',
|
|
updated_at: '2025-01-01',
|
|
},
|
|
{
|
|
uuid: 'host-2',
|
|
name: 'CustomCertHost',
|
|
domain_names: 'custom.example.com',
|
|
forward_scheme: 'http',
|
|
forward_host: '10.0.0.2',
|
|
forward_port: 8080,
|
|
ssl_forced: false,
|
|
websocket_support: false,
|
|
certificate: { provider: 'custom', name: 'ACME-CUSTOM' },
|
|
enabled: false,
|
|
created_at: '2025-01-01',
|
|
updated_at: '2025-01-01',
|
|
}
|
|
],
|
|
loading: false,
|
|
isFetching: false,
|
|
error: null,
|
|
createHost: vi.fn(),
|
|
updateHost: (uuid: string, data: Partial<ProxyHost>) => mockUpdateHost(uuid, data),
|
|
deleteHost: vi.fn(),
|
|
bulkUpdateACL: vi.fn(),
|
|
isBulkUpdating: false,
|
|
}))
|
|
}))
|
|
|
|
vi.doMock('../../hooks/useCertificates', () => ({
|
|
useCertificates: vi.fn(() => ({
|
|
certificates: [
|
|
{ id: 1, name: 'StagingCert', domain: 'staging.example.com', status: 'untrusted', provider: 'letsencrypt-staging' }
|
|
],
|
|
isLoading: false,
|
|
error: null,
|
|
}))
|
|
}))
|
|
|
|
vi.doMock('../../hooks/useAccessLists', () => ({ useAccessLists: vi.fn(() => ({ data: [] })) }))
|
|
|
|
vi.doMock('../../api/settings', () => ({ getSettings: vi.fn(() => Promise.resolve({ 'ui.domain_link_behavior': 'new_window' })) }))
|
|
|
|
// Import page after mocks are in place
|
|
const { default: ProxyHosts } = await import('../ProxyHosts')
|
|
|
|
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } })
|
|
const wrapper = (ui: React.ReactNode) => (
|
|
<QueryClientProvider client={qc}>{ui}</QueryClientProvider>
|
|
)
|
|
|
|
return { ProxyHosts, mockUpdateHost, wrapper }
|
|
}
|
|
|
|
it('renders SSL staging badge, websocket badge', async () => {
|
|
const { ProxyHosts } = await renderPage()
|
|
|
|
render(
|
|
<QueryClientProvider client={new QueryClient({ defaultOptions: { queries: { retry: false } } })}>
|
|
<ProxyHosts />
|
|
</QueryClientProvider>
|
|
)
|
|
|
|
await waitFor(() => expect(screen.getByText('StagingHost')).toBeInTheDocument())
|
|
|
|
// Staging badge shows "Staging" text
|
|
expect(screen.getByText('Staging')).toBeInTheDocument()
|
|
// Websocket badge shows "WS"
|
|
expect(screen.getByText('WS')).toBeInTheDocument()
|
|
// Custom cert hosts don't show the cert name in the table - just check the host is shown
|
|
expect(screen.getByText('CustomCertHost')).toBeInTheDocument()
|
|
})
|
|
|
|
it('opens domain link in new window when linkBehavior is new_window', async () => {
|
|
const { ProxyHosts } = await renderPage()
|
|
|
|
const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null)
|
|
|
|
render(
|
|
<QueryClientProvider client={new QueryClient({ defaultOptions: { queries: { retry: false } } })}>
|
|
<ProxyHosts />
|
|
</QueryClientProvider>
|
|
)
|
|
|
|
await waitFor(() => expect(screen.getByText('staging.example.com')).toBeInTheDocument())
|
|
const link = screen.getByText('staging.example.com').closest('a') as HTMLAnchorElement
|
|
await act(async () => {
|
|
await userEvent.click(link!)
|
|
})
|
|
|
|
expect(openSpy).toHaveBeenCalled()
|
|
openSpy.mockRestore()
|
|
})
|
|
|
|
it('bulk apply merges host data and calls updateHost', async () => {
|
|
const { ProxyHosts, mockUpdateHost } = await renderPage()
|
|
|
|
render(
|
|
<QueryClientProvider client={new QueryClient({ defaultOptions: { queries: { retry: false } } })}>
|
|
<ProxyHosts />
|
|
</QueryClientProvider>
|
|
)
|
|
|
|
await waitFor(() => expect(screen.getByText('StagingHost')).toBeInTheDocument())
|
|
|
|
// Select hosts by finding rows and clicking first checkbox (selection)
|
|
const row1 = screen.getByText('StagingHost').closest('tr') as HTMLTableRowElement
|
|
const row2 = screen.getByText('CustomCertHost').closest('tr') as HTMLTableRowElement
|
|
await userEvent.click(within(row1).getAllByRole('checkbox')[0])
|
|
await userEvent.click(within(row2).getAllByRole('checkbox')[0])
|
|
|
|
const bulkBtn = screen.getByText('Bulk Apply')
|
|
await userEvent.click(bulkBtn)
|
|
|
|
// Find the modal dialog
|
|
await waitFor(() => expect(screen.getByText('Bulk Apply Settings')).toBeInTheDocument())
|
|
|
|
// The bulk apply modal has checkboxes for each setting - find them by role
|
|
const modalCheckboxes = screen.getAllByRole('checkbox').filter(
|
|
cb => cb.closest('[role="dialog"]') !== null
|
|
)
|
|
expect(modalCheckboxes.length).toBeGreaterThan(0)
|
|
// Click the first setting checkbox to enable it
|
|
await userEvent.click(modalCheckboxes[0])
|
|
|
|
const applyBtn = screen.getByRole('button', { name: /Apply/ })
|
|
await userEvent.click(applyBtn)
|
|
|
|
await waitFor(() => {
|
|
expect(mockUpdateHost).toHaveBeenCalled()
|
|
})
|
|
|
|
const calls = vi.mocked(mockUpdateHost).mock.calls
|
|
expect(calls.length).toBeGreaterThanOrEqual(1)
|
|
const [calledUuid, calledData] = calls[0]
|
|
expect(typeof calledUuid).toBe('string')
|
|
expect(Object.prototype.hasOwnProperty.call(calledData, 'ssl_forced')).toBe(true)
|
|
})
|
|
})
|
|
|
|
export {}
|