Files
webmail/lib/__tests__/announce.test.ts
T

102 lines
3.6 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { announce, cancelAnnouncement, ensureLiveRegions, LIVE_REGION_IDS, resetAnnouncerForTests } from '../announce';
const polite = () => document.getElementById(LIVE_REGION_IDS.polite);
const assertive = () => document.getElementById(LIVE_REGION_IDS.assertive);
describe('announce', () => {
beforeEach(() => {
vi.useFakeTimers();
resetAnnouncerForTests();
document.body.innerHTML = '';
});
afterEach(() => {
vi.useRealTimers();
});
it('creates one polite and one assertive region, once', () => {
ensureLiveRegions();
ensureLiveRegions();
expect(document.querySelectorAll(`#${LIVE_REGION_IDS.polite}`)).toHaveLength(1);
expect(polite()).toHaveAttribute('aria-live', 'polite');
expect(polite()).toHaveAttribute('role', 'status');
expect(assertive()).toHaveAttribute('aria-live', 'assertive');
expect(assertive()).toHaveAttribute('role', 'alert');
});
it('writes a message to the polite region after clearing it', () => {
announce('Moved to Archive');
expect(polite()?.textContent).toBe('');
vi.advanceTimersByTime(100);
expect(polite()?.textContent).toBe('Moved to Archive');
expect(assertive()?.textContent).toBe('');
});
it('sends errors to the assertive region', () => {
announce('Could not delete', { assertive: true });
vi.advanceTimersByTime(100);
expect(assertive()?.textContent).toBe('Could not delete');
expect(polite()?.textContent).toBe('');
});
it('does not repeat the same message within a short time, but does later', () => {
announce('Draft saved');
vi.advanceTimersByTime(100);
polite()!.textContent = '';
announce('Draft saved');
vi.advanceTimersByTime(100);
expect(polite()?.textContent).toBe('');
vi.advanceTimersByTime(3000);
announce('Draft saved');
vi.advanceTimersByTime(100);
expect(polite()?.textContent).toBe('Draft saved');
});
it('reads messages that arrive together as one', () => {
announce('Email archived');
announce('3 new messages');
vi.advanceTimersByTime(100);
expect(polite()?.textContent).toBe('Email archived 3 new messages');
});
it('reads only the last debounced message of a burst', () => {
announce('Inbox, 10 messages', { key: 'list', debounceMs: 500 });
vi.advanceTimersByTime(200);
announce('Inbox, 12 messages', { key: 'list', debounceMs: 500 });
vi.advanceTimersByTime(700);
expect(polite()?.textContent).toBe('Inbox, 12 messages');
});
it('drops a cancelled debounced message', () => {
announce('12 results', { key: 'search', debounceMs: 500 });
cancelAnnouncement('search');
vi.advanceTimersByTime(1000);
expect(polite()?.textContent ?? '').toBe('');
});
it('clears old text after a while', () => {
announce('Email deleted');
vi.advanceTimersByTime(100);
expect(polite()?.textContent).toBe('Email deleted');
vi.advanceTimersByTime(8000);
expect(polite()?.textContent).toBe('');
});
});
describe('announce without a page', () => {
it('does nothing when a pending write runs after the document is gone', () => {
vi.useFakeTimers();
resetAnnouncerForTests();
announce('Moved to Archive');
const realDocument = globalThis.document;
// As when a test file's DOM is torn down before the timer fires.
Object.defineProperty(globalThis, 'document', { value: undefined, configurable: true });
try {
expect(() => vi.advanceTimersByTime(1000)).not.toThrow();
} finally {
Object.defineProperty(globalThis, 'document', { value: realDocument, configurable: true });
vi.useRealTimers();
}
});
});