Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions packages/fuselage/src/components/Message/Message.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { composeStories } from '@storybook/react-webpack5';
import { axe } from 'jest-axe';

import { render } from '../../testing';

Expand All @@ -10,4 +11,69 @@ describe('[Message Component]', () => {
it('renders without crashing', () => {
render(<Default />);
});

describe('Accessibility', () => {
it('should have no a11y violations', async () => {
const { container } = render(<Default />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});

it('should have role="article" on message container', () => {
const { container } = render(<Default />);
const message = container.querySelector('.rcx-message');
expect(message).toHaveAttribute('role', 'article');
});

it('should have aria-label on message container', () => {
const { container } = render(<Default />);
const message = container.querySelector('.rcx-message');
expect(message).toHaveAttribute('aria-label');
});

it('should be keyboard accessible when clickable', () => {
const onClick = jest.fn();
const { container } = render(
<Default onClick={onClick} clickable />
);
const message = container.querySelector('.rcx-message');
expect(message).toHaveAttribute('tabIndex', '0');
});

it('should indicate pending state with aria-busy', () => {
const { container } = render(<Default isPending />);
const message = container.querySelector('.rcx-message');
expect(message).toHaveAttribute('aria-busy', 'true');
});

it('should indicate selected state with aria-selected', () => {
const { container } = render(<Default isSelected />);
const message = container.querySelector('.rcx-message');
expect(message).toHaveAttribute('aria-selected', 'true');
});
});

describe('Keyboard Navigation', () => {
it('should be focusable when clickable', () => {
const { container } = render(<Default clickable />);
const message = container.querySelector('.rcx-message');
expect(message).toHaveAttribute('tabIndex', '0');
});

it('should not be focusable when not clickable', () => {
const { container } = render(<Default />);
const message = container.querySelector('.rcx-message');
expect(message).not.toHaveAttribute('tabIndex');
});
});

describe('ARIA Attributes', () => {
it('should support custom aria-label', () => {
const { container } = render(
<Default aria-label="Custom message label" />
);
const message = container.querySelector('.rcx-message');
expect(message).toHaveAttribute('aria-label', 'Custom message label');
});
});
});
7 changes: 7 additions & 0 deletions packages/fuselage/src/components/Message/Message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export type MessageProps = AllHTMLAttributes<HTMLDivElement> & {
isEditing?: boolean;
isPending?: boolean;
highlight?: boolean;
'aria-label'?: string;
};

const Message = Object.assign(
Expand All @@ -40,13 +41,19 @@ const Message = Object.assign(
isEditing,
isPending,
highlight,
'aria-label': ariaLabel,
...props
},
ref,
) {
return (
<div
ref={ref}
role="article"
aria-label={ariaLabel || 'Message'}
aria-busy={isPending}
aria-selected={isSelected}
tabIndex={clickable || props.onClick ? 0 : undefined}
className={prependClassName(
className,
[
Expand Down
60 changes: 60 additions & 0 deletions packages/fuselage/src/components/Message/MessageBody.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { axe } from 'jest-axe';

import { render } from '../../testing';

import MessageBody from './MessageBody';

describe('[MessageBody Component]', () => {
it('renders without crashing', () => {
render(<MessageBody>Test content</MessageBody>);
});

describe('Accessibility', () => {
it('should have no a11y violations', async () => {
const { container } = render(
<MessageBody>Test message content</MessageBody>
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});

it('should have role="region"', () => {
const { container } = render(
<MessageBody>Test content</MessageBody>
);
const body = container.querySelector('.rcx-message-body');
expect(body).toHaveAttribute('role', 'region');
});

it('should have aria-label', () => {
const { container } = render(
<MessageBody>Test content</MessageBody>
);
const body = container.querySelector('.rcx-message-body');
expect(body).toHaveAttribute('aria-label', 'Message content');
});

it('should apply clamp class when clamp prop is provided', () => {
const { container } = render(
<MessageBody clamp={2}>Test content</MessageBody>
);
const body = container.querySelector('.rcx-message-body');
expect(body).toHaveClass('rcx-message-body--clamp');
expect(body).toHaveClass('rcx-message-body--clamp-2');
});

it('should handle different clamp values', () => {
const { container: container2 } = render(
<MessageBody clamp={3}>Test content</MessageBody>
);
const body2 = container2.querySelector('.rcx-message-body');
expect(body2).toHaveClass('rcx-message-body--clamp-3');

const { container: container3 } = render(
<MessageBody clamp={4}>Test content</MessageBody>
);
const body3 = container3.querySelector('.rcx-message-body');
expect(body3).toHaveClass('rcx-message-body--clamp-4');
});
});
});
2 changes: 2 additions & 0 deletions packages/fuselage/src/components/Message/MessageBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export type MessageBodyProps = HTMLAttributes<HTMLDivElement> & {

const MessageBody = ({ clamp, className, ...props }: MessageBodyProps) => (
<div
role="region"
aria-label="Message content"
className={
prependClassName(
className,
Expand Down
48 changes: 48 additions & 0 deletions packages/fuselage/src/components/Message/MessageHeader.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { axe } from 'jest-axe';

import { render } from '../../testing';

import MessageHeader from './MessageHeader';

describe('[MessageHeader Component]', () => {
it('renders without crashing', () => {
render(<MessageHeader>Test header</MessageHeader>);
});

describe('Accessibility', () => {
it('should have no a11y violations', async () => {
const { container } = render(
<MessageHeader>Test header content</MessageHeader>
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});

it('should have role="group"', () => {
const { container } = render(
<MessageHeader>Test header</MessageHeader>
);
const header = container.querySelector('.rcx-message-header');
expect(header).toHaveAttribute('role', 'group');
});

it('should have aria-label', () => {
const { container } = render(
<MessageHeader>Test header</MessageHeader>
);
const header = container.querySelector('.rcx-message-header');
expect(header).toHaveAttribute('aria-label', 'Message header');
});

it('should render children correctly', () => {
const { getByText } = render(
<MessageHeader>
<span>Author Name</span>
<span>Timestamp</span>
</MessageHeader>
);
expect(getByText('Author Name')).toBeInTheDocument();
expect(getByText('Timestamp')).toBeInTheDocument();
});
});
});
7 changes: 6 additions & 1 deletion packages/fuselage/src/components/Message/MessageHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ import type { HTMLAttributes } from 'react';
export type MessageHeaderProps = HTMLAttributes<HTMLDivElement>;

const MessageHeader = ({ children, ...props }: MessageHeaderProps) => (
<div className='rcx-box rcx-box--full rcx-message-header' {...props}>
<div
className='rcx-box rcx-box--full rcx-message-header'
role="group"
aria-label="Message header"
{...props}
>
<div className='rcx-box rcx-box--full rcx-message-header__wrapper'>
{children}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ export type MessageMetricsProps = HTMLAttributes<HTMLDivElement>;
const MessageMetrics = Object.assign(
(props: MessageMetricsProps) => (
<MessageMetricsContentItem>
<div className='rcx-message-metrics__content-wrapper' {...props} />
<div
className='rcx-message-metrics__content-wrapper'
role='group'
aria-label='Message metrics'
{...props}
/>
</MessageMetricsContentItem>
),
{
Expand Down
Loading