diff --git a/src/components/settings/AccessibilitySettings.tsx b/src/components/settings/AccessibilitySettings.tsx index 65fc629c..e62891ea 100644 --- a/src/components/settings/AccessibilitySettings.tsx +++ b/src/components/settings/AccessibilitySettings.tsx @@ -5,7 +5,7 @@ import Button from '../Button'; import { spacing } from '../../theme/tokens'; import { useLocaleStore } from '../../stores/locale-store'; import { usePolicyStore, useSettingHidden, useSettingLocked } from '../../stores/policy-store'; -import { policyAllowsValue } from '../../lib/org-policy'; +import { isLockedSetting, policyAllowsValue } from '../../lib/org-policy'; import { APP_FONTS, FONT_SIZES, useSettingsStore, type AppFont, type FontSize } from '../../stores/settings-store'; import { uiFontsBundled } from '../../theme/reading-fonts'; import { toast } from '../../stores/toast-store'; @@ -104,7 +104,15 @@ export function AccessibilitySettings() { const applyPreset = (id: AccessibilityPresetId) => { const name = presetName(id); - const changes = presetChanges(id, useSettingsStore.getState()); + // Settings the organisation pins or bounds stay as they are (webmail + // accessibility-settings.tsx isSettingLocked, allowsValue). + const rules = usePolicyStore.getState().rules; + const changes = presetChanges( + id, + useSettingsStore.getState(), + (key) => isLockedSetting(rules, key), + (key, value) => policyAllowsValue(key, value, rules), + ); if (changes.length === 0) { toast.info(t('settings.accessibility.presets.nothing_changed', '{name}: your settings already match.', { name })); return; diff --git a/src/lib/__tests__/accessibility-presets-policy.test.ts b/src/lib/__tests__/accessibility-presets-policy.test.ts new file mode 100644 index 00000000..37ff5e86 --- /dev/null +++ b/src/lib/__tests__/accessibility-presets-policy.test.ts @@ -0,0 +1,43 @@ +import { describe, it, expect } from 'vitest'; +import fs from 'node:fs'; +import path from 'node:path'; +import { presetChanges } from '../accessibility-presets'; +import { EMPTY_SETTINGS_RULES, isLockedSetting, policyAllowsValue, type SettingsRules } from '../org-policy'; + +// Presets leave alone what the organisation pins or bounds (webmail +// lib/accessibility-presets.ts presetChanges with isSettingLocked and +// allowsValue). The app's presetChanges changed locked settings too. + +const read = (rel: string) => fs.readFileSync(path.resolve(__dirname, '..', '..', rel), 'utf8'); + +describe('accessibility presets and the organisation policy', () => { + const state = { fontSize: 'medium' as const, showToolbarLabels: false }; + + it('skips a setting the organisation locked', () => { + expect(presetChanges('low-vision', state, (key) => key === 'fontSize')) + .toEqual([{ key: 'showToolbarLabels', from: false, to: true }]); + }); + + it('skips a value the organisation does not allow', () => { + const rules: SettingsRules = { + ...EMPTY_SETTINGS_RULES, + restrictions: { fontSize: { allowedValues: ['medium', 'large'] } }, + }; + const changes = presetChanges( + 'low-vision', + state, + (key) => isLockedSetting(rules, key), + (key, value) => policyAllowsValue(key, value, rules), + ); + expect(changes).toEqual([{ key: 'showToolbarLabels', from: false, to: true }]); + }); + + it('changes everything without a policy', () => { + expect(presetChanges('low-vision', state).map((c) => c.key)).toEqual(['fontSize', 'showToolbarLabels']); + }); + + it('passes the locks and bounds from Settings > Accessibility', () => { + const src = read('components/settings/AccessibilitySettings.tsx'); + expect(src).toMatch(/presetChanges\(\s+id,\s+useSettingsStore\.getState\(\),\s+\(key\) => isLockedSetting\(rules, key\),\s+\(key, value\) => policyAllowsValue\(key, value, rules\),\s+\)/); + }); +}); diff --git a/src/lib/accessibility-presets.ts b/src/lib/accessibility-presets.ts index 7303d020..79ae2cca 100644 --- a/src/lib/accessibility-presets.ts +++ b/src/lib/accessibility-presets.ts @@ -45,11 +45,20 @@ export interface PresetChange { to: unknown; } -/** The changes a preset would make to `state`: only values that differ. */ -export function presetChanges(id: AccessibilityPresetId, state: Partial): PresetChange[] { +/** + * The changes a preset would make to `state`: only values that differ, and + * none the organisation has locked (`isLocked`) or bounded (`allows`, such + * as a minimum toast duration). + */ +export function presetChanges( + id: AccessibilityPresetId, + state: Partial, + isLocked: (key: string) => boolean = () => false, + allows: (key: string, value: unknown) => boolean = () => true, +): PresetChange[] { const changes: PresetChange[] = []; for (const [key, to] of Object.entries(ACCESSIBILITY_PRESETS[id]) as Array<[keyof PersistedSettings, unknown]>) { - if (!(key in state) || state[key] === to) continue; + if (!(key in state) || state[key] === to || isLocked(key) || !allows(key, to)) continue; changes.push({ key, from: state[key], to }); } return changes;