mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-01 00:13:37 +00:00
85 lines
3.2 KiB
TypeScript
85 lines
3.2 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
import { act, fireEvent, render, screen } from '@testing-library/react';
|
|
import { NextIntlClientProvider } from 'next-intl';
|
|
import { GLOSSARY_IDS, Term } from '../term';
|
|
import en from '@/locales/en/common.json';
|
|
|
|
// Jargon explanations (CO-14) must not depend on hovering: the info button is
|
|
// in the tab order, opens with the keyboard or a tap, and screen readers get
|
|
// the explanation as its description without opening it.
|
|
|
|
vi.unmock('next-intl');
|
|
|
|
const imap = en.glossary.terms.imap;
|
|
|
|
function renderTerm(onRowClick = () => {}) {
|
|
return render(
|
|
<NextIntlClientProvider locale="en" messages={en}>
|
|
<button type="button">Before</button>
|
|
<p onClick={onRowClick}>
|
|
Use <Term id="imap">IMAP</Term> to read mail in other apps.
|
|
</p>
|
|
</NextIntlClientProvider>,
|
|
);
|
|
}
|
|
|
|
const infoButton = () => screen.getByRole('button', { name: 'What does “IMAP” mean?' });
|
|
|
|
describe('Term', () => {
|
|
it('keeps the term in the sentence and adds a named info button', () => {
|
|
const { container } = renderTerm();
|
|
expect(container.querySelector('p')?.textContent).toContain('Use IMAP');
|
|
expect(infoButton()).toHaveAttribute('type', 'button');
|
|
});
|
|
|
|
it('is reachable with the keyboard', () => {
|
|
renderTerm();
|
|
const before = screen.getByRole('button', { name: 'Before' });
|
|
before.focus();
|
|
// The info button is a real, focusable button in document order.
|
|
expect(infoButton().tabIndex).toBe(0);
|
|
expect(before.compareDocumentPosition(infoButton()) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
|
infoButton().focus();
|
|
expect(infoButton()).toHaveFocus();
|
|
});
|
|
|
|
it('gives screen readers the explanation as the description while closed', () => {
|
|
renderTerm();
|
|
expect(infoButton()).toHaveAttribute('aria-expanded', 'false');
|
|
expect(infoButton()).toHaveAccessibleDescription(imap.text);
|
|
expect(screen.queryByText(imap.text)).not.toBeVisible();
|
|
});
|
|
|
|
it('opens on activation and closes with Escape, returning focus', () => {
|
|
const onRowClick = vi.fn();
|
|
renderTerm(onRowClick);
|
|
const button = infoButton();
|
|
button.focus();
|
|
fireEvent.click(button);
|
|
expect(button).toHaveAttribute('aria-expanded', 'true');
|
|
expect(screen.getByText(imap.text)).toBeVisible();
|
|
expect(document.getElementById(button.getAttribute('aria-controls')!)).toHaveTextContent(imap.text);
|
|
// The surrounding row doesn't react to the tap.
|
|
expect(onRowClick).not.toHaveBeenCalled();
|
|
|
|
fireEvent.keyDown(screen.getByText(imap.text), { key: 'Escape' });
|
|
expect(button).toHaveAttribute('aria-expanded', 'false');
|
|
expect(screen.queryByText(imap.text)).not.toBeVisible();
|
|
expect(button).toHaveFocus();
|
|
});
|
|
|
|
it('closes on a tap outside', () => {
|
|
renderTerm();
|
|
fireEvent.click(infoButton());
|
|
expect(screen.getByText(imap.text)).toBeVisible();
|
|
act(() => {
|
|
document.body.dispatchEvent(new Event('pointerdown', { bubbles: true }));
|
|
});
|
|
expect(screen.queryByText(imap.text)).not.toBeVisible();
|
|
});
|
|
|
|
it('knows exactly the terms the glossary explains', () => {
|
|
expect([...GLOSSARY_IDS].sort()).toEqual(Object.keys(en.glossary.terms).sort());
|
|
});
|
|
});
|