Skip to content

Commit

Permalink
Update tests.
Browse files Browse the repository at this point in the history
  • Loading branch information
avinashbot committed Apr 25, 2024
1 parent e2eb76f commit c5423c1
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 44 deletions.
71 changes: 46 additions & 25 deletions src/internal/components/live-region/__tests__/live-region.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,61 +2,73 @@
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
import { render, waitFor } from '@testing-library/react';
import createWrapper from '../../../../../lib/components/test-utils/dom';
import LiveRegion from '../../../../../lib/components/internal/components/live-region';
import LiveRegion, { polite, assertive } from '../../../../../lib/components/internal/components/live-region';
import { mockInnerText } from '../../../../internal/analytics/__tests__/mocks';
import styles from '../../../../../lib/components/internal/components/live-region/styles.css.js';

mockInnerText();

const renderLiveRegion = async (jsx: React.ReactElement) => {
const { container } = render(jsx);
const wrapper = createWrapper(container);

await waitFor(() => expect(wrapper.find('[aria-live]')!.getElement()).not.toBeEmptyDOMElement());
await waitFor(() => expect(document.querySelector('[aria-live=polite]')));

return {
wrapper,
container,
visibleSource: wrapper.find(':first-child')?.getElement(),
hiddenSource: wrapper.find('[aria-hidden=true]')?.getElement(),
liveRegion: wrapper.find('[aria-live]')!.getElement(),
visibleSource: container.querySelector(`.${styles.root}`),
hiddenSource: container.querySelector('[hidden]'),
politeRegion: document.querySelector('[aria-live=polite]')!,
assertiveRegion: document.querySelector('[aria-live=assertive]')!,
};
};

// The announcers persist throughout the lifecycle of the application.
// We need to reset them after each test.
afterEach(() => {
polite.reset();
assertive.reset();
});

