Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TASK-1046] Introduce unit tests for UI components #5120

Merged
merged 11 commits into from
Oct 1, 2024
54 changes: 32 additions & 22 deletions jsapp/js/components/common/button.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,55 +4,65 @@ import {render, screen} from '@testing-library/react';
import {describe, it, expect, jest} from '@jest/globals';
import userEvent from '@testing-library/user-event';

describe('Button', () => {
it('Should render and be clickable when enabled', async () => {
const user = userEvent.setup();
const user = userEvent.setup();

// Mock
const handleClickFunction = jest.fn();
// Mock
const handleClickFunction = jest.fn();

// Render

describe('Enabled button', () => {
beforeEach(() => {
pauloamorimbr marked this conversation as resolved.
Show resolved Hide resolved
render(
<Button
type={'primary'}
size={'l'}
type='primary'
size='l'
label='Button Label'
onClick={handleClickFunction}
/>
);
});

it('should render', async () => {
// Assert
expect(screen.getByLabelText('Button Label')).toBeInTheDocument();
});

it('should be clickable', async () => {
handleClickFunction.mockReset();

// Act
const button = screen.getByLabelText('Button Label');
await user.click(button);

// Assert
expect(button).toBeInTheDocument();
expect(handleClickFunction).toHaveBeenCalled();
expect(handleClickFunction).toHaveBeenCalledTimes(1);
});
});

it('Should render and not be clickable when disabled', async () => {
const user = userEvent.setup();

// Mock
const handleClickFunction = jest.fn();

// Render
describe('Disabled button', () => {
beforeEach(() => {
render(
<Button
type={'primary'}
size={'l'}
type='primary'
size='l'
label='Button Label'
onClick={handleClickFunction}
isDisabled
/>
);
});

it('should render', async () => {
// Assert
expect(screen.getByLabelText('Button Label')).toBeInTheDocument();
});

it('should not be clickable', async () => {
handleClickFunction.mockReset();

// Act
const button = screen.getByLabelText('Button Label');
await user.click(button);

// Assert
expect(button).toBeInTheDocument();
expect(handleClickFunction).not.toHaveBeenCalled();
});
});
176 changes: 81 additions & 95 deletions jsapp/js/components/special/koboAccessibleSelect.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import KoboSelect3 from './koboAccessibleSelect';
import {useState} from 'react';

const options: KoboSelectOption[] = [
{value: '1', label: 'Option 1'},
{value: '2', label: 'Option 2'},
{value: '3', label: 'Option 3'},
{value: '1', label: 'Apple'},
{value: '2', label: 'Banana'},
{value: '3', label: 'Avocado'},
];

// A wrapper is needed for the component to retain value changes
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📓 Good point — many of our Kobo components are like this — 'controlled'-only. It'd be possible to make them more flexible.

'Course, then there would be more to test 😅

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a common practice I found for single component testing, since most of the components will depend on external context anyways. So I really don't think it's the case of making them 'uncontrolled' just for the sake of tests. We only need to be careful to not create biased tests that could influence the component behavior. (but yeah, cool article! 😄)

Expand All @@ -32,119 +32,105 @@ const Wrapper = ({onChange}: {onChange: (newValue: string) => void}) => {


describe('KoboSelect3', () => {
it('Should respond to mouse interaction', async () => {
const user = userEvent.setup();
const user = userEvent.setup();

// Mock
const scrollIntoViewMock = jest.fn();
window.HTMLElement.prototype.scrollIntoView = scrollIntoViewMock;
const onChangeMock = jest.fn();
// Mock
const scrollIntoViewMock = jest.fn();
window.HTMLElement.prototype.scrollIntoView = scrollIntoViewMock;
const onChangeMock = jest.fn();

// Render
beforeEach(() => {
render(<Wrapper onChange={onChangeMock} />);
});

// Actors
it('should render with proper placeholder', async () => {
const trigger = screen.getByRole('combobox');
const triggerLabel = trigger.querySelector('label');
const list = screen.getByRole('listbox');
const listOptions = screen.getAllByRole('option');


// Trigger should be present and have the correct placeholder
expect(trigger).toBeInTheDocument();
expect(triggerLabel).toHaveTextContent('Select…');
});


// There should be 3 options
expect(listOptions).toHaveLength(3);

// List should not be expanded on creation
expect(list.dataset.expanded).toBe('false');

// Clicks the trigger
await user.click(trigger);
expect(scrollIntoViewMock).toHaveBeenCalled();

// List should be expanded after click
expect(list.dataset.expanded).toBe('true');

// Select first option
await user.click(listOptions[0]);

// Onchange should be called with the correct value
expect(onChangeMock).lastCalledWith('1');

// List should be collapsed after selection
it('should have the list closed on start', async () => {
const list = screen.getByRole('listbox');
expect(list.dataset.expanded).toBe('false');
});

it('Should respond to keyboard interaction', async () => {
// Mock
const onChangeMock = jest.fn();

// Render
render(<Wrapper onChange={onChangeMock} />);

// Actors
const trigger = screen.getByRole('combobox');
const triggerLabel = trigger.querySelector('label');
const list = screen.getByRole('listbox');
it('should have a list with the correct items count', async () => {
const listOptions = screen.getAllByRole('option');
expect(listOptions).toHaveLength(3);
});

it('should be selectable by mouse click', async () => {
const trigger = screen.getByRole('combobox');
const list = screen.getByRole('listbox');
const listOptions = screen.getAllByRole('option');

// Trigger should be present and have the correct placeholder
expect(trigger).toBeInTheDocument();
expect(triggerLabel).toHaveTextContent('Select…');

// Clicks the trigger
await user.click(trigger);
expect(list.dataset.expanded).toBe('true');

// There should be 3 options
expect(listOptions).toHaveLength(3);
// Select first option
await user.click(listOptions[0]);

// List should not be expanded on creation
expect(list.dataset.expanded).toBe('false');
// Onchange should be called with the correct value
expect(onChangeMock).lastCalledWith(options[0].value);

// Increase option on arrow down
fireEvent.keyDown(trigger, {key: 'ArrowDown'});
expect(onChangeMock).lastCalledWith('1');
fireEvent.keyDown(trigger, {key: 'ArrowDown'});
expect(onChangeMock).lastCalledWith('2');
fireEvent.keyDown(trigger, {key: 'ArrowDown'});
expect(onChangeMock).lastCalledWith('3');

// Decrease option on arrow up
fireEvent.keyDown(trigger, {key: 'ArrowUp'});
expect(onChangeMock).lastCalledWith('2');
fireEvent.keyDown(trigger, {key: 'ArrowUp'});
expect(onChangeMock).lastCalledWith('1');

// Open list on Alt + ArrowDown
fireEvent.keyDown(trigger, {key: 'ArrowDown', altKey: true});
expect(list.dataset.expanded).toBe('true');

// Close list on Escape
fireEvent.keyDown(trigger, {key: 'Escape'});
expect(list.dataset.expanded).toBe('false');
expect(list.dataset.expanded).toBe('false');
});

// Toggle list on Alt + ArrowDown
fireEvent.keyDown(trigger, {key: 'ArrowDown', altKey: true});
expect(list.dataset.expanded).toBe('true');
fireEvent.keyDown(trigger, {key: 'ArrowDown', altKey: true});
expect(list.dataset.expanded).toBe('false');
it('should be selectable by keyboard arrows', async () => {
const trigger = screen.getByRole('combobox');
const triggerLabel = trigger.querySelector('label');

// No item selected
expect(triggerLabel).toHaveTextContent('Select…');

// Increase option on arrow down
fireEvent.keyDown(trigger, {key: 'ArrowDown'});
expect(onChangeMock).lastCalledWith(options[0].value);
expect(triggerLabel).toHaveTextContent(options[0].label);

// Increase option on arrow right
fireEvent.keyDown(trigger, {key: 'ArrowRight'});
expect(onChangeMock).lastCalledWith(options[1].value);
expect(triggerLabel).toHaveTextContent(options[1].label);
fireEvent.keyDown(trigger, {key: 'ArrowRight'});
expect(onChangeMock).lastCalledWith(options[2].value);
expect(triggerLabel).toHaveTextContent(options[2].label);

// Don't go past the last one
onChangeMock.mockReset();
fireEvent.keyDown(trigger, {key: 'ArrowDown'});
expect(onChangeMock).not.toHaveBeenCalled();
expect(triggerLabel).toHaveTextContent(options[2].label);

// Decrease option on arrow up
fireEvent.keyDown(trigger, {key: 'ArrowUp'});
expect(onChangeMock).lastCalledWith(options[1].value);
expect(triggerLabel).toHaveTextContent(options[1].label);

// Decrease option on arrow left
fireEvent.keyDown(trigger, {key: 'ArrowLeft'});
expect(onChangeMock).lastCalledWith(options[0].value);
expect(triggerLabel).toHaveTextContent(options[0].label);

// Don't go past the first one
onChangeMock.mockReset();
fireEvent.keyDown(trigger, {key: 'ArrowUp'});
expect(onChangeMock).not.toHaveBeenCalled();
expect(triggerLabel).toHaveTextContent(options[0].label);
});

// Toggle list on Alt + ArrowUp
fireEvent.keyDown(trigger, {key: 'ArrowUp', altKey: true});
expect(list.dataset.expanded).toBe('true');
fireEvent.keyDown(trigger, {key: 'ArrowUp', altKey: true});
expect(list.dataset.expanded).toBe('false');
it('should be selectable by typing', async () => {
const trigger = screen.getByRole('combobox');
const triggerLabel = trigger.querySelector('label');

// Open menu, navigate and select option on Enter
fireEvent.keyDown(trigger, {key: 'ArrowDown', altKey: true});
fireEvent.keyDown(trigger, {key: 'ArrowDown'});
fireEvent.keyDown(trigger, {key: 'Enter'});
expect(onChangeMock).lastCalledWith('2');
// No item selected
expect(triggerLabel).toHaveTextContent('Select…');

// List should be collapsed after selection
expect(list.dataset.expanded).toBe('false');
// Type 'b' to select Banana
fireEvent.keyDown(trigger, {key: 'b'});
expect(onChangeMock).lastCalledWith(options[1].value);
expect(triggerLabel).toHaveTextContent(options[1].label);
});

});
p2edwards marked this conversation as resolved.
Show resolved Hide resolved
2 changes: 1 addition & 1 deletion jsapp/js/components/special/koboAccessibleSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@ interface KoboSelect3Props {
// 'data-cy'?: string; // not yet needed
}

// Needs to be exported to be referenced in the test file.
/** Needs to be exported to be referenced in the test file. */
export interface KoboSelectOption {
/** Must be unique! */
value: string;
Expand Down
Loading