import { describe, expect, it, vi } from 'vitest'; import { readFileSync } from 'node:fs'; import { join } from 'node:path'; vi.mock('@/lib/browser-navigation', () => ({ apiFetch: vi.fn() })); import { adminPolicyPatch, useSettingsStore } from '@/stores/settings-store'; import { ACCESSIBILITY_ALLOWED, ACCESSIBILITY_KEYS, UNLOCKABLE_SETTING_KEYS } from '@/lib/accessibility-settings'; import { RESTRICTION_KEYS, restrictionKeysForPage } from '@/lib/admin/restriction-keys'; import { ACCESSIBILITY_POLICY_SETTINGS, PRESETTABLE_SETTINGS, defaultAt, effectiveDefault, otherDefaultKeys, parseFreeSettingValue, restrictionAt, withDefaultAt, withRestrictionAt, } from '@/lib/admin/policy-settings'; import { DEFAULT_POLICY, type SettingsPolicy } from '@/lib/admin/types'; // The User policy page presets and locks settings with values users' // browsers accept: the same keys, labels and values as the settings store // and the user's own Settings > Accessibility page (AL-11, AL-12). const en = JSON.parse(readFileSync(join(process.cwd(), 'locales/en/common.json'), 'utf-8')); const lookup = (path: string) => path.split('.').reduce((node, key) => (node as Record)?.[key], en); const builtIn = useSettingsStore.getState() as unknown as Record; /** What the store applies for this policy, for a user who never chose the setting. */ function applied(key: string, policy: Partial, current: unknown = Symbol('unset')): unknown { const patch = adminPolicyPatch({ explicitSettings: [], [key]: current }, policy); return Object.prototype.hasOwnProperty.call(patch, key) ? patch[key] : undefined; } describe('the accessibility defaults catalog', () => { it('lists every accessibility option and text size, once each', () => { const keys = ACCESSIBILITY_POLICY_SETTINGS.map((s) => s.key); expect(new Set(keys).size).toBe(keys.length); for (const key of [...ACCESSIBILITY_KEYS, 'fontSize']) expect(keys).toContain(key); expect(ACCESSIBILITY_POLICY_SETTINGS.every((s) => s.category === 'Accessibility')).toBe(true); }); it('uses the labels users see in their settings', () => { for (const setting of ACCESSIBILITY_POLICY_SETTINGS) { expect(setting.labelKey, setting.key).toBeTruthy(); expect(setting.label, setting.key).toBe(lookup(setting.labelKey!)); } }); it('offers exactly the values each option accepts', () => { for (const setting of ACCESSIBILITY_POLICY_SETTINGS) { const allowed = ACCESSIBILITY_ALLOWED[setting.key]; if (!allowed) continue; expect(setting.type, setting.key).toBe('enum'); expect(new Set(setting.options!.map((o) => o.value)), setting.key).toEqual(new Set(allowed)); } }); }); describe('every setting the defaults editor offers', () => { it('has the build default the settings store starts with', () => { for (const setting of PRESETTABLE_SETTINGS) { expect(setting.defaultValue, setting.key).toEqual(builtIn[setting.key]); } }); it('only offers values the store applies as a default', () => { for (const setting of PRESETTABLE_SETTINGS) { for (const option of setting.options ?? []) { expect(applied(setting.key, { defaults: { [setting.key]: option.value } }), `${setting.key}=${String(option.value)}`).toEqual(option.value); } } // And a value it doesn't know is not applied. expect(applied('contrast', { defaults: { contrast: 'max' } })).toBeUndefined(); }); }); describe('the lock list (lib/admin/restriction-keys.ts)', () => { const policyKeys = restrictionKeysForPage('policy'); it('offers no lock for text size, contrast, motion, fonts or reading aids', () => { for (const def of RESTRICTION_KEYS) { const modes = def.modes ?? ['locked', 'hidden']; if (UNLOCKABLE_SETTING_KEYS.has(def.key)) expect(modes, def.key).not.toContain('locked'); } const accessibility = policyKeys.filter((def) => def.category === 'Accessibility').map((def) => def.key); expect(accessibility.sort()).toEqual(['sendConfirmation', 'singleKeyShortcuts', 'toastDuration']); expect(policyKeys.find((def) => def.key === 'toastDuration')?.modes).toEqual(['min']); }); it('has the store\'s defaults and values', () => { for (const def of policyKeys) { expect(def.defaultValue, def.key).toEqual(builtIn[def.key]); for (const value of def.allowedValues ?? []) { expect(applied(def.key, { restrictions: { [def.key]: { locked: true, value } } }), `${def.key}=${value}`).toBe(value); } } }); it('lists the values of the store\'s types', () => { // The store checks only the type of these, so the list has to match // the unions in stores/settings-store.ts by hand. const expected: Record = { density: ['extra-compact', 'compact', 'regular', 'comfortable'], interfaceLayout: ['default', 'mountain-view'], deleteAction: ['trash', 'trash-and-read', 'permanent'], mailLayout: ['split', 'focus', 'horizontal'], externalContentPolicy: ['ask', 'block', 'allow'], defaultReplyMode: ['reply', 'replyAll'], replyIdentityMatch: ['exact', 'domain'], }; for (const def of policyKeys.filter((d) => d.type === 'enum')) { expect(new Set(def.allowedValues), def.key).toEqual(new Set(expected[def.key])); } }); it('a lock with a value sets it for everyone; a minimum raises nothing in the store', () => { expect(applied('singleKeyShortcuts', { restrictions: { singleKeyShortcuts: { locked: true, value: false } } }, true)).toBe(false); expect(applied('toastDuration', { restrictions: { toastDuration: { min: 20 } }, defaults: { toastDuration: 5 } }, 10)).toBeUndefined(); }); }); const policy = (patch: Partial = {}): SettingsPolicy => ({ ...DEFAULT_POLICY, restrictions: {}, defaults: {}, scopes: [{ id: 'team', label: 'Team', match: { groups: ['team@example.org'] }, policy: {} }], ...patch, }); describe('edits at a scope', () => { it('locks for Everyone with a value and drops the value with the lock', () => { let p = withRestrictionAt(policy(), '', 'singleKeyShortcuts', { locked: true, value: false }); expect(p.restrictions.singleKeyShortcuts).toEqual({ locked: true, value: false }); p = withRestrictionAt(p, '', 'sendConfirmation', { hidden: true }); p = withRestrictionAt(p, '', 'sendConfirmation', { hidden: true, locked: true, value: true }); p = withRestrictionAt(p, '', 'sendConfirmation', { ...p.restrictions.sendConfirmation, locked: false }); expect(p.restrictions.sendConfirmation).toEqual({ hidden: true }); p = withRestrictionAt(p, '', 'singleKeyShortcuts', { locked: false }); expect(p.restrictions).not.toHaveProperty('singleKeyShortcuts'); }); it('keeps a minimum without a lock', () => { const p = withRestrictionAt(policy(), '', 'toastDuration', { min: 20 }); expect(p.restrictions.toastDuration).toEqual({ min: 20 }); expect(withRestrictionAt(p, '', 'toastDuration', { min: undefined }).restrictions).not.toHaveProperty('toastDuration'); }); it('writes a scope\'s own lock, unlocks with {} and inherits again', () => { let p = withRestrictionAt(policy(), '', 'singleKeyShortcuts', { locked: true, value: false }); expect(restrictionAt(p, 'team', 'singleKeyShortcuts')).toEqual({ value: { locked: true, value: false }, own: false, everyone: { locked: true, value: false } }); p = withRestrictionAt(p, 'team', 'singleKeyShortcuts', { locked: false, value: false }); expect(p.scopes![0].policy.restrictions).toEqual({ singleKeyShortcuts: {} }); expect(p.restrictions.singleKeyShortcuts).toEqual({ locked: true, value: false }); p = withRestrictionAt(p, 'team', 'singleKeyShortcuts', undefined); expect(p.scopes![0].policy.restrictions).toEqual({}); }); it('sets and removes defaults for Everyone and for a scope', () => { let p = withDefaultAt(policy(), '', 'fontSize', 'large'); expect(p.defaults).toEqual({ fontSize: 'large' }); expect(defaultAt(p, 'team', 'fontSize')).toEqual({ value: 'large', own: false, everyone: 'large' }); p = withDefaultAt(p, 'team', 'fontSize', 'huge'); expect(p.scopes![0].policy.defaults).toEqual({ fontSize: 'huge' }); expect(effectiveDefault(p, 'team', 'fontSize')).toBe('huge'); expect(effectiveDefault(p, '', 'contrast')).toBe('system'); p = withDefaultAt(p, 'team', 'fontSize', undefined); expect(defaultAt(p, 'team', 'fontSize').own).toBe(false); expect(p.defaults).toEqual({ fontSize: 'large' }); }); it('finds presets the editor has no row for, and reads typed values', () => { const p = policy({ defaults: { fontSize: 'large', dateFormat: 'iso' } }); expect(otherDefaultKeys(p, '')).toEqual(['dateFormat']); expect(parseFreeSettingValue('true')).toBe(true); expect(parseFreeSettingValue('12')).toBe(12); expect(parseFreeSettingValue('iso')).toBe('iso'); }); });