diff --git a/components/email/__tests__/composer-a11y.test.tsx b/components/email/__tests__/composer-a11y.test.tsx index e8a87819b..54a96693d 100644 --- a/components/email/__tests__/composer-a11y.test.tsx +++ b/components/email/__tests__/composer-a11y.test.tsx @@ -325,3 +325,24 @@ describe('moving a recipient by keyboard (MO-07)', () => { expect(await screen.findByTestId('recipient-move-bcc')).toBeInTheDocument(); }); }); + +describe('message body (SR-07)', () => { + it('labels the plain-text body', () => { + settings.plainTextMode = true; + render(); + expect(screen.getByLabelText('body_label').tagName).toBe('TEXTAREA'); + }); + + it('marks an empty body invalid and links the error text', async () => { + render(); + const editor = screen.getByTestId('rich-text-editor'); + expect(editor).not.toHaveAttribute('data-invalid'); + + fireEvent.keyDown(screen.getByTestId('composer-subject'), { key: 'Enter', ctrlKey: true }); + await act(async () => {}); + + expect(editor).toHaveAttribute('data-invalid', 'true'); + const error = document.getElementById(editor.getAttribute('data-describedby')!); + expect(error).toHaveTextContent('validation.body_required'); + }); +}); diff --git a/components/email/__tests__/rich-text-editor-a11y.test.tsx b/components/email/__tests__/rich-text-editor-a11y.test.tsx new file mode 100644 index 000000000..2334fbc4e --- /dev/null +++ b/components/email/__tests__/rich-text-editor-a11y.test.tsx @@ -0,0 +1,51 @@ +import { render, waitFor } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; +import React from 'react'; +import type { Editor } from '@tiptap/core'; + +import { RichTextEditor } from '../rich-text-editor'; + +type Props = Partial>; + +async function setup(props: Props = {}) { + let editor: Editor | null = null; + const view = render( + {}} + onEditorReady={(ed) => { editor = ed as unknown as Editor; }} + {...props} + /> + ); + await waitFor(() => expect(editor).not.toBeNull()); + const rerender = (next: Props) => view.rerender( + {}} {...props} {...next} /> + ); + return { ...view, editor: editor as unknown as Editor, rerender }; +} + +describe('RichTextEditor editing area (SR-07)', () => { + it('is a named multi-line textbox', async () => { + const { editor } = await setup({ ariaLabel: 'Message' }); + const dom = editor.view.dom; + expect(dom).toHaveAttribute('role', 'textbox'); + expect(dom).toHaveAttribute('aria-multiline', 'true'); + expect(dom).toHaveAttribute('aria-label', 'Message'); + expect(dom).not.toHaveAttribute('aria-invalid'); + }); + + it('marks the body invalid and points at the error once validation fails', async () => { + const { editor, rerender } = await setup({ ariaLabel: 'Message', ariaDescribedBy: 'body-error' }); + expect(editor.view.dom).not.toHaveAttribute('aria-describedby'); + + rerender({ ariaInvalid: true }); + await waitFor(() => expect(editor.view.dom).toHaveAttribute('aria-invalid', 'true')); + expect(editor.view.dom).toHaveAttribute('aria-describedby', 'body-error'); + // Still the editor's own class and name. + expect(editor.view.dom).toHaveClass('tiptap'); + expect(editor.view.dom).toHaveAttribute('aria-label', 'Message'); + + rerender({ ariaInvalid: false }); + await waitFor(() => expect(editor.view.dom).not.toHaveAttribute('aria-invalid')); + }); +}); diff --git a/components/email/email-composer.tsx b/components/email/email-composer.tsx index 2761df12a..23962448e 100644 --- a/components/email/email-composer.tsx +++ b/components/email/email-composer.tsx @@ -4394,7 +4394,10 @@ export function EmailComposer({ )} {/* Body */} {plainTextMode ? ( + <> +