describe('LiveRegion', () => {
it('renders', async () => {
const { hiddenSource, liveRegion } = await renderLiveRegion(<LiveRegion>Announcement</LiveRegion>);
const { hiddenSource, politeRegion } = await renderLiveRegion(<LiveRegion delay={0}>Announcement</LiveRegion>);

expect(hiddenSource).toHaveTextContent('Announcement');
expect(liveRegion).toHaveAttribute('aria-live', 'polite');
expect(liveRegion).toHaveAttribute('aria-atomic', 'true');
expect(liveRegion).toHaveTextContent('Announcement');
expect(politeRegion).toHaveAttribute('aria-live', 'polite');
expect(politeRegion).toHaveAttribute('aria-atomic', 'true');
expect(politeRegion).toHaveTextContent('Announcement');
});

it('renders with a span by default', async () => {
const { hiddenSource, liveRegion } = await renderLiveRegion(<LiveRegion>Announcement</LiveRegion>);
const { hiddenSource, politeRegion } = await renderLiveRegion(<LiveRegion delay={0}>Announcement</LiveRegion>);

expect(hiddenSource!.tagName).toBe('SPAN');
expect(liveRegion).toHaveTextContent('Announcement');
expect(politeRegion).toHaveTextContent('Announcement');
});

it('wraps visible content in a span by default', async () => {
const { visibleSource } = await renderLiveRegion(<LiveRegion visible={true}>Announcement</LiveRegion>);
const { visibleSource } = await renderLiveRegion(
<LiveRegion delay={0} visible={true}>
Announcement
</LiveRegion>
);

expect(visibleSource!.tagName).toBe('SPAN');
expect(visibleSource).toHaveTextContent('Announcement');
});

it('can render with a div', async () => {
const { hiddenSource } = await renderLiveRegion(<LiveRegion tagName="div">Announcement</LiveRegion>);
const { hiddenSource } = await renderLiveRegion(
<LiveRegion delay={0} tagName="div">
Announcement
</LiveRegion>
);

expect(hiddenSource!.tagName).toBe('DIV');
expect(hiddenSource).toHaveTextContent('Announcement');
});

it('can wrap visible content in a div', async () => {
const { visibleSource } = await renderLiveRegion(
<LiveRegion tagName="div" visible={true}>
<LiveRegion delay={0} tagName="div" visible={true}>
Announcement
</LiveRegion>
);
Expand All @@ -65,19 +77,28 @@ describe('LiveRegion', () => {
});

it('can render assertive live region', async () => {
const { liveRegion } = await renderLiveRegion(<LiveRegion assertive={true}>Announcement</LiveRegion>);
expect(liveRegion).toHaveAttribute('aria-live', 'assertive');
const { politeRegion, assertiveRegion } = await renderLiveRegion(
<LiveRegion delay={0} assertive={true}>
Announcement
</LiveRegion>
);
console.log({ assertiveRegion, politeRegion });
expect(assertiveRegion).toHaveAttribute('aria-live', 'assertive');
expect(assertiveRegion).toHaveTextContent('Announcement');
expect(politeRegion).toBeEmptyDOMElement();
});

it('uses the `source` parameter if provided', async () => {
const ref = { current: null };
const { liveRegion } = await renderLiveRegion(
const { politeRegion } = await renderLiveRegion(
<>
<LiveRegion source={['static text', ref, undefined, 'more static text']}>Announcement</LiveRegion>
<LiveRegion delay={0} source={['static text', ref, undefined, 'more static text']}>
Announcement
</LiveRegion>
<span ref={ref}>Element text</span>
</>
);
expect(liveRegion).toHaveTextContent('static text Element text more static text');
expect(liveRegion).not.toHaveTextContent('Announcement');
expect(politeRegion).toHaveTextContent('static text Element text more static text');
expect(politeRegion).not.toHaveTextContent('Announcement');
});
});
33 changes: 18 additions & 15 deletions src/internal/components/live-region/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,25 @@ import styles from './styles.css.js';
class LiveRegionController {
private _element: HTMLElement | undefined;
private _timeoutId: number | undefined;
private _delay: number;
private _delay = 0;
private _lastAnnouncement = '';
private readonly _nextMessages = new Set<string>();

constructor(public readonly politeness: 'polite' | 'assertive', public readonly defaultDelay: number = 50) {
this._delay = defaultDelay;
}
constructor(public readonly politeness: 'polite' | 'assertive') {}

initialize() {
if (!this._element) {
this._element = document.createElement('div');
this._element.className = styles.announcer;
this._element.ariaLive = this.politeness;
this._element.ariaAtomic = 'true';

// Doesn't serve a technical purpose, just helps to track this element in the DOM.
this._element.dataset.awsuiLiveAnnouncer = 'true';
this._element.setAttribute('aria-live', this.politeness);
this._element.setAttribute('aria-atomic', 'true');
this._element.setAttribute('data-awsui-live-announcer', 'true');

document.body.appendChild(this._element);
}
}

announce(message: string, minDelay?: number) {
announce(message: string, minDelay = 50) {
this._nextMessages.add(message);

// A message was added with a longer delay, so we delay the whole announcement.
Expand All @@ -39,15 +35,22 @@ class LiveRegionController {
this._timeoutId = undefined;
}

if (this._timeoutId === undefined) {
if (this._delay === 0 && minDelay === 0) {
// If the delay is 0, just skip the timeout shenanigans and update the
// element synchronously. Great for tests.
this._updateElement();
} else if (this._timeoutId === undefined) {
this._timeoutId = setTimeout(() => this._updateElement(), this._delay);
}
}

destroy() {
this._element?.remove();
reset() {
if (this._element) {
this._element.textContent = '';
}
if (this._timeoutId) {
clearTimeout(this._timeoutId);
this._timeoutId = undefined;

Check warning on line 53 in src/internal/components/live-region/controller.ts

View check run for this annotation

Codecov / codecov/patch

src/internal/components/live-region/controller.ts#L52-L53

Added lines #L52 - L53 were not covered by tests
}
}

Expand All @@ -67,12 +70,12 @@ class LiveRegionController {
// The aria-atomic does not work properly in Voice Over, causing
// certain parts of the content to be ignored. To fix that,
// we assign the source text content as a single node.
this._element.innerText = nextAnnouncement;
this._element.textContent = nextAnnouncement;
this._lastAnnouncement = nextAnnouncement;

// Reset the state for the next announcement.
this._timeoutId = undefined;
this._delay = this.defaultDelay;
this._delay = 0;
this._nextMessages.clear();
}
}
Expand Down
10 changes: 6 additions & 4 deletions src/internal/components/live-region/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import { BaseComponentProps } from '../../base-component';
import { polite, assertive } from './controller';

import styles from './styles.css.js';
import ScreenreaderOnly from '../screenreader-only';

// Export announcers for components that want to imperatively announce content.
export { polite, assertive };

export interface LiveRegionProps extends BaseComponentProps {
/**
Expand Down Expand Up @@ -112,17 +114,17 @@ export default function LiveRegion({
if (content && content !== previousSourceContentRef.current) {
const announcer = isAssertive ? assertive : polite;
announcer.announce(content, delay);
previousSourceContentRef.current = content;
}
previousSourceContentRef.current = content;
});

if (source) {
return null;
}

return (
<TagName ref={sourceRef} className={clsx(styles.root, className)} {...restProps}>
{visible ? children : <ScreenreaderOnly>{children}</ScreenreaderOnly>}
<TagName ref={sourceRef} className={clsx(styles.root, className)} hidden={!visible} {...restProps}>
{children}
</TagName>
);
}
Expand Down

0 comments on commit c5423c1

Please sign in to comment.