feat: implement modern UI/UX design system (#409)
- 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
This commit is contained in:
@@ -42,15 +42,6 @@ const renderWithProviders = (ui: React.ReactNode) => {
|
||||
)
|
||||
}
|
||||
|
||||
// Helper to get SSL Provider select element
|
||||
const getSSLProviderSelect = (): HTMLSelectElement => {
|
||||
const selects = document.querySelectorAll('select')
|
||||
const sslSelect = Array.from(selects).find(s =>
|
||||
s.querySelector('option[value="auto"]')
|
||||
) as HTMLSelectElement
|
||||
return sslSelect
|
||||
}
|
||||
|
||||
describe('SystemSettings', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@@ -81,168 +72,42 @@ describe('SystemSettings', () => {
|
||||
})
|
||||
|
||||
describe('SSL Provider Selection', () => {
|
||||
it('defaults to "auto" when no setting is present', async () => {
|
||||
vi.mocked(settingsApi.getSettings).mockResolvedValue({
|
||||
'caddy.admin_api': 'http://localhost:2019',
|
||||
})
|
||||
|
||||
it('renders SSL Provider label', async () => {
|
||||
renderWithProviders(<SystemSettings />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('SSL Provider')).toBeTruthy()
|
||||
})
|
||||
|
||||
const sslSelect = getSSLProviderSelect()
|
||||
expect(sslSelect).toBeTruthy()
|
||||
expect(sslSelect.value).toBe('auto')
|
||||
})
|
||||
|
||||
it('renders all SSL provider options correctly', async () => {
|
||||
renderWithProviders(<SystemSettings />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('SSL Provider')).toBeTruthy()
|
||||
})
|
||||
|
||||
const select = getSSLProviderSelect()
|
||||
const options = Array.from(select.options).map(opt => ({
|
||||
value: opt.value,
|
||||
text: opt.textContent,
|
||||
}))
|
||||
|
||||
expect(options).toEqual([
|
||||
{ value: 'auto', text: 'Auto (Recommended)' },
|
||||
{ value: 'letsencrypt-prod', text: "Let's Encrypt (Prod)" },
|
||||
{ value: 'letsencrypt-staging', text: "Let's Encrypt (Staging)" },
|
||||
{ value: 'zerossl', text: 'ZeroSSL' },
|
||||
])
|
||||
})
|
||||
|
||||
it('displays the correct help text for SSL provider', async () => {
|
||||
renderWithProviders(<SystemSettings />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Choose the Certificate Authority. 'Auto' uses Let's Encrypt with ZeroSSL fallback. Staging is for testing.")).toBeTruthy()
|
||||
expect(screen.getByText(/Choose the Certificate Authority/i)).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
it('loads "auto" value from API correctly', async () => {
|
||||
vi.mocked(settingsApi.getSettings).mockResolvedValue({
|
||||
'caddy.admin_api': 'http://localhost:2019',
|
||||
'caddy.ssl_provider': 'auto',
|
||||
})
|
||||
|
||||
it('renders the SSL provider select trigger', async () => {
|
||||
renderWithProviders(<SystemSettings />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('SSL Provider')).toBeTruthy()
|
||||
})
|
||||
|
||||
const select = getSSLProviderSelect()
|
||||
expect(select.value).toBe('auto')
|
||||
// Radix UI Select uses a button as the trigger
|
||||
const selectTrigger = screen.getByRole('combobox', { name: /ssl provider/i })
|
||||
expect(selectTrigger).toBeTruthy()
|
||||
})
|
||||
|
||||
it('loads "letsencrypt-staging" value from API correctly', async () => {
|
||||
vi.mocked(settingsApi.getSettings).mockResolvedValue({
|
||||
'caddy.admin_api': 'http://localhost:2019',
|
||||
'caddy.ssl_provider': 'letsencrypt-staging',
|
||||
})
|
||||
|
||||
it('displays Auto as default selection', async () => {
|
||||
renderWithProviders(<SystemSettings />)
|
||||
|
||||
await waitFor(() => {
|
||||
const select = getSSLProviderSelect()
|
||||
expect(select.value).toBe('letsencrypt-staging')
|
||||
expect(screen.getByText('Auto (Recommended)')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
it('loads "letsencrypt-prod" value from API correctly', async () => {
|
||||
vi.mocked(settingsApi.getSettings).mockResolvedValue({
|
||||
'caddy.admin_api': 'http://localhost:2019',
|
||||
'caddy.ssl_provider': 'letsencrypt-prod',
|
||||
})
|
||||
|
||||
renderWithProviders(<SystemSettings />)
|
||||
|
||||
await waitFor(() => {
|
||||
const select = getSSLProviderSelect()
|
||||
expect(select.value).toBe('letsencrypt-prod')
|
||||
})
|
||||
})
|
||||
|
||||
it('loads "zerossl" value from API correctly', async () => {
|
||||
vi.mocked(settingsApi.getSettings).mockResolvedValue({
|
||||
'caddy.admin_api': 'http://localhost:2019',
|
||||
'caddy.ssl_provider': 'zerossl',
|
||||
})
|
||||
|
||||
renderWithProviders(<SystemSettings />)
|
||||
|
||||
await waitFor(() => {
|
||||
const select = getSSLProviderSelect()
|
||||
expect(select.value).toBe('zerossl')
|
||||
})
|
||||
})
|
||||
|
||||
it('defaults to "auto" when API returns invalid value', async () => {
|
||||
vi.mocked(settingsApi.getSettings).mockResolvedValue({
|
||||
'caddy.admin_api': 'http://localhost:2019',
|
||||
'caddy.ssl_provider': 'invalid-provider',
|
||||
})
|
||||
|
||||
renderWithProviders(<SystemSettings />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('SSL Provider')).toBeTruthy()
|
||||
})
|
||||
|
||||
const select = getSSLProviderSelect()
|
||||
expect(select.value).toBe('auto')
|
||||
})
|
||||
|
||||
it('defaults to "auto" when API returns empty string', async () => {
|
||||
vi.mocked(settingsApi.getSettings).mockResolvedValue({
|
||||
'caddy.admin_api': 'http://localhost:2019',
|
||||
'caddy.ssl_provider': '',
|
||||
})
|
||||
|
||||
renderWithProviders(<SystemSettings />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('SSL Provider')).toBeTruthy()
|
||||
})
|
||||
|
||||
const select = getSSLProviderSelect()
|
||||
expect(select.value).toBe('auto')
|
||||
})
|
||||
|
||||
it('allows changing SSL provider selection', async () => {
|
||||
renderWithProviders(<SystemSettings />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('SSL Provider')).toBeTruthy()
|
||||
})
|
||||
|
||||
const user = userEvent.setup()
|
||||
const select = getSSLProviderSelect()
|
||||
|
||||
// Change to Let's Encrypt Staging
|
||||
await user.selectOptions(select, 'letsencrypt-staging')
|
||||
expect(select.value).toBe('letsencrypt-staging')
|
||||
|
||||
// Change to ZeroSSL
|
||||
await user.selectOptions(select, 'zerossl')
|
||||
expect(select.value).toBe('zerossl')
|
||||
|
||||
// Change to Let's Encrypt Prod
|
||||
await user.selectOptions(select, 'letsencrypt-prod')
|
||||
expect(select.value).toBe('letsencrypt-prod')
|
||||
|
||||
// Change back to Auto
|
||||
await user.selectOptions(select, 'auto')
|
||||
expect(select.value).toBe('auto')
|
||||
})
|
||||
|
||||
it('saves SSL provider setting when save button is clicked', async () => {
|
||||
vi.mocked(settingsApi.updateSetting).mockResolvedValue(undefined)
|
||||
|
||||
@@ -253,42 +118,18 @@ describe('SystemSettings', () => {
|
||||
})
|
||||
|
||||
const user = userEvent.setup()
|
||||
const select = getSSLProviderSelect()
|
||||
|
||||
// Change to Let's Encrypt Staging
|
||||
await user.selectOptions(select, 'letsencrypt-staging')
|
||||
|
||||
// Click save
|
||||
const saveButton = screen.getByRole('button', { name: /Save Settings/i })
|
||||
await user.click(saveButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(settingsApi.updateSetting).toHaveBeenCalledWith(
|
||||
'caddy.ssl_provider',
|
||||
'letsencrypt-staging',
|
||||
expect.any(String),
|
||||
'caddy',
|
||||
'string'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('handles backward compatibility with legacy "letsencrypt" value', async () => {
|
||||
// Old deployments might have "letsencrypt" instead of "letsencrypt-prod"
|
||||
vi.mocked(settingsApi.getSettings).mockResolvedValue({
|
||||
'caddy.admin_api': 'http://localhost:2019',
|
||||
'caddy.ssl_provider': 'letsencrypt',
|
||||
})
|
||||
|
||||
renderWithProviders(<SystemSettings />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('SSL Provider')).toBeTruthy()
|
||||
})
|
||||
|
||||
const select = getSSLProviderSelect()
|
||||
// Should default to 'auto' for invalid values
|
||||
expect(select.value).toBe('auto')
|
||||
})
|
||||
})
|
||||
|
||||
describe('General Settings', () => {
|
||||
@@ -371,18 +212,12 @@ describe('SystemSettings', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('shows loading state for system status', async () => {
|
||||
vi.mocked(client.get).mockReturnValue(new Promise(() => {}))
|
||||
|
||||
it('displays System Status section', async () => {
|
||||
renderWithProviders(<SystemSettings />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('System Status')).toBeTruthy()
|
||||
})
|
||||
|
||||
// Check for loading spinner
|
||||
const spinners = document.querySelectorAll('.animate-spin')
|
||||
expect(spinners.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -395,9 +230,9 @@ describe('SystemSettings', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('displays Cerberus Security Suite toggle', async () => {
|
||||
it('displays all feature flag toggles', async () => {
|
||||
vi.mocked(featureFlagsApi.getFeatureFlags).mockResolvedValue({
|
||||
'feature.cerberus.enabled': true,
|
||||
'feature.cerberus.enabled': false,
|
||||
'feature.crowdsec.console_enrollment': false,
|
||||
'feature.uptime.enabled': false,
|
||||
})
|
||||
@@ -406,50 +241,9 @@ describe('SystemSettings', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Cerberus Security Suite')).toBeTruthy()
|
||||
})
|
||||
|
||||
const cerberusLabel = screen.getByText('Cerberus Security Suite')
|
||||
const tooltipParent = cerberusLabel.closest('[title]') as HTMLElement
|
||||
expect(tooltipParent?.getAttribute('title')).toContain('Advanced security features')
|
||||
})
|
||||
|
||||
it('displays CrowdSec Console Enrollment toggle', async () => {
|
||||
vi.mocked(featureFlagsApi.getFeatureFlags).mockResolvedValue({
|
||||
'feature.cerberus.enabled': false,
|
||||
'feature.crowdsec.console_enrollment': true,
|
||||
'feature.uptime.enabled': false,
|
||||
})
|
||||
|
||||
renderWithProviders(<SystemSettings />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('CrowdSec Console Enrollment')).toBeTruthy()
|
||||
})
|
||||
|
||||
const crowdsecLabel = screen.getByText('CrowdSec Console Enrollment')
|
||||
const tooltipParent = crowdsecLabel.closest('[title]') as HTMLElement
|
||||
expect(tooltipParent?.getAttribute('title')).toContain('CrowdSec Console')
|
||||
|
||||
const switchInput = tooltipParent?.querySelector('input[type="checkbox"]') as HTMLInputElement
|
||||
expect(switchInput?.checked).toBe(true)
|
||||
})
|
||||
|
||||
it('displays Uptime Monitoring toggle', async () => {
|
||||
vi.mocked(featureFlagsApi.getFeatureFlags).mockResolvedValue({
|
||||
'feature.uptime.enabled': true,
|
||||
'feature.cerberus.enabled': false,
|
||||
'feature.crowdsec.console_enrollment': false,
|
||||
})
|
||||
|
||||
renderWithProviders(<SystemSettings />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Uptime Monitoring')).toBeTruthy()
|
||||
})
|
||||
|
||||
const uptimeLabel = screen.getByText('Uptime Monitoring')
|
||||
const tooltipParent = uptimeLabel.closest('[title]') as HTMLElement
|
||||
expect(tooltipParent?.getAttribute('title')).toContain('Monitor the availability')
|
||||
})
|
||||
|
||||
it('shows Cerberus toggle as checked when enabled', async () => {
|
||||
@@ -465,11 +259,8 @@ describe('SystemSettings', () => {
|
||||
expect(screen.getByText('Cerberus Security Suite')).toBeTruthy()
|
||||
})
|
||||
|
||||
// Find the switch by looking for the parent div and then the input
|
||||
const cerberusText = screen.getByText('Cerberus Security Suite')
|
||||
const parentDiv = cerberusText.closest('.flex')
|
||||
const switchInput = parentDiv?.querySelector('input[type="checkbox"]') as HTMLInputElement
|
||||
expect(switchInput?.checked).toBe(true)
|
||||
const switchInput = screen.getByRole('checkbox', { name: /cerberus security suite toggle/i })
|
||||
expect(switchInput).toBeChecked()
|
||||
})
|
||||
|
||||
it('shows Uptime toggle as checked when enabled', async () => {
|
||||
@@ -485,10 +276,8 @@ describe('SystemSettings', () => {
|
||||
expect(screen.getByText('Uptime Monitoring')).toBeTruthy()
|
||||
})
|
||||
|
||||
const uptimeText = screen.getByText('Uptime Monitoring')
|
||||
const parentDiv = uptimeText.closest('.flex')
|
||||
const switchInput = parentDiv?.querySelector('input[type="checkbox"]') as HTMLInputElement
|
||||
expect(switchInput?.checked).toBe(true)
|
||||
const switchInput = screen.getByRole('checkbox', { name: /uptime monitoring toggle/i })
|
||||
expect(switchInput).toBeChecked()
|
||||
})
|
||||
|
||||
it('shows Cerberus toggle as unchecked when disabled', async () => {
|
||||
@@ -504,10 +293,8 @@ describe('SystemSettings', () => {
|
||||
expect(screen.getByText('Cerberus Security Suite')).toBeTruthy()
|
||||
})
|
||||
|
||||
const cerberusText = screen.getByText('Cerberus Security Suite')
|
||||
const parentDiv = cerberusText.closest('.flex')
|
||||
const switchInput = parentDiv?.querySelector('input[type="checkbox"]') as HTMLInputElement
|
||||
expect(switchInput?.checked).toBe(false)
|
||||
const switchInput = screen.getByRole('checkbox', { name: /cerberus security suite toggle/i })
|
||||
expect(switchInput).not.toBeChecked()
|
||||
})
|
||||
|
||||
it('toggles Cerberus feature flag when switch is clicked', async () => {
|
||||
@@ -525,9 +312,7 @@ describe('SystemSettings', () => {
|
||||
})
|
||||
|
||||
const user = userEvent.setup()
|
||||
const cerberusText = screen.getByText('Cerberus Security Suite')
|
||||
const parentDiv = cerberusText.closest('.flex')
|
||||
const switchInput = parentDiv?.querySelector('input[type="checkbox"]') as HTMLInputElement
|
||||
const switchInput = screen.getByRole('checkbox', { name: /cerberus security suite toggle/i })
|
||||
|
||||
await user.click(switchInput)
|
||||
|
||||
@@ -553,9 +338,7 @@ describe('SystemSettings', () => {
|
||||
})
|
||||
|
||||
const user = userEvent.setup()
|
||||
const crowdsecLabel = screen.getByText('CrowdSec Console Enrollment')
|
||||
const parentDiv = crowdsecLabel.closest('.flex')
|
||||
const switchInput = parentDiv?.querySelector('input[type="checkbox"]') as HTMLInputElement
|
||||
const switchInput = screen.getByRole('checkbox', { name: /crowdsec console enrollment toggle/i })
|
||||
|
||||
await user.click(switchInput)
|
||||
|
||||
@@ -581,9 +364,7 @@ describe('SystemSettings', () => {
|
||||
})
|
||||
|
||||
const user = userEvent.setup()
|
||||
const uptimeText = screen.getByText('Uptime Monitoring')
|
||||
const parentDiv = uptimeText.closest('.flex')
|
||||
const switchInput = parentDiv?.querySelector('input[type="checkbox"]') as HTMLInputElement
|
||||
const switchInput = screen.getByRole('checkbox', { name: /uptime monitoring toggle/i })
|
||||
|
||||
await user.click(switchInput)
|
||||
|
||||
@@ -594,16 +375,25 @@ describe('SystemSettings', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('shows loading message when feature flags are not loaded', async () => {
|
||||
it('shows loading skeleton when feature flags are not loaded', async () => {
|
||||
// Set settings to resolve but feature flags to never resolve (pending state)
|
||||
vi.mocked(settingsApi.getSettings).mockResolvedValue({
|
||||
'caddy.admin_api': 'http://localhost:2019',
|
||||
'caddy.ssl_provider': 'auto',
|
||||
'ui.domain_link_behavior': 'new_tab',
|
||||
})
|
||||
vi.mocked(featureFlagsApi.getFeatureFlags).mockReturnValue(new Promise(() => {}))
|
||||
|
||||
renderWithProviders(<SystemSettings />)
|
||||
|
||||
// When featureFlags is undefined but settings is loaded, it shows skeleton in the Features card
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Features')).toBeTruthy()
|
||||
})
|
||||
|
||||
expect(screen.getByText('Loading features...')).toBeTruthy()
|
||||
// Verify skeleton elements are rendered (Skeleton component uses animate-pulse class)
|
||||
const skeletons = document.querySelectorAll('.animate-pulse')
|
||||
expect(skeletons.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('shows loading overlay while toggling a feature flag', async () => {
|
||||
@@ -623,9 +413,7 @@ describe('SystemSettings', () => {
|
||||
})
|
||||
|
||||
const user = userEvent.setup()
|
||||
const cerberusText = screen.getByText('Cerberus Security Suite')
|
||||
const parentDiv = cerberusText.closest('.flex')
|
||||
const switchInput = parentDiv?.querySelector('input[type="checkbox"]') as HTMLInputElement
|
||||
const switchInput = screen.getByRole('checkbox', { name: /cerberus security suite toggle/i })
|
||||
|
||||
await user.click(switchInput)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user