From dcc21acd077e44532a39c4d57e29463da3385d2e Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Thu, 29 Oct 2026 16:17:16 +0100 Subject: [PATCH] fix: name the security settings fields and let a settings row's label name its field --- .../__tests__/form-labels-security.test.tsx | 28 ++++++++ .../settings/account-security-settings.tsx | 64 +++++++++++-------- components/settings/settings-section.tsx | 22 ++++++- 3 files changed, 88 insertions(+), 26 deletions(-) create mode 100644 components/__tests__/form-labels-security.test.tsx diff --git a/components/__tests__/form-labels-security.test.tsx b/components/__tests__/form-labels-security.test.tsx new file mode 100644 index 000000000..1ecccdbbf --- /dev/null +++ b/components/__tests__/form-labels-security.test.tsx @@ -0,0 +1,28 @@ +import { act, fireEvent, render, screen } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; +import { unnamedFields } from '@/lib/__tests__/helpers/form-labels'; +import { AccountSecuritySettings } from '@/components/settings/account-security-settings'; +import { useAccountSecurityStore } from '@/stores/account-security-store'; +import { useAuthStore } from '@/stores/auth-store'; + +// Settings > Security: the password, app password, API key and public key +// forms name every field (see form-labels.test.tsx). + +describe('security settings fields have labels', () => { + it('names every field, with each add form open', () => { + useAuthStore.setState({ isAuthenticated: false, authMode: 'basic', client: null } as never); + useAccountSecurityStore.setState({ + isStalwart: true, isProbing: false, otpEnabled: false, isSaving: false, + isLoadingAuth: false, isLoadingPrincipal: false, displayName: 'Ann', + appPasswords: [], apiKeys: [], publicKeys: [], + } as never); + render(); + // Open every add form there is. + for (const name of [/app_passwords\.new/, /api_keys\.new/, /public_keys\.add/]) { + const button = screen.queryAllByRole('button', { name }).at(0); + if (button) act(() => { fireEvent.click(button); }); + } + expect(document.querySelectorAll('input, select, textarea').length).toBeGreaterThan(5); + expect(unnamedFields()).toEqual([]); + }); +}); diff --git a/components/settings/account-security-settings.tsx b/components/settings/account-security-settings.tsx index 9e1e8fbaf..8b59e76ff 100644 --- a/components/settings/account-security-settings.tsx +++ b/components/settings/account-security-settings.tsx @@ -24,8 +24,15 @@ import { ConfirmDialog } from '@/components/ui/confirm-dialog'; import { IconButton } from '@/components/ui/icon-button'; import { useConfirmDialog } from '@/hooks/use-confirm-dialog'; +/** Ids that tie a visible label to its field: labelFor("key"). */ +function useLabelIds() { + const base = useId(); + return (key: string) => `${base}-${key}`; +} + function PasswordChangeSection() { const t = useTranslations('settings.security'); + const labelFor = useLabelIds(); const tLogin = useTranslations('login'); const { changePassword, isSaving, otpEnabled } = useAccountSecurityStore(); const [currentPassword, setCurrentPassword] = useState(''); @@ -78,9 +85,9 @@ function PasswordChangeSection() {
- +
- setCurrentPassword(e.target.value)} @@ -100,8 +107,8 @@ function PasswordChangeSection() {
{otpEnabled && (
- - {t('totp.verification_code')} + setOtpCode(e.target.value)} required @@ -112,9 +119,9 @@ function PasswordChangeSection() {
)}
- +
- setNewPassword(e.target.value)} @@ -134,8 +141,8 @@ function PasswordChangeSection() {
- - {t('password.confirm')} + setConfirmPassword(e.target.value)} @@ -228,6 +235,7 @@ function generateTotp(accountLabel: string, issuer: string): { totp: OTPAuth.TOT function TotpSection() { const t = useTranslations('settings.security'); + const labelFor = useLabelIds(); const { otpEnabled, enableTotp, disableTotp, isSaving, isLoadingAuth } = useAccountSecurityStore(); const { client } = useAuthStore(); const { appName } = useConfig(); @@ -345,12 +353,12 @@ function TotpSection() { {setupUrl}
- - setPassword(e.target.value)} autoComplete="current-password" /> + + setPassword(e.target.value)} autoComplete="current-password" />
- - setOtpCode(e.target.value)} inputMode="numeric" maxLength={6} /> + + setOtpCode(e.target.value)} inputMode="numeric" maxLength={6} />
{setupError &&

{setupError}

}
@@ -371,12 +379,14 @@ function TotpSection() { value={password} onChange={(e) => setPassword(e.target.value)} placeholder={t('password.current')} + aria-label={t('password.current')} autoComplete="current-password" /> setOtpCode(e.target.value)} placeholder={t('totp.verification_code')} + aria-label={t('totp.verification_code')} inputMode="numeric" autoComplete="one-time-code" maxLength={6} @@ -450,6 +460,7 @@ interface CredentialSectionProps { function CredentialSection({ icon: Icon, i18nNamespace, entries, onCreate, onRemove }: CredentialSectionProps) { const t = useTranslations('settings.security'); + const labelFor = useLabelIds(); const tCommon = useTranslations('common'); const tk = (key: string) => t(`${i18nNamespace}.${key}`); const { isSaving, isLoadingAuth } = useAccountSecurityStore(); @@ -555,8 +566,8 @@ function CredentialSection({ icon: Icon, i18nNamespace, entries, onCreate, onRem {showAdd && (
- - {tk('name_label')} + setNewDescription(e.target.value)} placeholder={tk('name_placeholder')} @@ -564,12 +575,12 @@ function CredentialSection({ icon: Icon, i18nNamespace, entries, onCreate, onRem />
- - setExpiresAt(e.target.value)} /> + + setExpiresAt(e.target.value)} />
- -