Skip to content

Commit

Permalink
Refactor DateInput internals
Browse files Browse the repository at this point in the history
  • Loading branch information
connor-baer committed Oct 25, 2024
1 parent 1bd13c1 commit a52c8c0
Show file tree
Hide file tree
Showing 15 changed files with 1,074 additions and 556 deletions.
175 changes: 113 additions & 62 deletions packages/circuit-ui/components/DateInput/DateInput.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { render, screen, axe, userEvent } from '../../util/test-utils.js';
import { DateInput } from './DateInput.js';

describe('DateInput', () => {
const baseProps = {
const props = {
onChange: vi.fn(),
label: 'Date of birth',
yearInputLabel: 'Year',
Expand All @@ -42,7 +42,7 @@ describe('DateInput', () => {

it('should forward a ref', () => {
const ref = createRef<HTMLDivElement>();
const { container } = render(<DateInput {...baseProps} ref={ref} />);
const { container } = render(<DateInput {...props} ref={ref} />);
// eslint-disable-next-line testing-library/no-container
const wrapper = container.querySelectorAll('div')[0];
expect(ref.current).toBe(wrapper);
Expand All @@ -51,7 +51,7 @@ describe('DateInput', () => {
it('should merge a custom class name with the default ones', () => {
const className = 'foo';
const { container } = render(
<DateInput {...baseProps} className={className} />,
<DateInput {...props} className={className} />,
);
// eslint-disable-next-line testing-library/no-container
const wrapper = container.querySelectorAll('div')[0];
Expand All @@ -61,7 +61,7 @@ describe('DateInput', () => {
describe('semantics', () => {
it('should optionally have an accessible description', () => {
const description = 'Description';
render(<DateInput {...baseProps} validationHint={description} />);
render(<DateInput {...props} validationHint={description} />);
const fieldset = screen.getByRole('group');
const inputs = screen.getAllByRole('spinbutton');

Expand All @@ -76,7 +76,7 @@ describe('DateInput', () => {
const customDescriptionId = 'customDescriptionId';
render(
<>
<DateInput {...baseProps} aria-describedby={customDescriptionId} />,
<DateInput {...props} aria-describedby={customDescriptionId} />,
<span id={customDescriptionId}>{customDescription}</span>
</>,
);
Expand All @@ -96,7 +96,7 @@ describe('DateInput', () => {
render(
<>
<DateInput
{...baseProps}
{...props}
validationHint={description}
aria-describedby={customDescriptionId}
/>
Expand All @@ -117,102 +117,130 @@ describe('DateInput', () => {
});

it('should render as disabled', async () => {
render(<DateInput {...baseProps} disabled />);
render(<DateInput {...props} disabled />);
expect(screen.getByLabelText(/day/i)).toBeDisabled();
expect(screen.getByLabelText(/month/i)).toBeDisabled();
expect(screen.getByLabelText(/year/i)).toBeDisabled();
expect(
screen.getByRole('button', { name: baseProps.openCalendarButtonLabel }),
screen.getByRole('button', { name: props.openCalendarButtonLabel }),
).toHaveAttribute('aria-disabled', 'true');
});

it('should render as read-only', async () => {
render(<DateInput {...baseProps} readOnly />);
render(<DateInput {...props} readOnly />);
expect(screen.getByLabelText(/day/i)).toHaveAttribute('readonly');
expect(screen.getByLabelText(/month/i)).toHaveAttribute('readonly');
expect(screen.getByLabelText(/year/i)).toHaveAttribute('readonly');
expect(
screen.getByRole('button', { name: baseProps.openCalendarButtonLabel }),
screen.getByRole('button', { name: props.openCalendarButtonLabel }),
).toHaveAttribute('aria-disabled', 'true');
});

it('should render as invalid', async () => {
render(<DateInput {...baseProps} invalid />);
render(<DateInput {...props} invalid />);
expect(screen.getByLabelText(/day/i)).toBeInvalid();
expect(screen.getByLabelText(/month/i)).toBeInvalid();
expect(screen.getByLabelText(/year/i)).toBeInvalid();
});

it('should render as required', async () => {
render(<DateInput {...baseProps} required />);
render(<DateInput {...props} required />);
expect(screen.getByLabelText(/day/i)).toBeRequired();
expect(screen.getByLabelText(/month/i)).toBeRequired();
expect(screen.getByLabelText(/year/i)).toBeRequired();
});

it('should have relevant minimum input values', () => {
render(<DateInput {...baseProps} min="2000-01-01" />);
expect(screen.getByLabelText(/day/i)).toHaveAttribute('min', '1');
expect(screen.getByLabelText(/month/i)).toHaveAttribute('min', '1');
expect(screen.getByLabelText(/year/i)).toHaveAttribute('min', '2000');
render(<DateInput {...props} min="2000-01-01" />);
expect(screen.getByLabelText(/day/i)).toHaveAttribute(
'aria-valuemin',
'1',
);
expect(screen.getByLabelText(/month/i)).toHaveAttribute(
'aria-valuemin',
'1',
);
expect(screen.getByLabelText(/year/i)).toHaveAttribute(
'aria-valuemin',
'2000',
);
});

it('should have relevant maximum input values', () => {
render(<DateInput {...baseProps} max="2001-01-01" />);
expect(screen.getByLabelText(/day/i)).toHaveAttribute('max', '31');
expect(screen.getByLabelText(/month/i)).toHaveAttribute('max', '12');
expect(screen.getByLabelText(/year/i)).toHaveAttribute('max', '2001');
render(<DateInput {...props} max="2001-01-01" />);
expect(screen.getByLabelText(/day/i)).toHaveAttribute(
'aria-valuemax',
'31',
);
expect(screen.getByLabelText(/month/i)).toHaveAttribute(
'aria-valuemax',
'12',
);
expect(screen.getByLabelText(/year/i)).toHaveAttribute(
'aria-valuemax',
'2001',
);
});

it('should mark the year input as readonly when the minimum and maximum dates have the same year', () => {
render(<DateInput {...baseProps} min="2000-04-29" max="2000-06-15" />);
it.skip('should mark the year input as readonly when the minimum and maximum dates have the same year', () => {
render(<DateInput {...props} min="2000-04-29" max="2000-06-15" />);
expect(screen.getByLabelText(/year/i)).toHaveAttribute('readonly');
expect(screen.getByLabelText(/month/i)).toHaveAttribute('min', '4');
expect(screen.getByLabelText(/month/i)).toHaveAttribute('max', '6');
expect(screen.getByLabelText(/month/i)).toHaveAttribute(
'aria-valuemin',
'4',
);
expect(screen.getByLabelText(/month/i)).toHaveAttribute(
'aria-valuemax',
'6',
);
});

it('should mark the year and month inputs as readonly when the minimum and maximum dates have the same year and month', () => {
render(<DateInput {...baseProps} min="2000-04-09" max="2000-04-27" />);
it.skip('should mark the year and month inputs as readonly when the minimum and maximum dates have the same year and month', () => {
render(<DateInput {...props} min="2000-04-09" max="2000-04-27" />);
expect(screen.getByLabelText(/year/i)).toHaveAttribute('readonly');
expect(screen.getByLabelText(/month/i)).toHaveAttribute('readonly');
expect(screen.getByLabelText(/day/i)).toHaveAttribute('min', '9');
expect(screen.getByLabelText(/day/i)).toHaveAttribute('max', '27');
expect(screen.getByLabelText(/day/i)).toHaveAttribute(
'aria-valuemin',
'9',
);
expect(screen.getByLabelText(/day/i)).toHaveAttribute(
'aria-valuemax',
'27',
);
});
});

describe('state', () => {
it('should display a default value', () => {
render(<DateInput {...baseProps} defaultValue="2000-01-12" />);
render(<DateInput {...props} defaultValue="2000-01-12" />);

expect(screen.getByLabelText(/day/i)).toHaveValue(12);
expect(screen.getByLabelText(/month/i)).toHaveValue(1);
expect(screen.getByLabelText(/year/i)).toHaveValue(2000);
expect(screen.getByLabelText(/day/i)).toHaveValue('12');
expect(screen.getByLabelText(/month/i)).toHaveValue('1');
expect(screen.getByLabelText(/year/i)).toHaveValue('2000');
});

it('should display an initial value', () => {
render(<DateInput {...baseProps} value="2000-01-12" />);
render(<DateInput {...props} value="2000-01-12" />);

expect(screen.getByLabelText(/day/i)).toHaveValue(12);
expect(screen.getByLabelText(/month/i)).toHaveValue(1);
expect(screen.getByLabelText(/year/i)).toHaveValue(2000);
expect(screen.getByLabelText(/day/i)).toHaveValue('12');
expect(screen.getByLabelText(/month/i)).toHaveValue('1');
expect(screen.getByLabelText(/year/i)).toHaveValue('2000');
});

it('should update the displayed value', () => {
const { rerender } = render(
<DateInput {...baseProps} value="2000-01-12" />,
);
const { rerender } = render(<DateInput {...props} value="2000-01-12" />);

rerender(<DateInput {...baseProps} value="2000-01-15" />);
rerender(<DateInput {...props} value="2000-01-15" />);

expect(screen.getByLabelText(/day/i)).toHaveValue(15);
expect(screen.getByLabelText(/month/i)).toHaveValue(1);
expect(screen.getByLabelText(/year/i)).toHaveValue(2000);
expect(screen.getByLabelText(/day/i)).toHaveValue('15');
expect(screen.getByLabelText(/month/i)).toHaveValue('1');
expect(screen.getByLabelText(/year/i)).toHaveValue('2000');
});
});

describe('user interactions', () => {
it('should focus the first input when clicking the label', async () => {
render(<DateInput {...baseProps} />);
render(<DateInput {...props} />);

await userEvent.click(screen.getByText('Date of birth'));

Expand All @@ -222,7 +250,7 @@ describe('DateInput', () => {
it('should allow users to type a date', async () => {
const onChange = vi.fn();

render(<DateInput {...baseProps} onChange={onChange} />);
render(<DateInput {...props} onChange={onChange} />);

await userEvent.type(screen.getByLabelText('Year'), '2017');
await userEvent.type(screen.getByLabelText('Month'), '8');
Expand All @@ -232,23 +260,52 @@ describe('DateInput', () => {
});

it('should update the minimum and maximum input values as the user types', async () => {
render(<DateInput {...baseProps} min="2000-04-29" max="2001-02-15" />);
render(<DateInput {...props} min="2000-04-29" max="2001-02-15" />);

await userEvent.type(screen.getByLabelText(/year/i), '2001');

expect(screen.getByLabelText(/month/i)).toHaveAttribute('min', '1');
expect(screen.getByLabelText(/month/i)).toHaveAttribute('max', '2');
expect(screen.getByLabelText(/month/i)).toHaveAttribute(
'aria-valuemin',
'1',
);
expect(screen.getByLabelText(/month/i)).toHaveAttribute(
'aria-valuemax',
'2',
);

await userEvent.type(screen.getByLabelText(/month/i), '2');

expect(screen.getByLabelText(/day/i)).toHaveAttribute('min', '1');
expect(screen.getByLabelText(/day/i)).toHaveAttribute('max', '15');
expect(screen.getByLabelText(/day/i)).toHaveAttribute(
'aria-valuemin',
'1',
);
expect(screen.getByLabelText(/day/i)).toHaveAttribute(
'aria-valuemax',
'15',
);
});

it('should allow users to delete the date', async () => {
const onChange = vi.fn();

render(
<DateInput {...props} defaultValue="2000-01-12" onChange={onChange} />,
);

await userEvent.click(screen.getByLabelText(/year/i));
await userEvent.keyboard(Array(9).fill('{backspace}').join(''));

expect(screen.getByLabelText(/day/i)).toHaveValue('');
expect(screen.getByLabelText(/month/i)).toHaveValue('');
expect(screen.getByLabelText(/year/i)).toHaveValue('');

expect(onChange).toHaveBeenCalledWith('');
});

it('should allow users to select a date on a calendar', async () => {
const onChange = vi.fn();

render(<DateInput {...baseProps} onChange={onChange} />);
render(<DateInput {...props} onChange={onChange} />);

const openCalendarButton = screen.getByRole('button', {
name: /change date/i,
Expand All @@ -268,11 +325,7 @@ describe('DateInput', () => {
const onChange = vi.fn();

render(
<DateInput
{...baseProps}
defaultValue="2000-01-12"
onChange={onChange}
/>,
<DateInput {...props} defaultValue="2000-01-12" onChange={onChange} />,
);

const openCalendarButton = screen.getByRole('button', {
Expand All @@ -292,33 +345,31 @@ describe('DateInput', () => {

describe('status messages', () => {
it('should render an empty live region on mount', () => {
render(<DateInput {...baseProps} />);
render(<DateInput {...props} />);
const liveRegionEl = screen.getByRole('status');

expect(liveRegionEl).toBeEmptyDOMElement();
});

it('should render status messages in a live region', () => {
const statusMessage = 'This field is required';
render(
<DateInput {...baseProps} invalid validationHint={statusMessage} />,
);
render(<DateInput {...props} invalid validationHint={statusMessage} />);
const liveRegionEl = screen.getByRole('status');

expect(liveRegionEl).toHaveTextContent(statusMessage);
});

it('should not render descriptions in a live region', () => {
const statusMessage = 'This field is required';
render(<DateInput {...baseProps} validationHint={statusMessage} />);
render(<DateInput {...props} validationHint={statusMessage} />);
const liveRegionEl = screen.getByRole('status');

expect(liveRegionEl).toBeEmptyDOMElement();
});
});

it('should have no accessibility violations', async () => {
const { container } = render(<DateInput {...baseProps} />);
const { container } = render(<DateInput {...props} />);
const actual = await axe(container);
expect(actual).toHaveNoViolations();
});
Expand Down
Loading

0 comments on commit a52c8c0

Please sign in to comment.