diff --git a/frontend-modern/src/components/Settings/RecoverySettingsPanel.tsx b/frontend-modern/src/components/Settings/RecoverySettingsPanel.tsx index 1f8fd95aea..06b773940f 100644 --- a/frontend-modern/src/components/Settings/RecoverySettingsPanel.tsx +++ b/frontend-modern/src/components/Settings/RecoverySettingsPanel.tsx @@ -1,4 +1,4 @@ -import { Component, Show, For, Accessor, Setter } from 'solid-js'; +import { Component, Show, For, Accessor, Setter, createUniqueId } from 'solid-js'; import SettingsPanel from '@/components/shared/SettingsPanel'; import { FormSelect } from '@/components/shared/FormSelect'; import { SectionHeader } from '@/components/shared/SectionHeader'; @@ -31,6 +31,8 @@ export interface RecoverySettingsPanelProps { } export const RecoverySettingsPanel: Component = (props) => { + const backupPollingTitleId = `settings-recovery-${createUniqueId()}-backup-polling-title`; + return ( {/* Backup Polling Section */} @@ -57,7 +59,9 @@ export const RecoverySettingsPanel: Component = (pro {/* Enable toggle */}
-

Enable backup polling

+

+ Enable backup polling +

Required for workload backup status, storage snapshots, and alerting.

@@ -68,6 +72,7 @@ export const RecoverySettingsPanel: Component = (pro class="sr-only peer" checked={props.backupPollingEnabled()} disabled={props.backupPollingEnvLocked()} + aria-labelledby={backupPollingTitleId} onChange={(e) => { props.setBackupPollingEnabled(e.currentTarget.checked); if (!props.backupPollingEnvLocked()) { diff --git a/frontend-modern/src/components/Settings/UpdatesSettingsPanel.tsx b/frontend-modern/src/components/Settings/UpdatesSettingsPanel.tsx index aaf4f17098..ad53d846d5 100644 --- a/frontend-modern/src/components/Settings/UpdatesSettingsPanel.tsx +++ b/frontend-modern/src/components/Settings/UpdatesSettingsPanel.tsx @@ -1,4 +1,4 @@ -import { Component, Show, For, Accessor, Setter } from 'solid-js'; +import { Component, Show, For, Accessor, Setter, createUniqueId } from 'solid-js'; import SettingsPanel from '@/components/shared/SettingsPanel'; import { FormSelect } from '@/components/shared/FormSelect'; import { HelpIcon } from '@/components/shared/HelpIcon'; @@ -47,6 +47,7 @@ export interface UpdatesSettingsPanelProps { } export const UpdatesSettingsPanel: Component = (props) => { + const autoUpdateTitleId = `settings-updates-${createUniqueId()}-auto-update-title`; const latestVersion = () => props.updateInfo()?.latestVersion; const dockerImageTag = () => buildDockerImageTag(latestVersion()); const systemdDownloadCommand = () => buildLinuxAmd64DownloadCommand(latestVersion()); @@ -88,11 +89,7 @@ export const UpdatesSettingsPanel: Component = (props })); return ( - +
{/* Version Status Section */} @@ -296,9 +293,9 @@ export const UpdatesSettingsPanel: Component = (props
- +

{UPDATES_PANEL_COPY.autoUpdateDescription}

@@ -312,6 +309,7 @@ export const UpdatesSettingsPanel: Component = (props props.setHasUnsavedChanges(true); }} disabled={autoUpdateLocked()} + aria-labelledby={autoUpdateTitleId} class="sr-only peer" />
diff --git a/frontend-modern/src/components/Settings/__tests__/SettingsSwitchLabels.test.tsx b/frontend-modern/src/components/Settings/__tests__/SettingsSwitchLabels.test.tsx new file mode 100644 index 0000000000..23c1a3b391 --- /dev/null +++ b/frontend-modern/src/components/Settings/__tests__/SettingsSwitchLabels.test.tsx @@ -0,0 +1,77 @@ +import { cleanup, render, screen } from '@solidjs/testing-library'; +import { createSignal } from 'solid-js'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { RecoverySettingsPanel } from '../RecoverySettingsPanel'; +import { UpdatesSettingsPanel } from '../UpdatesSettingsPanel'; + +describe('settings switch labels', () => { + afterEach(() => { + cleanup(); + }); + + it('associates the auto-update switch with its visible label', () => { + const [updateChannel, setUpdateChannel] = createSignal<'stable' | 'rc'>('stable'); + const [autoUpdateEnabled, setAutoUpdateEnabled] = createSignal(false); + const [autoUpdateCheckInterval, setAutoUpdateCheckInterval] = createSignal(24); + const [autoUpdateTime, setAutoUpdateTime] = createSignal('03:00'); + + render(() => ( + null} + updateInfo={() => null} + checkingForUpdates={() => false} + updateChannel={updateChannel} + setUpdateChannel={setUpdateChannel} + autoUpdateEnabled={autoUpdateEnabled} + setAutoUpdateEnabled={setAutoUpdateEnabled} + autoUpdateCheckInterval={autoUpdateCheckInterval} + setAutoUpdateCheckInterval={setAutoUpdateCheckInterval} + autoUpdateTime={autoUpdateTime} + setAutoUpdateTime={setAutoUpdateTime} + checkForUpdates={vi.fn().mockResolvedValue(undefined)} + setHasUnsavedChanges={vi.fn()} + updatePlan={() => null} + onInstallUpdate={vi.fn()} + isInstalling={() => false} + /> + )); + + expect(screen.getByRole('checkbox', { name: 'Automatic Stable Updates' })).toBeInTheDocument(); + }); + + it('associates the backup polling switch with its visible label', () => { + const [backupPollingEnabled, setBackupPollingEnabled] = createSignal(true); + const [backupPollingInterval, setBackupPollingInterval] = createSignal(3600); + const [backupPollingCustomMinutes, setBackupPollingCustomMinutes] = createSignal(60); + const [backupPollingUseCustom, setBackupPollingUseCustom] = createSignal(false); + const [showExportDialog, setShowExportDialog] = createSignal(false); + const [showImportDialog, setShowImportDialog] = createSignal(false); + const [useCustomPassphrase, setUseCustomPassphrase] = createSignal(false); + + render(() => ( + false} + backupIntervalSelectValue={() => '3600'} + backupIntervalSummary={() => 'Every hour'} + setHasUnsavedChanges={vi.fn()} + showExportDialog={showExportDialog} + setShowExportDialog={setShowExportDialog} + showImportDialog={showImportDialog} + setShowImportDialog={setShowImportDialog} + setUseCustomPassphrase={setUseCustomPassphrase} + securityStatus={() => ({ hasAuthentication: true })} + /> + )); + + expect(screen.getByRole('checkbox', { name: 'Enable backup polling' })).toBeChecked(); + expect(useCustomPassphrase()).toBe(false); + }); +}); diff --git a/frontend-modern/src/features/alerts/AlertAppriseDestinationsSection.test.tsx b/frontend-modern/src/features/alerts/AlertAppriseDestinationsSection.test.tsx index 5196d7ba48..c17a4919c2 100644 --- a/frontend-modern/src/features/alerts/AlertAppriseDestinationsSection.test.tsx +++ b/frontend-modern/src/features/alerts/AlertAppriseDestinationsSection.test.tsx @@ -39,6 +39,10 @@ describe('AlertAppriseDestinationsSection', () => { it('associates visible labels with CLI mode fields', () => { renderSection(makeConfig({ mode: 'cli' })); + expect(screen.getByRole('button', { name: 'Apprise notifications Enabled' })).toHaveAttribute( + 'aria-pressed', + 'true', + ); expect(screen.getByRole('combobox', { name: 'Delivery mode' })).toBeInTheDocument(); expect(screen.getByRole('textbox', { name: 'Delivery targets' })).toBeInTheDocument(); expect(screen.getByRole('textbox', { name: 'CLI path' })).toBeInTheDocument(); diff --git a/frontend-modern/src/features/alerts/AlertAppriseDestinationsSection.tsx b/frontend-modern/src/features/alerts/AlertAppriseDestinationsSection.tsx index eafa4d5dd7..1e65b37552 100644 --- a/frontend-modern/src/features/alerts/AlertAppriseDestinationsSection.tsx +++ b/frontend-modern/src/features/alerts/AlertAppriseDestinationsSection.tsx @@ -51,6 +51,7 @@ interface AlertAppriseDestinationsSectionProps { export function AlertAppriseDestinationsSection(props: AlertAppriseDestinationsSectionProps) { const fieldIdPrefix = `alert-apprise-${createUniqueId()}`; const fieldIds = { + title: `${fieldIdPrefix}-title`, targets: `${fieldIdPrefix}-targets`, cliPath: `${fieldIdPrefix}-cli-path`, serverUrl: `${fieldIdPrefix}-server-url`, @@ -63,6 +64,7 @@ export function AlertAppriseDestinationsSection(props: AlertAppriseDestinationsS return ( {getAlertDestinationsStatusLabel(props.config.enabled)} diff --git a/frontend-modern/src/features/alerts/AlertEmailDestinationsSection.test.tsx b/frontend-modern/src/features/alerts/AlertEmailDestinationsSection.test.tsx new file mode 100644 index 0000000000..397eacfdbd --- /dev/null +++ b/frontend-modern/src/features/alerts/AlertEmailDestinationsSection.test.tsx @@ -0,0 +1,53 @@ +import { cleanup, render, screen } from '@solidjs/testing-library'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { AlertEmailDestinationsSection } from './AlertEmailDestinationsSection'; +import type { UIEmailConfig } from './types'; + +vi.mock('@/api/notifications', () => ({ + NotificationsAPI: { + getEmailProviders: vi.fn().mockResolvedValue([]), + }, +})); + +function makeConfig(overrides: Partial = {}): UIEmailConfig { + return { + enabled: false, + provider: '', + server: '', + port: 587, + username: '', + password: '', + from: '', + to: [], + tls: false, + startTLS: true, + replyTo: '', + maxRetries: 3, + retryDelay: 60, + rateLimit: 10, + ...overrides, + }; +} + +describe('AlertEmailDestinationsSection', () => { + afterEach(() => { + cleanup(); + }); + + it('associates the visible panel heading with its status toggle', () => { + render(() => ( + + )); + + expect(screen.getByRole('button', { name: 'Email notifications Disabled' })).toHaveAttribute( + 'aria-pressed', + 'false', + ); + }); +}); diff --git a/frontend-modern/src/features/alerts/AlertEmailDestinationsSection.tsx b/frontend-modern/src/features/alerts/AlertEmailDestinationsSection.tsx index 09a55840b8..b0ef160f86 100644 --- a/frontend-modern/src/features/alerts/AlertEmailDestinationsSection.tsx +++ b/frontend-modern/src/features/alerts/AlertEmailDestinationsSection.tsx @@ -1,3 +1,5 @@ +import { createUniqueId } from 'solid-js'; + import { EmailProviderSelect } from '@/components/Alerts/EmailProviderSelect'; import { SettingsPanel } from '@/components/shared/SettingsPanel'; import { Toggle } from '@/components/shared/Toggle'; @@ -17,8 +19,11 @@ interface AlertEmailDestinationsSectionProps { } export function AlertEmailDestinationsSection(props: AlertEmailDestinationsSectionProps) { + const titleId = `alert-email-destinations-${createUniqueId()}-title`; + return ( {getAlertDestinationsStatusLabel(props.config.enabled)}