mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-05 16:16:49 +00:00
A security and an accessibility review of the proofreading hook found gaps, fixed here. What leaves the browser: - A checker gets a name for each composer of its own, not the composer's id, so it can't reach the message through the other api.composer calls after proofreading stops. - Checking pauses while a plugin that can sign or encrypt is turned on but still starting, and while the policy couldn't be fetched (the built-in defaults could undo an admin's lock). - No plugin can hold up turning proofreading off (onBeforeSettingChange). - Suggestions can't carry zero-width, bidi or line-separator characters; messages lose bidi overrides; a flood of annotations is cut off early. - The notices name the plugin as installed, not a label it chose, and say that text written before signing or encryption was turned on may already have been checked. - A checker registered during start-up no longer fails its first check. Keyboard and screen readers: - Escape in the issue panel closes the panel, not the message. - Underlines carry aria-invalid, as the browser's own markers do. - The focus goes somewhere sensible when a button or an issue goes away, Tab past the popup's edges goes back to the text, and the shortcuts work in the popup and the panel. - Ctrl+' and Ctrl+; follow the character typed, so other layouts keep their own keys (Ctrl+S on Dvorak), with Ä and Ö on German keyboards. - The popup is described by its message, sits inside a modal composer, fits narrow screens, and the panel fits a phone.
70 lines
3.3 KiB
TypeScript
70 lines
3.3 KiB
TypeScript
import { fireEvent, render, screen } from '@testing-library/react';
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { ProofreadingSettings } from '../proofreading-settings';
|
|
import { usePolicyStore } from '@/stores/policy-store';
|
|
import { useSettingsStore } from '@/stores/settings-store';
|
|
import { DEFAULT_POLICY, type SettingsPolicy } from '@/lib/admin/types';
|
|
import { resetCheckers, setPluginChecker } from '@/lib/plugin-sandbox/checkers';
|
|
|
|
// Settings > Composing > Proofreading as the user sees it under each admin choice.
|
|
|
|
vi.mock('../settings-section', async (importOriginal) => ({
|
|
...(await importOriginal<typeof import('../settings-section')>()),
|
|
SettingsSection: ({ children }: { children: React.ReactNode }) => <div data-testid="section">{children}</div>,
|
|
}));
|
|
|
|
const policy = (patch: Partial<SettingsPolicy>) =>
|
|
usePolicyStore.setState({ policy: { ...DEFAULT_POLICY, ...patch }, loaded: true, partial: false, subjectPending: false, source: 'server' });
|
|
|
|
const register = (pluginId: string, pluginName: string) => setPluginChecker(pluginId, pluginName, {
|
|
id: 'check', label: pluginName, kinds: ['spelling'], debounceMs: 800, languages: ['de-DE'], mode: 'typing', warnBeforeSend: false,
|
|
});
|
|
|
|
beforeEach(() => {
|
|
useSettingsStore.getState().resetToDefaults();
|
|
policy({});
|
|
register('lt', 'LanguageTool');
|
|
});
|
|
afterEach(() => resetCheckers());
|
|
|
|
describe('ProofreadingSettings', () => {
|
|
it('lets the user turn it on', () => {
|
|
render(<ProofreadingSettings />);
|
|
const toggle = screen.getByTestId('settings-proofreading');
|
|
expect(toggle.getAttribute('aria-checked')).toBe('false');
|
|
fireEvent.click(toggle);
|
|
expect(useSettingsStore.getState().proofreading).toBe(true);
|
|
});
|
|
|
|
it('shows it on and locked when the organisation requires it, whatever the user stored', () => {
|
|
policy({ restrictions: { proofreading: { locked: true, value: true } } });
|
|
useSettingsStore.setState({ proofreading: false });
|
|
const { container } = render(<ProofreadingSettings />);
|
|
expect(screen.getByTestId('settings-proofreading').getAttribute('aria-checked')).toBe('true');
|
|
expect(container.querySelector('[data-org-locked]')).not.toBeNull();
|
|
});
|
|
|
|
it('hides the switch when the organisation turned it off and hid it', () => {
|
|
policy({ restrictions: { proofreading: { locked: true, value: false, hidden: true } } });
|
|
render(<ProofreadingSettings />);
|
|
expect(screen.queryByTestId('settings-proofreading')).toBeNull();
|
|
// The writing language stays.
|
|
expect(screen.getByText('writing_language')).toBeInTheDocument();
|
|
});
|
|
|
|
it('has a switch per checker plugin when there are several', () => {
|
|
register('ai', 'AI assistant');
|
|
useSettingsStore.setState({ proofreading: true });
|
|
render(<ProofreadingSettings />);
|
|
fireEvent.click(screen.getByTestId('settings-proofreading-ai'));
|
|
expect(useSettingsStore.getState().proofreadingPlugins).toEqual({ ai: false });
|
|
});
|
|
|
|
it('locks the writing language when the organisation set it', () => {
|
|
policy({ restrictions: { writingLanguage: { locked: true, value: 'de-CH' } } });
|
|
const { container } = render(<ProofreadingSettings />);
|
|
expect(container.querySelectorAll('[data-org-locked]')).toHaveLength(1);
|
|
expect(useSettingsStore.getState().writingLanguage).toBe('de-CH');
|
|
});
|
|
});
|